@etus/bhono-app 0.1.5 → 0.1.6

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 (269) hide show
  1. package/dist/index.js +0 -0
  2. package/package.json +5 -1
  3. package/templates/base/.husky/pre-push +26 -0
  4. package/templates/base/CLAUDE.md +5 -5
  5. package/templates/base/README.md +31 -20
  6. package/templates/base/docs/app_spec.txt +13 -10
  7. package/templates/base/docs/architecture/README.md +3 -0
  8. package/templates/base/docs/architecture/data-requirements.md +4 -3
  9. package/templates/base/docs/architecture/db-bootstrap.md +39 -0
  10. package/templates/base/docs/architecture/drizzle-migration-plan.md +125 -0
  11. package/templates/base/docs/architecture/erd.md +1 -1
  12. package/templates/base/docs/architecture/sql-standards.md +100 -0
  13. package/templates/base/docs/testing.md +36 -29
  14. package/templates/base/package.json +6 -5
  15. package/templates/base/pnpm-lock.yaml +0 -123
  16. package/templates/base/schema.sql +84 -0
  17. package/templates/base/scripts/init.sh +244 -59
  18. package/templates/base/src/client/hooks/use-auth.ts +5 -0
  19. package/templates/base/src/client/routes/_authenticated/dashboard.tsx +1 -1
  20. package/templates/base/src/client/routes/index.tsx +1 -1
  21. package/templates/base/src/server/db/client.ts +3 -5
  22. package/templates/base/src/server/db/records.ts +81 -0
  23. package/templates/base/src/server/db/seed.ts +3 -2
  24. package/templates/base/src/server/db/sql.ts +96 -0
  25. package/templates/base/src/server/index.ts +16 -2
  26. package/templates/base/src/server/lib/audit.ts +74 -26
  27. package/templates/base/src/server/lib/audited-db.ts +219 -109
  28. package/templates/base/src/server/lib/transaction.ts +10 -16
  29. package/templates/base/src/server/middleware/account.ts +8 -15
  30. package/templates/base/src/server/middleware/auth.ts +102 -38
  31. package/templates/base/src/server/middleware/rate-limit.ts +6 -1
  32. package/templates/base/src/server/routes/accounts/handlers.ts +18 -6
  33. package/templates/base/src/server/routes/audits/handlers.ts +3 -1
  34. package/templates/base/src/server/routes/auth/handlers.ts +14 -9
  35. package/templates/base/src/server/routes/auth/test-login.ts +99 -45
  36. package/templates/base/src/server/routes/health/handlers.ts +4 -4
  37. package/templates/base/src/server/routes/invitations/handlers.ts +6 -3
  38. package/templates/base/src/server/routes/users/handlers.ts +21 -14
  39. package/templates/base/src/server/services/accounts.ts +242 -217
  40. package/templates/base/src/server/services/audits.ts +114 -61
  41. package/templates/base/src/server/services/auth.ts +310 -180
  42. package/templates/base/src/server/services/invitations.ts +282 -222
  43. package/templates/base/src/server/services/users.ts +383 -293
  44. package/templates/base/src/server/types/index.ts +1 -2
  45. package/templates/base/{src/server/__tests__/fixtures.ts → tests/fixtures/server.ts} +3 -3
  46. package/templates/base/{src/client/__tests__/setup-browser.ts → tests/helpers/client-setup-browser.ts} +2 -2
  47. package/templates/base/{src/client/__tests__/setup.ts → tests/helpers/client-setup.ts} +1 -1
  48. package/templates/base/{src/client/__tests__/test-utils.tsx → tests/helpers/client-test-utils.tsx} +2 -2
  49. package/templates/base/{src/server/__tests__/setup.ts → tests/helpers/server.ts} +9 -9
  50. package/templates/base/tests/integration/accounts/crud.test.ts +2 -11
  51. package/templates/base/tests/integration/audits/list.test.ts +2 -11
  52. package/templates/base/tests/integration/auth/auth-service.test.ts +1 -10
  53. package/templates/base/tests/integration/auth/invitation-token.test.ts +2 -11
  54. package/templates/base/tests/integration/auth/logout.test.ts +2 -11
  55. package/templates/base/tests/integration/auth/oauth.test.ts +23 -42
  56. package/templates/base/tests/integration/auth/refresh-token.test.ts +1 -9
  57. package/templates/base/tests/integration/auth/session-expiry.test.ts +1 -9
  58. package/templates/base/tests/integration/auth/session.test.ts +2 -11
  59. package/templates/base/tests/integration/auth/super-admin.test.ts +1 -9
  60. package/templates/base/tests/integration/authorization/analytics-role.test.ts +2 -11
  61. package/templates/base/tests/integration/authorization/billing-role.test.ts +2 -11
  62. package/templates/base/tests/integration/authorization/guards-roles.test.ts +1 -9
  63. package/templates/base/tests/integration/authorization/multi-tenancy.test.ts +2 -11
  64. package/templates/base/tests/integration/authorization/roles.test.ts +2 -11
  65. package/templates/base/tests/integration/config/production-behavior.test.ts +2 -11
  66. package/templates/base/tests/integration/health/health.test.ts +25 -44
  67. package/templates/base/tests/integration/invitations/crud.test.ts +2 -11
  68. package/templates/base/tests/integration/invitations/email.test.ts +1 -9
  69. package/templates/base/tests/integration/middleware/auth.test.ts +3 -12
  70. package/templates/base/tests/integration/middleware/request-logger.test.ts +1 -9
  71. package/templates/base/tests/integration/performance/response-times.test.ts +1 -9
  72. package/templates/base/tests/integration/security/cookie-security.test.ts +2 -11
  73. package/templates/base/tests/integration/security/csrf-protection.test.ts +2 -11
  74. package/templates/base/tests/integration/security/log-sanitization.test.ts +1 -9
  75. package/templates/base/tests/integration/security/rate-limiting.test.ts +1 -9
  76. package/templates/base/tests/integration/security/sql-injection.test.ts +7 -18
  77. package/templates/base/tests/integration/security/xss-prevention.test.ts +2 -11
  78. package/templates/base/tests/integration/setup.ts +13 -90
  79. package/templates/base/tests/integration/smoke.test.ts +3 -2
  80. package/templates/base/tests/integration/storage/upload.test.ts +2 -11
  81. package/templates/base/tests/integration/storage/validation.test.ts +2 -11
  82. package/templates/base/tests/integration/users/crud.test.ts +2 -11
  83. package/templates/base/tests/integration/users/list.test.ts +2 -11
  84. package/templates/base/tests/integration/vitest.config.ts +2 -9
  85. package/templates/base/{src/server/__tests__ → tests}/mocks/db.ts +1 -1
  86. package/templates/base/{src/server/__tests__ → tests}/mocks/index.ts +1 -1
  87. package/templates/base/{src/server/__tests__ → tests}/mocks/kv.ts +1 -1
  88. package/templates/base/{src/server/__tests__ → tests}/mocks/r2.ts +1 -1
  89. package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/sidebar.test.tsx +1 -1
  90. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/avatar.test.tsx +1 -1
  91. package/templates/base/{src/client/__tests__ → tests/unit/client/components/ui}/button.test.tsx +1 -1
  92. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/card.test.tsx +1 -1
  93. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/dialog.test.tsx +1 -1
  94. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/input.test.tsx +1 -1
  95. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/loading-skeleton.test.tsx +1 -1
  96. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/skeleton.test.tsx +1 -1
  97. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/sonner.test.tsx +1 -1
  98. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/tabs.test.tsx +1 -1
  99. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/account.test.tsx +1 -1
  100. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/integrations.test.tsx +1 -1
  101. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/settings.test.tsx +1 -1
  102. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/team.test.tsx +1 -1
  103. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/authenticated-layout.test.tsx +1 -1
  104. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/dashboard.test.tsx +1 -1
  105. package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/invite.test.tsx +1 -1
  106. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/login.test.tsx +1 -1
  107. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/navigation.test.tsx +1 -1
  108. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/root-layout.test.tsx +1 -1
  109. package/templates/base/{src/server/auth/__tests__ → tests/unit/server/auth}/guards.test.ts +2 -2
  110. package/templates/base/{src → tests/unit}/server/auth/permissions.test.ts +1 -1
  111. package/templates/base/{src → tests/unit}/server/auth/roles.test.ts +1 -1
  112. package/templates/base/tests/unit/server/db/sql.test.ts +68 -0
  113. package/templates/base/{src → tests/unit}/server/env.test.ts +1 -1
  114. package/templates/base/tests/unit/server/lib/audited-db.test.ts +78 -0
  115. package/templates/base/{src → tests/unit}/server/lib/email.test.ts +1 -1
  116. package/templates/base/{src → tests/unit}/server/lib/errors.test.ts +1 -1
  117. package/templates/base/{src → tests/unit}/server/lib/oauth.test.ts +1 -1
  118. package/templates/base/{src → tests/unit}/server/lib/pagination.test.ts +1 -1
  119. package/templates/base/{src → tests/unit}/server/lib/password.test.ts +1 -1
  120. package/templates/base/{src → tests/unit}/server/lib/providers.test.ts +1 -1
  121. package/templates/base/{src → tests/unit}/server/lib/r2-storage.test.ts +2 -2
  122. package/templates/base/{src → tests/unit}/server/lib/session.test.ts +2 -2
  123. package/templates/base/{src → tests/unit}/server/lib/tokens.test.ts +1 -1
  124. package/templates/base/{src → tests/unit}/server/lib/transaction.test.ts +5 -14
  125. package/templates/base/{src → tests/unit}/server/middleware/account.test.ts +16 -24
  126. package/templates/base/{src → tests/unit}/server/middleware/auth.test.ts +71 -42
  127. package/templates/base/{src → tests/unit}/server/middleware/cors.test.ts +1 -1
  128. package/templates/base/{src → tests/unit}/server/middleware/error-handler.test.ts +2 -2
  129. package/templates/base/{src → tests/unit}/server/middleware/rate-limit.test.ts +3 -2
  130. package/templates/base/{src → tests/unit}/server/middleware/request-context.test.ts +1 -1
  131. package/templates/base/{src → tests/unit}/server/middleware/request-logger.test.ts +1 -1
  132. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/db.test.ts +1 -1
  133. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/kv.test.ts +1 -1
  134. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/r2.test.ts +1 -1
  135. package/templates/base/{src/server/routes/accounts/__tests__ → tests/unit/server/routes/accounts}/handlers.test.ts +12 -12
  136. package/templates/base/{src/server/routes/audits/__tests__ → tests/unit/server/routes/audits}/handlers.test.ts +11 -11
  137. package/templates/base/{src/server/routes/auth/__tests__ → tests/unit/server/routes/auth}/handlers.test.ts +13 -13
  138. package/templates/base/{src/server/routes/health/__tests__ → tests/unit/server/routes/health}/handlers.test.ts +27 -23
  139. package/templates/base/{src/server/routes/invitations/__tests__ → tests/unit/server/routes/invitations}/handlers.test.ts +14 -17
  140. package/templates/base/{src/server/routes/storage/__tests__ → tests/unit/server/routes/storage}/handlers.test.ts +6 -6
  141. package/templates/base/{src/server/routes/users/__tests__ → tests/unit/server/routes/users}/handlers.test.ts +12 -12
  142. package/templates/base/tests/unit/server/services/accounts.test.ts +258 -0
  143. package/templates/base/tests/unit/server/services/audits.test.ts +141 -0
  144. package/templates/base/tests/unit/server/services/auth.test.ts +179 -0
  145. package/templates/base/tests/unit/server/services/invitations.test.ts +165 -0
  146. package/templates/base/tests/unit/server/services/users.test.ts +351 -0
  147. package/templates/base/tsconfig.json +2 -1
  148. package/templates/base/vitest.config.browser.ts +3 -2
  149. package/templates/base/vitest.config.frontend.ts +3 -2
  150. package/templates/base/vitest.config.ts +7 -14
  151. package/templates/base/.claude/settings.local.json +0 -11
  152. package/templates/base/config/drizzle.config.ts +0 -10
  153. package/templates/base/src/server/db/schema/accounts.ts +0 -20
  154. package/templates/base/src/server/db/schema/audit-logs.ts +0 -26
  155. package/templates/base/src/server/db/schema/index.ts +0 -7
  156. package/templates/base/src/server/db/schema/invitations.ts +0 -30
  157. package/templates/base/src/server/db/schema/refresh-tokens.ts +0 -22
  158. package/templates/base/src/server/db/schema/user-accounts.ts +0 -25
  159. package/templates/base/src/server/db/schema/users.ts +0 -33
  160. package/templates/base/src/server/lib/audited-db.test.ts +0 -107
  161. package/templates/base/src/server/lib/schema-helpers.ts +0 -16
  162. package/templates/base/src/server/services/__tests__/accounts.test.ts +0 -764
  163. package/templates/base/src/server/services/__tests__/audits.test.ts +0 -235
  164. package/templates/base/src/server/services/__tests__/auth.test.ts +0 -765
  165. package/templates/base/src/server/services/__tests__/invitations.test.ts +0 -704
  166. package/templates/base/src/server/services/__tests__/users.test.ts +0 -755
  167. package/templates/base/tests/integration/lib/schema-helpers.test.ts +0 -129
  168. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-can-be-collapsed-by-default-1.png +0 -0
  169. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-expands-when-collapsed-and-expand-button-is-clicked-1.png +0 -0
  170. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-logout-button-click-1.png +0 -0
  171. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-navigation-clicks-1.png +0 -0
  172. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-hides-navigation-labels-when-collapsed-1.png +0 -0
  173. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-highlights-active-route-1.png +0 -0
  174. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-renders-sidebar-navigation-items-1.png +0 -0
  175. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-shows-keyboard-shortcut-hint-when-expanded-1.png +0 -0
  176. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-info-when-authenticated-1.png +0 -0
  177. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-initials-in-avatar-fallback-1.png +0 -0
  178. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/error-boundary.test.tsx +0 -0
  179. /package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/error-fallback.test.tsx +0 -0
  180. /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-auth.test.tsx +0 -0
  181. /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-theme.test.tsx +0 -0
  182. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-dashboard-stats-cards-1.png +0 -0
  183. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-quick-action-cards-1.png +0 -0
  184. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-recent-activity-section-1.png +0 -0
  185. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-user-first-name-in-welcome-message-1.png +0 -0
  186. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-user-information-in-sidebar-1.png +0 -0
  187. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-render-dashboard-when-authenticated-1.png +0 -0
  188. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-show-navigation-sidebar-1.png +0 -0
  189. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-unauthenticated-should-redirect-to-login-when-not-authenticated-1.png +0 -0
  190. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-accept-invitation-button-1.png +0 -0
  191. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-decline-button-linking-to-homepage-1.png +0 -0
  192. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-invitation-details--email--workspace--role--1.png +0 -0
  193. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-have-a-logo-link-to-homepage-1.png +0 -0
  194. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-render-invitation-page-with-inviter-name-and-workspace-1.png +0 -0
  195. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-show-terms-of-service-and-privacy-policy-links-1.png +0 -0
  196. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-display-Google-OAuth-login-button-1.png +0 -0
  197. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-have-a-link-back-to-home-page-1.png +0 -0
  198. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-render-login-content-without-waiting-for-authentication-1.png +0 -0
  199. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-render-login-page-at--login-route-1.png +0 -0
  200. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-show-Terms-of-Service-and-Privacy-Policy-links-1.png +0 -0
  201. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-trigger-OAuth-flow-when-clicking-login-button-1.png +0 -0
  202. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-display-404-text-on-not-found-page-1.png +0 -0
  203. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-have-navigation-options-on-404-page-1.png +0 -0
  204. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-render-404-page-for-unknown-routes-1.png +0 -0
  205. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-account-page-1.png +0 -0
  206. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-integrations-page-1.png +0 -0
  207. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-settings-page-1.png +0 -0
  208. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-team-page-1.png +0 -0
  209. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-home-page-navigation-should-display-navigation-links-on-home-page-1.png +0 -0
  210. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-home-page-navigation-should-have-correct-link-destinations-on-home-page-1.png +0 -0
  211. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-allow-access-to-home-page-without-authentication-1.png +0 -0
  212. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-allow-access-to-login-page-without-authentication-1.png +0 -0
  213. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-dashboard-to-login-1.png +0 -0
  214. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-settings-to-login-1.png +0 -0
  215. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-team-to-login-1.png +0 -0
  216. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-render-Active-Sessions-section-with-session-cards-1.png +0 -0
  217. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-render-page-with-correct-title--Account--1.png +0 -0
  218. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-API-Access-section-1.png +0 -0
  219. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-Connected-Accounts-section-with-Google-connected-1.png +0 -0
  220. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-Danger-Zone-section-with-delete-button-1.png +0 -0
  221. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-Security-section-with-Two-Factor-Authentication-1.png +0 -0
  222. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-API-documentation-section-should-display-API-documentation-link-section-1.png +0 -0
  223. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-Create-Webhook-Dialog-should-have-Add-Webhook-trigger-button-1.png +0 -0
  224. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-category-filters-should-display-all-category-buttons-1.png +0 -0
  225. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-display-all-integration-cards-with-names-1.png +0 -0
  226. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-display-category-badges-on-cards-1.png +0 -0
  227. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Configure-button-for-connected-integrations-1.png +0 -0
  228. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Connect-button-for-not-connected-integrations-1.png +0 -0
  229. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Connected-badge-for-connected-integrations-1.png +0 -0
  230. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-Available-Integrations-section-1.png +0 -0
  231. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-Webhooks-section-1.png +0 -0
  232. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-connected-count-1.png +0 -0
  233. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-page-description-1.png +0 -0
  234. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-search-input-1.png +0 -0
  235. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-render-with-correct-title--Integrations--1.png +0 -0
  236. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-filter-integrations-based-on-search-query-1.png +0 -0
  237. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-search-by-description-1.png +0 -0
  238. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-show-no-results-message-when-search-has-no-matches-1.png +0 -0
  239. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-existing-webhook-1.png +0 -0
  240. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-events-1.png +0 -0
  241. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-last-delivery-info-1.png +0 -0
  242. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-success-status-1.png +0 -0
  243. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-have-Add-Webhook-button-1.png +0 -0
  244. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Account-tab-should-display-connected-accounts-section-with-Google-provider-1.png +0 -0
  245. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Account-tab-should-display-sessions-and-danger-zone-sections-1.png +0 -0
  246. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-all-notification-toggle-options-1.png +0 -0
  247. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-email-notifications-section-1.png +0 -0
  248. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-toggle-switches-for-notification-options-1.png +0 -0
  249. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-have-toggles-checked-by-default-1.png +0 -0
  250. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display--Save-Changes--button-1.png +0 -0
  251. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-personal-information-form-1.png +0 -0
  252. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-profile-picture-section-1.png +0 -0
  253. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-email-in-disabled-email-input-1.png +0 -0
  254. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-initials-in-avatar-1.png +0 -0
  255. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-name-in-the-name-input-1.png +0 -0
  256. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-display-all-three-tabs-1.png +0 -0
  257. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-display-page-description-1.png +0 -0
  258. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-render-with-correct-title--Settings--1.png +0 -0
  259. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-show-Profile-tab-as-default-active-tab-1.png +0 -0
  260. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-switch-to-Account-tab-when-clicked-1.png +0 -0
  261. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-switch-to-Notifications-tab-when-clicked-1.png +0 -0
  262. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-display-Active-Members-section-with-member-count-1.png +0 -0
  263. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-display-Pending-Invitations-section-with-invitation-details-1.png +0 -0
  264. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-have-invite-member-button-that-can-be-clicked-1.png +0 -0
  265. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-render-page-with-correct-title-and-description-1.png +0 -0
  266. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-render-search-input-that-filters-team-members-1.png +0 -0
  267. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-show-current-user-with---you---indicator-and-role-badge-1.png +0 -0
  268. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/error-components.test.tsx +0 -0
  269. /package/templates/base/{src/shared/schemas/__tests__ → tests/unit/shared}/schemas.test.ts +0 -0
@@ -14,13 +14,14 @@ import type {
14
14
  export const listUsersHandler: RouteHandler<typeof listUsersRoute, HonoEnv> = async (c) => {
15
15
  const query = c.req.valid('query')
16
16
  const db = c.get('db')
17
+ const usersDb = c.env.DB ?? db
17
18
  const accountId = c.get('accountId')
18
19
  const user = c.get('user')
19
20
  const transactionId = c.get('transactionId')
20
21
  const ip = c.get('ip')
21
22
  const userAgent = c.get('userAgent')
22
23
 
23
- if (!db || !accountId || !user) {
24
+ if (!db || !usersDb || !accountId || !user) {
24
25
  throw new Error('Missing required context')
25
26
  }
26
27
 
@@ -32,7 +33,7 @@ export const listUsersHandler: RouteHandler<typeof listUsersRoute, HonoEnv> = as
32
33
  userAgent,
33
34
  }
34
35
 
35
- const result = await usersService.findAll(db, ctx, {
36
+ const result = await usersService.findAll(usersDb, ctx, {
36
37
  page: query.page,
37
38
  limit: query.limit,
38
39
  sortBy: query.sortBy,
@@ -46,13 +47,14 @@ export const listUsersHandler: RouteHandler<typeof listUsersRoute, HonoEnv> = as
46
47
  export const getUserHandler: RouteHandler<typeof getUserRoute, HonoEnv> = async (c) => {
47
48
  const { id } = c.req.valid('param')
48
49
  const db = c.get('db')
50
+ const usersDb = c.env.DB ?? db
49
51
  const accountId = c.get('accountId')
50
52
  const user = c.get('user')
51
53
  const transactionId = c.get('transactionId')
52
54
  const ip = c.get('ip')
53
55
  const userAgent = c.get('userAgent')
54
56
 
55
- if (!db || !accountId || !user) {
57
+ if (!db || !usersDb || !accountId || !user) {
56
58
  throw new Error('Missing required context')
57
59
  }
58
60
 
@@ -64,7 +66,7 @@ export const getUserHandler: RouteHandler<typeof getUserRoute, HonoEnv> = async
64
66
  userAgent,
65
67
  }
66
68
 
67
- const foundUser = await usersService.findById(db, ctx, id)
69
+ const foundUser = await usersService.findById(usersDb, ctx, id)
68
70
  return c.json({ data: foundUser }, 200)
69
71
  }
70
72
 
@@ -100,13 +102,14 @@ export const updateUserHandler: RouteHandler<typeof updateUserRoute, HonoEnv> =
100
102
  const { id } = c.req.valid('param')
101
103
  const data = c.req.valid('json')
102
104
  const db = c.get('db')
105
+ const usersDb = c.env.DB ?? db
103
106
  const accountId = c.get('accountId')
104
107
  const user = c.get('user')
105
108
  const transactionId = c.get('transactionId')
106
109
  const ip = c.get('ip')
107
110
  const userAgent = c.get('userAgent')
108
111
 
109
- if (!db || !accountId || !user) {
112
+ if (!db || !usersDb || !accountId || !user) {
110
113
  throw new Error('Missing required context')
111
114
  }
112
115
 
@@ -118,7 +121,7 @@ export const updateUserHandler: RouteHandler<typeof updateUserRoute, HonoEnv> =
118
121
  userAgent,
119
122
  }
120
123
 
121
- const updatedUser = await usersService.update(db, ctx, id, {
124
+ const updatedUser = await usersService.update(usersDb, ctx, id, {
122
125
  name: data.name,
123
126
  status: data.status,
124
127
  })
@@ -129,13 +132,14 @@ export const updateUserHandler: RouteHandler<typeof updateUserRoute, HonoEnv> =
129
132
  export const deleteUserHandler: RouteHandler<typeof deleteUserRoute, HonoEnv> = async (c) => {
130
133
  const { id } = c.req.valid('param')
131
134
  const db = c.get('db')
135
+ const usersDb = c.env.DB ?? db
132
136
  const accountId = c.get('accountId')
133
137
  const user = c.get('user')
134
138
  const transactionId = c.get('transactionId')
135
139
  const ip = c.get('ip')
136
140
  const userAgent = c.get('userAgent')
137
141
 
138
- if (!db || !accountId || !user) {
142
+ if (!db || !usersDb || !accountId || !user) {
139
143
  throw new Error('Missing required context')
140
144
  }
141
145
 
@@ -147,7 +151,7 @@ export const deleteUserHandler: RouteHandler<typeof deleteUserRoute, HonoEnv> =
147
151
  userAgent,
148
152
  }
149
153
 
150
- await usersService.delete(db, ctx, id)
154
+ await usersService.delete(usersDb, ctx, id)
151
155
  return c.body(null, 204)
152
156
  }
153
157
 
@@ -155,13 +159,14 @@ export const deleteUserHandler: RouteHandler<typeof deleteUserRoute, HonoEnv> =
155
159
  export const createBulkUserAccountsHandler: RouteHandler<typeof createBulkUserAccountsRoute, HonoEnv> = async (c) => {
156
160
  const data = c.req.valid('json')
157
161
  const db = c.get('db')
162
+ const usersDb = c.env.DB ?? db
158
163
  const accountId = c.get('accountId')
159
164
  const user = c.get('user')
160
165
  const transactionId = c.get('transactionId')
161
166
  const ip = c.get('ip')
162
167
  const userAgent = c.get('userAgent')
163
168
 
164
- if (!db || !accountId || !user) {
169
+ if (!db || !usersDb || !accountId || !user) {
165
170
  throw new Error('Missing required context')
166
171
  }
167
172
 
@@ -173,20 +178,21 @@ export const createBulkUserAccountsHandler: RouteHandler<typeof createBulkUserAc
173
178
  userAgent,
174
179
  }
175
180
 
176
- const result = await usersService.createUserAccounts(db, ctx, data)
181
+ const result = await usersService.createUserAccounts(usersDb, ctx, data)
177
182
  return c.json(result, 201)
178
183
  }
179
184
 
180
185
  export const deleteBulkUserAccountsHandler: RouteHandler<typeof deleteBulkUserAccountsRoute, HonoEnv> = async (c) => {
181
186
  const data = c.req.valid('json')
182
187
  const db = c.get('db')
188
+ const usersDb = c.env.DB ?? db
183
189
  const accountId = c.get('accountId')
184
190
  const user = c.get('user')
185
191
  const transactionId = c.get('transactionId')
186
192
  const ip = c.get('ip')
187
193
  const userAgent = c.get('userAgent')
188
194
 
189
- if (!db || !accountId || !user) {
195
+ if (!db || !usersDb || !accountId || !user) {
190
196
  throw new Error('Missing required context')
191
197
  }
192
198
 
@@ -198,20 +204,21 @@ export const deleteBulkUserAccountsHandler: RouteHandler<typeof deleteBulkUserAc
198
204
  userAgent,
199
205
  }
200
206
 
201
- const result = await usersService.deleteUserAccounts(db, ctx, data)
207
+ const result = await usersService.deleteUserAccounts(usersDb, ctx, data)
202
208
  return c.json(result, 200)
203
209
  }
204
210
 
205
211
  export const restoreUserHandler: RouteHandler<typeof restoreUserRoute, HonoEnv> = async (c) => {
206
212
  const { id } = c.req.valid('param')
207
213
  const db = c.get('db')
214
+ const usersDb = c.env.DB ?? db
208
215
  const accountId = c.get('accountId')
209
216
  const user = c.get('user')
210
217
  const transactionId = c.get('transactionId')
211
218
  const ip = c.get('ip')
212
219
  const userAgent = c.get('userAgent')
213
220
 
214
- if (!db || !accountId || !user) {
221
+ if (!db || !usersDb || !accountId || !user) {
215
222
  throw new Error('Missing required context')
216
223
  }
217
224
 
@@ -223,6 +230,6 @@ export const restoreUserHandler: RouteHandler<typeof restoreUserRoute, HonoEnv>
223
230
  userAgent,
224
231
  }
225
232
 
226
- const result = await usersService.restore(db, ctx, id)
233
+ const result = await usersService.restore(usersDb, ctx, id)
227
234
  return c.json({ data: result }, 200)
228
235
  }
@@ -1,11 +1,10 @@
1
1
  // src/services/accounts.ts
2
- import { eq, and, isNull, isNotNull, sql } from 'drizzle-orm'
3
- import type { Database } from '../db/client'
4
- import { accounts, userAccounts, type AccountRecord } from '../db/schema'
2
+ import type { AccountRecord } from '../db/records'
5
3
  import { auditedInsert, auditedUpdate, auditedDelete } from '../lib/audited-db'
6
4
  import { createPaginationMeta, calculateOffset } from '../lib/pagination'
7
5
  import { NotFoundError, ConflictError, ForbiddenError } from '../lib/errors'
8
6
  import type { ServiceContext, PaginationQuery, PaginatedResponse, Account } from '../types'
7
+ import { queryAll, queryOne, type SqlRow } from '../db/sql'
9
8
 
10
9
  interface CreateAccountInput {
11
10
  name: string
@@ -19,251 +18,277 @@ interface UpdateAccountInput {
19
18
  domain?: string
20
19
  }
21
20
 
22
- export const accountsService = {
23
- async findAll(
24
- db: Database,
25
- ctx: ServiceContext,
26
- pagination: PaginationQuery
27
- ): Promise<PaginatedResponse<Account>> {
28
- const offset = calculateOffset(pagination.page, pagination.limit)
29
- const conditions = [isNull(accounts.deletedAt)]
30
-
31
- // Non-super-admin sees only their accounts
32
- if (!ctx.user.isSuperAdmin) {
33
- const accountIdsForUser = db
34
- .select({ accountId: userAccounts.accountId })
35
- .from(userAccounts)
36
- .where(eq(userAccounts.userId, ctx.user.id))
37
21
 
38
- conditions.push(sql`${accounts.id} IN ${accountIdsForUser}`)
39
- }
22
+ const ACCOUNT_SELECT_COLUMNS = `
23
+ id,
24
+ name,
25
+ description,
26
+ domain,
27
+ created_at as createdAt,
28
+ updated_at as updatedAt,
29
+ deleted_at as deletedAt
30
+ `
31
+
32
+
33
+ function mapAccountRow(row: SqlRow): AccountRecord {
34
+ const createdAt = row.createdAt ?? row.created_at
35
+ const updatedAt = row.updatedAt ?? row.updated_at
36
+ const deletedAt = row.deletedAt ?? row.deleted_at
37
+
38
+ return {
39
+ id: String(row.id ?? ''),
40
+ name: String(row.name ?? ''),
41
+ description: row.description ? String(row.description) : null,
42
+ domain: row.domain ? String(row.domain) : null,
43
+ createdAt: String(createdAt ?? ''),
44
+ updatedAt: String(updatedAt ?? ''),
45
+ deletedAt: deletedAt ? String(deletedAt) : null,
46
+ }
47
+ }
40
48
 
41
- // Add search filter
42
- if (pagination.query) {
43
- conditions.push(
44
- sql`(${accounts.name} LIKE ${'%' + pagination.query + '%'} OR ${accounts.domain} LIKE ${'%' + pagination.query + '%'})`
45
- )
46
- }
49
+ function toAccount(record: AccountRecord): Account {
50
+ return {
51
+ id: record.id,
52
+ name: record.name,
53
+ description: record.description,
54
+ domain: record.domain,
55
+ createdAt: record.createdAt,
56
+ updatedAt: record.updatedAt,
57
+ deletedAt: record.deletedAt,
58
+ }
59
+ }
47
60
 
48
- // Get count
49
- const countResults = await db
50
- .select({ count: sql<number>`count(*)` })
51
- .from(accounts)
52
- .where(and(...conditions))
53
-
54
- const totalItems = countResults.at(0)?.count ?? 0
55
-
56
- // Get data
57
- const data = await db
58
- .select()
59
- .from(accounts)
60
- .where(and(...conditions))
61
- .limit(pagination.limit)
62
- .offset(offset)
63
- .orderBy(sql`${accounts.createdAt} DESC`)
64
-
65
- return {
66
- data: data.map((a) => ({
67
- id: a.id,
68
- name: a.name,
69
- description: a.description,
70
- domain: a.domain,
71
- createdAt: a.createdAt,
72
- updatedAt: a.updatedAt,
73
- deletedAt: a.deletedAt,
74
- })),
75
- meta: createPaginationMeta(totalItems, pagination.page, pagination.limit),
76
- }
77
- },
61
+ async function findAllSql(
62
+ db: D1Database,
63
+ ctx: ServiceContext,
64
+ pagination: PaginationQuery
65
+ ): Promise<PaginatedResponse<Account>> {
66
+ const offset = calculateOffset(pagination.page, pagination.limit)
67
+ const whereClauses: string[] = ['a.deleted_at IS NULL']
68
+ const params: unknown[] = []
69
+
70
+ if (!ctx.user.isSuperAdmin) {
71
+ whereClauses.push('a.id IN (SELECT account_id FROM user_accounts WHERE user_id = ?)')
72
+ params.push(ctx.user.id)
73
+ }
74
+
75
+ if (pagination.query) {
76
+ whereClauses.push('(a.name LIKE ? OR a.domain LIKE ?)')
77
+ const like = `%${pagination.query}%`
78
+ params.push(like, like)
79
+ }
80
+
81
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : ''
82
+
83
+ const countRow = await queryOne<{ count: number }>(
84
+ db,
85
+ `SELECT count(*) as count FROM accounts a ${whereSql}`,
86
+ params
87
+ )
88
+ const totalItems = countRow?.count ?? 0
89
+
90
+ const rows = await queryAll(
91
+ db,
92
+ `SELECT ${ACCOUNT_SELECT_COLUMNS}
93
+ FROM accounts a
94
+ ${whereSql}
95
+ ORDER BY a.created_at DESC
96
+ LIMIT ? OFFSET ?`,
97
+ [...params, pagination.limit, offset]
98
+ )
99
+
100
+ return {
101
+ data: rows.map((row) => toAccount(mapAccountRow(row))),
102
+ meta: createPaginationMeta(totalItems, pagination.page, pagination.limit),
103
+ }
104
+ }
78
105
 
79
- async findById(db: Database, ctx: ServiceContext, id: string): Promise<Account> {
80
- const accountResults = await db
81
- .select()
82
- .from(accounts)
83
- .where(and(eq(accounts.id, id), isNull(accounts.deletedAt)))
84
- .limit(1)
106
+ async function findByIdSql(db: D1Database, ctx: ServiceContext, id: string): Promise<Account> {
107
+ const row = await queryOne(
108
+ db,
109
+ `SELECT ${ACCOUNT_SELECT_COLUMNS}
110
+ FROM accounts a
111
+ WHERE a.id = ? AND a.deleted_at IS NULL
112
+ LIMIT 1`,
113
+ [id]
114
+ )
115
+
116
+ if (!row) {
117
+ throw new NotFoundError('Account')
118
+ }
119
+
120
+ if (!ctx.user.isSuperAdmin) {
121
+ const membership = await queryOne(
122
+ db,
123
+ `SELECT 1 as ok FROM user_accounts WHERE user_id = ? AND account_id = ? LIMIT 1`,
124
+ [ctx.user.id, id]
125
+ )
85
126
 
86
- const accountRecord = accountResults.at(0)
87
- if (!accountRecord) {
127
+ if (!membership) {
88
128
  throw new NotFoundError('Account')
89
129
  }
130
+ }
90
131
 
91
- // Check access for non-super-admin
92
- if (!ctx.user.isSuperAdmin) {
93
- const membershipResults = await db
94
- .select()
95
- .from(userAccounts)
96
- .where(
97
- and(
98
- eq(userAccounts.userId, ctx.user.id),
99
- eq(userAccounts.accountId, id)
100
- )
101
- )
102
- .limit(1)
103
-
104
- const membership = membershipResults.at(0)
105
- if (!membership) {
106
- throw new NotFoundError('Account')
107
- }
108
- }
109
-
110
- return {
111
- id: accountRecord.id,
112
- name: accountRecord.name,
113
- description: accountRecord.description,
114
- domain: accountRecord.domain,
115
- createdAt: accountRecord.createdAt,
116
- updatedAt: accountRecord.updatedAt,
117
- deletedAt: accountRecord.deletedAt,
118
- }
119
- },
120
-
121
- async create(db: Database, ctx: ServiceContext, input: CreateAccountInput): Promise<Account> {
122
- // Only super-admin can create accounts
123
- if (!ctx.user.isSuperAdmin) {
124
- throw new ForbiddenError('Only super-admin can create accounts')
125
- }
126
-
127
- // Check domain uniqueness if provided
128
- if (input.domain) {
129
- const existingResults = await db
130
- .select()
131
- .from(accounts)
132
- .where(eq(accounts.domain, input.domain))
133
- .limit(1)
134
-
135
- if (existingResults.at(0)) {
136
- throw new ConflictError('Account with this domain already exists')
137
- }
138
- }
132
+ return toAccount(mapAccountRow(row))
133
+ }
139
134
 
140
- // Create account with audit
141
- const insertResults = await auditedInsert<AccountRecord>(
135
+ async function createSql(
136
+ db: D1Database,
137
+ ctx: ServiceContext,
138
+ input: CreateAccountInput
139
+ ): Promise<Account> {
140
+ if (!ctx.user.isSuperAdmin) {
141
+ throw new ForbiddenError('Only super-admin can create accounts')
142
+ }
143
+
144
+ if (input.domain) {
145
+ const existing = await queryOne(
142
146
  db,
143
- ctx,
144
- accounts,
145
- {
146
- name: input.name,
147
- description: input.description ?? null,
148
- domain: input.domain ?? null,
149
- }
147
+ `SELECT 1 as ok FROM accounts WHERE domain = ? LIMIT 1`,
148
+ [input.domain]
150
149
  )
151
150
 
152
- const accountRecord: AccountRecord | undefined = insertResults.at(0)
153
- if (!accountRecord) {
154
- throw new Error('Failed to create account')
151
+ if (existing) {
152
+ throw new ConflictError('Account with this domain already exists')
155
153
  }
154
+ }
156
155
 
157
- return {
158
- id: accountRecord.id,
159
- name: accountRecord.name,
160
- description: accountRecord.description,
161
- domain: accountRecord.domain,
162
- createdAt: accountRecord.createdAt,
163
- updatedAt: accountRecord.updatedAt,
164
- deletedAt: accountRecord.deletedAt,
165
- }
166
- },
156
+ const insertResults = await auditedInsert<AccountRecord>(db, ctx, 'accounts', {
157
+ name: input.name,
158
+ description: input.description ?? null,
159
+ domain: input.domain ?? null,
160
+ })
167
161
 
168
- async update(db: Database, ctx: ServiceContext, id: string, input: UpdateAccountInput): Promise<Account> {
169
- await this.findById(db, ctx, id)
162
+ const accountRecord = insertResults.at(0)
163
+ if (!accountRecord) {
164
+ throw new Error('Failed to create account')
165
+ }
170
166
 
171
- // Check domain uniqueness if changing
172
- if (input.domain) {
173
- const existingResults = await db
174
- .select()
175
- .from(accounts)
176
- .where(and(eq(accounts.domain, input.domain), sql`${accounts.id} != ${id}`))
177
- .limit(1)
167
+ return toAccount(mapAccountRow(accountRecord as unknown as SqlRow))
168
+ }
178
169
 
179
- if (existingResults.at(0)) {
180
- throw new ConflictError('Account with this domain already exists')
181
- }
182
- }
170
+ async function updateSql(
171
+ db: D1Database,
172
+ ctx: ServiceContext,
173
+ id: string,
174
+ input: UpdateAccountInput
175
+ ): Promise<Account> {
176
+ if (!ctx.user.isSuperAdmin) {
177
+ throw new ForbiddenError('Only super-admin can update accounts')
178
+ }
183
179
 
184
- // Update account with audit
185
- const updateResults = await auditedUpdate<AccountRecord>(
180
+ await findByIdSql(db, ctx, id)
181
+
182
+ if (input.domain) {
183
+ const existing = await queryOne(
186
184
  db,
187
- ctx,
188
- accounts,
189
- {
190
- ...input,
191
- updatedAt: new Date().toISOString(),
192
- },
193
- eq(accounts.id, id)
185
+ `SELECT 1 as ok FROM accounts WHERE domain = ? AND id != ? LIMIT 1`,
186
+ [input.domain, id]
194
187
  )
195
188
 
196
- const accountRecord: AccountRecord | undefined = updateResults.at(0)
197
- if (!accountRecord) {
198
- throw new Error('Failed to update account')
189
+ if (existing) {
190
+ throw new ConflictError('Account with this domain already exists')
199
191
  }
192
+ }
193
+
194
+ const updates: Record<string, unknown> = {
195
+ updated_at: new Date().toISOString(),
196
+ }
197
+
198
+ if (input.name !== undefined) updates.name = input.name
199
+ if (input.description !== undefined) updates.description = input.description ?? null
200
+ if (input.domain !== undefined) updates.domain = input.domain ?? null
201
+
202
+ const updateResults = await auditedUpdate<AccountRecord>(
203
+ db,
204
+ ctx,
205
+ 'accounts',
206
+ updates,
207
+ { clause: 'id = ?', params: [id] }
208
+ )
209
+
210
+ const accountRecord = updateResults.at(0)
211
+ if (!accountRecord) {
212
+ throw new Error('Failed to update account')
213
+ }
214
+
215
+ return toAccount(mapAccountRow(accountRecord as unknown as SqlRow))
216
+ }
200
217
 
201
- return {
202
- id: accountRecord.id,
203
- name: accountRecord.name,
204
- description: accountRecord.description,
205
- domain: accountRecord.domain,
206
- createdAt: accountRecord.createdAt,
207
- updatedAt: accountRecord.updatedAt,
208
- deletedAt: accountRecord.deletedAt,
209
- }
210
- },
218
+ async function deleteSql(db: D1Database, ctx: ServiceContext, id: string): Promise<void> {
219
+ if (!ctx.user.isSuperAdmin) {
220
+ throw new ForbiddenError('Only super-admin can delete accounts')
221
+ }
211
222
 
212
- async delete(db: Database, ctx: ServiceContext, id: string): Promise<void> {
213
- // Only super-admin can delete accounts
214
- if (!ctx.user.isSuperAdmin) {
215
- throw new ForbiddenError('Only super-admin can delete accounts')
216
- }
223
+ await findByIdSql(db, ctx, id)
224
+ await auditedDelete(db, ctx, 'accounts', { clause: 'id = ?', params: [id] })
225
+ }
217
226
 
218
- await this.findById(db, ctx, id)
227
+ async function restoreSql(db: D1Database, ctx: ServiceContext, id: string): Promise<Account> {
228
+ if (!ctx.user.isSuperAdmin) {
229
+ throw new ForbiddenError('Only super-admin can restore accounts')
230
+ }
231
+
232
+ const row = await queryOne(
233
+ db,
234
+ `SELECT ${ACCOUNT_SELECT_COLUMNS}
235
+ FROM accounts a
236
+ WHERE a.id = ? AND a.deleted_at IS NOT NULL
237
+ LIMIT 1`,
238
+ [id]
239
+ )
240
+
241
+ if (!row) {
242
+ throw new NotFoundError('Account not found or not deleted')
243
+ }
244
+
245
+ const restoreResults = await auditedUpdate<AccountRecord>(
246
+ db,
247
+ ctx,
248
+ 'accounts',
249
+ { deleted_at: null },
250
+ { clause: 'id = ?', params: [id] }
251
+ )
252
+
253
+ const restored = restoreResults.at(0)
254
+ if (!restored) {
255
+ throw new NotFoundError('Failed to restore account')
256
+ }
257
+
258
+ return toAccount(mapAccountRow(restored as unknown as SqlRow))
259
+ }
219
260
 
220
- // Soft delete with audit
221
- await auditedDelete(db, ctx, accounts, eq(accounts.id, id))
261
+ export const accountsService = {
262
+ async findAll(
263
+ db: D1Database,
264
+ ctx: ServiceContext,
265
+ pagination: PaginationQuery
266
+ ): Promise<PaginatedResponse<Account>> {
267
+ return findAllSql(db, ctx, pagination)
222
268
  },
223
269
 
224
- async restore(db: Database, ctx: ServiceContext, id: string): Promise<Account> {
225
- // Only super-admin can restore accounts
226
- if (!ctx.user.isSuperAdmin) {
227
- throw new ForbiddenError('Only super-admin can restore accounts')
228
- }
270
+ async findById(db: D1Database, ctx: ServiceContext, id: string): Promise<Account> {
271
+ return findByIdSql(db, ctx, id)
272
+ },
229
273
 
230
- // Find deleted record
231
- const recordResults = await db
232
- .select()
233
- .from(accounts)
234
- .where(and(
235
- eq(accounts.id, id),
236
- isNotNull(accounts.deletedAt)
237
- ))
238
- .limit(1)
239
-
240
- const record = recordResults.at(0)
241
- if (!record) {
242
- throw new NotFoundError('Account not found or not deleted')
243
- }
274
+ async create(db: D1Database, ctx: ServiceContext, input: CreateAccountInput): Promise<Account> {
275
+ return createSql(db, ctx, input)
276
+ },
244
277
 
245
- // Restore account
246
- const restoreResults = await auditedUpdate<AccountRecord>(
247
- db,
248
- ctx,
249
- accounts,
250
- { deletedAt: null },
251
- eq(accounts.id, id)
252
- )
278
+ async update(
279
+ db: D1Database,
280
+ ctx: ServiceContext,
281
+ id: string,
282
+ input: UpdateAccountInput
283
+ ): Promise<Account> {
284
+ return updateSql(db, ctx, id, input)
285
+ },
253
286
 
254
- const restored: AccountRecord | undefined = restoreResults.at(0)
255
- if (!restored) {
256
- throw new NotFoundError('Failed to restore account')
257
- }
287
+ async delete(db: D1Database, ctx: ServiceContext, id: string): Promise<void> {
288
+ await deleteSql(db, ctx, id)
289
+ },
258
290
 
259
- return {
260
- id: restored.id,
261
- name: restored.name,
262
- description: restored.description,
263
- domain: restored.domain,
264
- createdAt: restored.createdAt,
265
- updatedAt: restored.updatedAt,
266
- deletedAt: restored.deletedAt,
267
- }
291
+ async restore(db: D1Database, ctx: ServiceContext, id: string): Promise<Account> {
292
+ return restoreSql(db, ctx, id)
268
293
  },
269
294
  }