@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
@@ -1,28 +1,57 @@
1
- import type { Database } from '../db/client'
2
- import { auditLogs } from '../db/schema'
1
+ import { execute } from '../db/sql'
3
2
  import type { ServiceContext } from '../types'
4
3
 
5
4
  export type AuditAction = 'INSERT' | 'UPDATE' | 'DELETE' | 'LOGIN' | 'LOGOUT' | 'SIGNUP' | 'TOKEN_REFRESH' | 'LOGIN_FAILED'
6
5
 
6
+ async function logAuditSql(
7
+ db: D1Database,
8
+ ctx: ServiceContext,
9
+ entity: string,
10
+ entityId: string,
11
+ action: AuditAction,
12
+ changes: Record<string, unknown>
13
+ ): Promise<void> {
14
+ const id = crypto.randomUUID()
15
+ const changesJson = Object.keys(changes).length > 0 ? JSON.stringify(changes) : null
16
+
17
+ await execute(
18
+ db,
19
+ `INSERT INTO audit_logs (
20
+ id,
21
+ transaction_id,
22
+ account_id,
23
+ user_id,
24
+ entity,
25
+ entity_id,
26
+ action,
27
+ changes,
28
+ ip_address,
29
+ user_agent
30
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
31
+ [
32
+ id,
33
+ ctx.transactionId ?? crypto.randomUUID(),
34
+ ctx.accountId,
35
+ ctx.user.id,
36
+ entity,
37
+ entityId,
38
+ action,
39
+ changesJson,
40
+ ctx.ip ?? null,
41
+ ctx.userAgent ?? null,
42
+ ]
43
+ )
44
+ }
45
+
7
46
  export async function logAudit(
8
- db: Database,
47
+ db: D1Database,
9
48
  ctx: ServiceContext,
10
49
  entity: string,
11
50
  entityId: string,
12
51
  action: AuditAction,
13
52
  changes: Record<string, unknown>
14
53
  ): Promise<void> {
15
- await db.insert(auditLogs).values({
16
- transactionId: ctx.transactionId ?? crypto.randomUUID(),
17
- accountId: ctx.accountId,
18
- userId: ctx.user.id,
19
- entity,
20
- entityId,
21
- action,
22
- changes,
23
- ipAddress: ctx.ip ?? null,
24
- userAgent: ctx.userAgent ?? null,
25
- })
54
+ await logAuditSql(db, ctx, entity, entityId, action, changes)
26
55
  }
27
56
 
28
57
  export function createChangeDiff(
@@ -53,21 +82,40 @@ export interface AuthEventContext {
53
82
  export type AuthAction = 'LOGIN' | 'LOGOUT' | 'SIGNUP' | 'TOKEN_REFRESH' | 'LOGIN_FAILED'
54
83
 
55
84
  export async function logAuthEvent(
56
- db: Database,
85
+ db: D1Database,
57
86
  ctx: AuthEventContext,
58
87
  action: AuthAction,
59
88
  userId: string | null,
60
89
  details: Record<string, unknown>
61
90
  ): Promise<void> {
62
- await db.insert(auditLogs).values({
63
- transactionId: ctx.transactionId ?? crypto.randomUUID(),
64
- accountId: null, // Auth events are account-agnostic
65
- userId,
66
- entity: 'Auth',
67
- entityId: userId ?? 'anonymous',
68
- action,
69
- changes: details,
70
- ipAddress: ctx.ip ?? null,
71
- userAgent: ctx.userAgent ?? null,
72
- })
91
+ const id = crypto.randomUUID()
92
+ const changesJson = Object.keys(details).length > 0 ? JSON.stringify(details) : null
93
+
94
+ await execute(
95
+ db,
96
+ `INSERT INTO audit_logs (
97
+ id,
98
+ transaction_id,
99
+ account_id,
100
+ user_id,
101
+ entity,
102
+ entity_id,
103
+ action,
104
+ changes,
105
+ ip_address,
106
+ user_agent
107
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
108
+ [
109
+ id,
110
+ ctx.transactionId ?? crypto.randomUUID(),
111
+ null,
112
+ userId,
113
+ 'Auth',
114
+ userId ?? 'anonymous',
115
+ action,
116
+ changesJson,
117
+ ctx.ip ?? null,
118
+ ctx.userAgent ?? null,
119
+ ]
120
+ )
73
121
  }
@@ -1,9 +1,7 @@
1
1
  // src/server/lib/audited-db.ts
2
- import { type SQL, getTableName as drizzleGetTableName, is, Table } from 'drizzle-orm'
3
- import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
4
- import type { Database } from '../db/client'
5
2
  import type { ServiceContext } from '../types'
6
3
  import { logAudit, createChangeDiff } from './audit'
4
+ import { execute, queryAll, type SqlParams } from '../db/sql'
7
5
 
8
6
  /**
9
7
  * Base interface for auditable records - requires an id field
@@ -12,106 +10,243 @@ interface AuditableRecord {
12
10
  id: string
13
11
  }
14
12
 
15
- /**
16
- * Get the table name from a Drizzle table definition
17
- */
18
- function getTableName(table: SQLiteTable): string {
19
- if (is(table, Table)) {
20
- return drizzleGetTableName(table)
13
+ export interface SqlWhereClause {
14
+ clause: string
15
+ params?: SqlParams
16
+ }
17
+
18
+ export interface SqlAuditOptions {
19
+ primaryKey?: string
20
+ columnMap?: Record<string, string>
21
+ now?: () => string
22
+ }
23
+
24
+ export interface SqlDeleteOptions extends SqlAuditOptions {
25
+ softDeleteColumns?: {
26
+ deletedAt?: string
27
+ deletedById?: string
28
+ updatedAt?: string
29
+ updatedById?: string
21
30
  }
22
- return 'unknown'
23
31
  }
24
32
 
25
- /**
26
- * Insert with automatic audit logging
27
- * Returns the inserted records (same as .returning())
28
- */
29
- export async function auditedInsert<TRecord extends AuditableRecord>(
30
- db: Database,
31
- ctx: ServiceContext,
32
- table: SQLiteTable,
33
- values: Record<string, unknown> | Record<string, unknown>[]
34
- ): Promise<TRecord[]> {
35
- const tableName = getTableName(table)
33
+ const DEFAULT_SOFT_DELETE_COLUMNS = {
34
+ deletedAt: 'deleted_at',
35
+ deletedById: 'deleted_by_id',
36
+ updatedAt: 'updated_at',
37
+ updatedById: 'updated_by_id',
38
+ }
39
+
40
+ function mapValues(values: Record<string, unknown>, columnMap?: Record<string, string>): Record<string, unknown> {
41
+ if (!columnMap) {
42
+ return values
43
+ }
44
+
45
+ const mapped: Record<string, unknown> = {}
46
+ for (const [key, value] of Object.entries(values)) {
47
+ const column = columnMap[key] ?? key
48
+ mapped[column] = value
49
+ }
50
+ return mapped
51
+ }
52
+
53
+ function normalizeRows(
54
+ values: Record<string, unknown> | Record<string, unknown>[],
55
+ columnMap?: Record<string, string>
56
+ ): { columns: string[]; rows: Record<string, unknown>[] } {
36
57
  const valuesArray = Array.isArray(values) ? values : [values]
58
+ const mapped = valuesArray.map((value) => mapValues(value, columnMap))
37
59
 
38
- const results = await db
39
- .insert(table)
40
- .values(valuesArray)
41
- .returning()
60
+ const columnSet = new Set<string>()
61
+ for (const row of mapped) {
62
+ for (const key of Object.keys(row)) {
63
+ columnSet.add(key)
64
+ }
65
+ }
66
+
67
+ const columns = Array.from(columnSet)
68
+ if (columns.length === 0) {
69
+ throw new Error('No columns provided for SQL operation')
70
+ }
71
+
72
+ const rows = mapped.map((row) => {
73
+ const normalized: Record<string, unknown> = {}
74
+ for (const column of columns) {
75
+ normalized[column] = row[column] ?? null
76
+ }
77
+ return normalized
78
+ })
79
+
80
+ return { columns, rows }
81
+ }
82
+
83
+ function buildInsertSql(tableName: string, columns: string[], rowCount: number): string {
84
+ const columnList = columns.join(', ')
85
+ const rowPlaceholders = `(${columns.map(() => '?').join(', ')})`
86
+ const valuesPlaceholders = Array.from({ length: rowCount }, () => rowPlaceholders).join(', ')
87
+ return `INSERT INTO ${tableName} (${columnList}) VALUES ${valuesPlaceholders} RETURNING *`
88
+ }
89
+
90
+ async function auditedInsertSql<TRecord extends AuditableRecord>(
91
+ db: D1Database,
92
+ ctx: ServiceContext,
93
+ tableName: string,
94
+ values: Record<string, unknown> | Record<string, unknown>[],
95
+ options?: SqlAuditOptions
96
+ ): Promise<TRecord[]> {
97
+ const { columns, rows } = normalizeRows(values, options?.columnMap)
98
+ const sql = buildInsertSql(tableName, columns, rows.length)
99
+ const params = rows.flatMap((row) => columns.map((column) => row[column]))
100
+ const results = await queryAll<Record<string, unknown>>(db, sql, params)
42
101
 
43
- // Log audit for each inserted record
44
102
  for (const record of results) {
45
- const typedRecord = record as unknown as TRecord
46
- await logAudit(
47
- db,
48
- ctx,
49
- tableName,
50
- typedRecord.id,
51
- 'INSERT',
52
- record as Record<string, unknown>
53
- )
103
+ const entityId = String(record[options?.primaryKey ?? 'id'] ?? '')
104
+ await logAudit(db, ctx, tableName, entityId, 'INSERT', record)
54
105
  }
55
106
 
56
- return results as unknown as TRecord[]
107
+ return results as TRecord[]
57
108
  }
58
109
 
59
- /**
60
- * Update with automatic audit logging (includes diff of changes)
61
- * Returns the updated records (same as .returning())
62
- */
63
- export async function auditedUpdate<TRecord extends AuditableRecord>(
64
- db: Database,
110
+ async function auditedUpdateSql<TRecord extends AuditableRecord>(
111
+ db: D1Database,
65
112
  ctx: ServiceContext,
66
- table: SQLiteTable,
113
+ tableName: string,
67
114
  values: Record<string, unknown>,
68
- where: SQL
115
+ where: SqlWhereClause,
116
+ options?: SqlAuditOptions
69
117
  ): Promise<TRecord[]> {
70
- const tableName = getTableName(table)
71
-
72
- // Get old data first for diff
73
- const oldRecords = await db
74
- .select()
75
- .from(table)
76
- .where(where)
77
- .limit(100) // Safety limit
78
-
79
- const results = await db
80
- .update(table)
81
- .set(values)
82
- .where(where)
83
- .returning()
84
-
85
- // Log audit for each updated record with diff
86
- for (let i = 0; i < results.length; i++) {
87
- const oldData = (oldRecords[i] ?? {}) as Record<string, unknown>
88
- const newData = results[i] as unknown as TRecord
89
- const diff = createChangeDiff(
90
- oldData,
91
- newData as Record<string, unknown>
118
+ const mappedValues = mapValues(values, options?.columnMap)
119
+ const columns = Object.keys(mappedValues)
120
+ if (columns.length === 0) {
121
+ throw new Error('No columns provided for SQL update')
122
+ }
123
+
124
+ const whereClause = where.clause.trim()
125
+ if (!whereClause) {
126
+ throw new Error('Missing SQL where clause for auditedUpdate')
127
+ }
128
+
129
+ const oldRecords = await queryAll<Record<string, unknown>>(
130
+ db,
131
+ `SELECT * FROM ${tableName} WHERE ${whereClause} LIMIT 100`,
132
+ where.params
133
+ )
134
+
135
+ const setClause = columns.map((column) => `${column} = ?`).join(', ')
136
+ const params = columns.map((column) => mappedValues[column]).concat(where.params ?? [])
137
+
138
+ let results: Record<string, unknown>[] = []
139
+ try {
140
+ results = await queryAll<Record<string, unknown>>(
141
+ db,
142
+ `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`,
143
+ params
92
144
  )
145
+ } catch {
146
+ await execute(db, `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`, params)
147
+ results = await queryAll<Record<string, unknown>>(
148
+ db,
149
+ `SELECT * FROM ${tableName} WHERE ${whereClause} LIMIT 100`,
150
+ where.params
151
+ )
152
+ }
153
+
154
+ const primaryKey = options?.primaryKey ?? 'id'
155
+ const oldById = new Map<string, Record<string, unknown>>()
156
+ for (const record of oldRecords) {
157
+ const id = String(record[primaryKey] ?? '')
158
+ oldById.set(id, record)
159
+ }
160
+
161
+ for (const record of results) {
162
+ const entityId = String(record[primaryKey] ?? '')
163
+ const oldData = oldById.get(entityId) ?? {}
164
+ const diff = createChangeDiff(oldData, record)
165
+ await logAudit(db, ctx, tableName, entityId, 'UPDATE', diff)
166
+ }
167
+
168
+ return results as TRecord[]
169
+ }
93
170
 
94
- await logAudit(
171
+ async function auditedDeleteSql(
172
+ db: D1Database,
173
+ ctx: ServiceContext,
174
+ tableName: string,
175
+ where: SqlWhereClause,
176
+ options?: SqlDeleteOptions
177
+ ): Promise<void> {
178
+ const whereClause = where.clause.trim()
179
+ if (!whereClause) {
180
+ throw new Error('Missing SQL where clause for auditedDelete')
181
+ }
182
+
183
+ const now = options?.now ?? (() => new Date().toISOString())
184
+ const timestamp = now()
185
+ const columns = {
186
+ ...DEFAULT_SOFT_DELETE_COLUMNS,
187
+ ...options?.softDeleteColumns,
188
+ }
189
+
190
+ const setColumns: Array<[string, unknown]> = [
191
+ [columns.deletedAt, timestamp],
192
+ [columns.deletedById, ctx.user.id],
193
+ [columns.updatedAt, timestamp],
194
+ [columns.updatedById, ctx.user.id],
195
+ ]
196
+
197
+ const setClause = setColumns.map(([column]) => `${column} = ?`).join(', ')
198
+ const params = setColumns.map(([, value]) => value).concat(where.params ?? [])
199
+
200
+ let results: Record<string, unknown>[] = []
201
+ try {
202
+ results = await queryAll<Record<string, unknown>>(
95
203
  db,
96
- ctx,
97
- tableName,
98
- newData.id,
99
- 'UPDATE',
100
- diff
204
+ `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`,
205
+ params
206
+ )
207
+ } catch {
208
+ await execute(db, `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`, params)
209
+ results = await queryAll<Record<string, unknown>>(
210
+ db,
211
+ `SELECT * FROM ${tableName} WHERE ${whereClause} LIMIT 100`,
212
+ where.params
101
213
  )
102
214
  }
103
215
 
104
- return results as unknown as TRecord[]
216
+ const primaryKey = options?.primaryKey ?? 'id'
217
+ for (const record of results) {
218
+ const entityId = String(record[primaryKey] ?? '')
219
+ await logAudit(db, ctx, tableName, entityId, 'DELETE', { deleted: true })
220
+ }
105
221
  }
106
222
 
107
223
  /**
108
- * Interface for soft-deletable tables with required audit fields
224
+ * Insert with automatic audit logging
225
+ * Returns the inserted records (same as .returning())
109
226
  */
110
- interface SoftDeletableFields {
111
- deletedAt: string | null
112
- deletedById: string | null
113
- updatedAt: string
114
- updatedById: string | null
227
+ export async function auditedInsert<TRecord extends AuditableRecord>(
228
+ db: D1Database,
229
+ ctx: ServiceContext,
230
+ table: string,
231
+ values: Record<string, unknown> | Record<string, unknown>[],
232
+ options?: SqlAuditOptions
233
+ ): Promise<TRecord[]> {
234
+ return auditedInsertSql(db, ctx, table, values, options)
235
+ }
236
+
237
+ /**
238
+ * Update with automatic audit logging (includes diff of changes)
239
+ * Returns the updated records (same as .returning())
240
+ */
241
+ export async function auditedUpdate<TRecord extends AuditableRecord>(
242
+ db: D1Database,
243
+ ctx: ServiceContext,
244
+ table: string,
245
+ values: Record<string, unknown>,
246
+ where: SqlWhereClause,
247
+ options?: SqlAuditOptions
248
+ ): Promise<TRecord[]> {
249
+ return auditedUpdateSql(db, ctx, table, values, where, options)
115
250
  }
116
251
 
117
252
  /**
@@ -119,36 +254,11 @@ interface SoftDeletableFields {
119
254
  * Sets deletedAt and deletedById fields
120
255
  */
121
256
  export async function auditedDelete(
122
- db: Database,
257
+ db: D1Database,
123
258
  ctx: ServiceContext,
124
- table: SQLiteTable,
125
- where: SQL
259
+ table: string,
260
+ where: SqlWhereClause,
261
+ options?: SqlDeleteOptions
126
262
  ): Promise<void> {
127
- const tableName = getTableName(table)
128
-
129
- const softDeleteValues: SoftDeletableFields = {
130
- deletedAt: new Date().toISOString(),
131
- deletedById: ctx.user.id,
132
- updatedAt: new Date().toISOString(),
133
- updatedById: ctx.user.id,
134
- }
135
-
136
- const results = await db
137
- .update(table)
138
- .set(softDeleteValues)
139
- .where(where)
140
- .returning()
141
-
142
- // Log audit for each deleted record
143
- for (const record of results) {
144
- const typedRecord = record as unknown as AuditableRecord
145
- await logAudit(
146
- db,
147
- ctx,
148
- tableName,
149
- typedRecord.id,
150
- 'DELETE',
151
- { deleted: true }
152
- )
153
- }
263
+ await auditedDeleteSql(db, ctx, table, where, options)
154
264
  }
@@ -1,24 +1,18 @@
1
- import type { Database } from '../db/client'
2
-
3
- type Transaction = Parameters<Parameters<Database['transaction']>[0]>[0]
1
+ // src/server/lib/transaction.ts
4
2
 
5
3
  /**
6
- * Execute a callback within a database transaction.
7
- * - Callback succeeds → automatic commit
8
- * - Callback throws → automatic rollback
4
+ * Execute a callback within a database unit of work.
9
5
  *
10
- * @example
11
- * const result = await withTransaction(db, async (tx) => {
12
- * const [account] = await tx.insert(accounts).values({ name: 'Acme' }).returning()
13
- * const [user] = await tx.insert(users).values({ email: 'admin@acme.com' }).returning()
14
- * return { account, user }
15
- * })
6
+ * Cloudflare D1 does not expose full transaction callbacks in the Worker runtime.
7
+ * This helper forwards the D1 database instance to the callback.
8
+ * If you need transactional guarantees, use explicit SQL (BEGIN/COMMIT) or
9
+ * D1 batch operations in the calling code.
16
10
  */
17
11
  export async function withTransaction<T>(
18
- db: Database,
19
- callback: (tx: Transaction) => Promise<T>
12
+ db: D1Database,
13
+ callback: (tx: D1Database) => Promise<T>
20
14
  ): Promise<T> {
21
- return db.transaction(callback)
15
+ return callback(db)
22
16
  }
23
17
 
24
- export type { Transaction }
18
+ export type Transaction = D1Database
@@ -1,10 +1,9 @@
1
1
  // src/middleware/account.ts
2
2
  import { createMiddleware } from 'hono/factory'
3
3
  import { HTTPException } from 'hono/http-exception'
4
- import { userAccounts } from '../db/schema'
5
- import { eq, and } from 'drizzle-orm'
6
4
  import type { HonoEnv } from '../types'
7
5
  import type { Role } from '../auth/roles'
6
+ import { queryOne } from '../db/sql'
8
7
 
9
8
  export const accountMiddleware = createMiddleware<HonoEnv>(async (c, next) => {
10
9
  // Check account-id header (required)
@@ -36,21 +35,15 @@ export const accountMiddleware = createMiddleware<HonoEnv>(async (c, next) => {
36
35
 
37
36
  // Check user-account membership in database
38
37
  const db = c.get('db')
39
- if (!db) {
38
+ const accountDb = c.env.DB ?? db
39
+ if (!accountDb) {
40
40
  throw new HTTPException(500, { message: 'Database not initialized' })
41
41
  }
42
- const membershipResults = await db
43
- .select()
44
- .from(userAccounts)
45
- .where(
46
- and(
47
- eq(userAccounts.userId, user.id),
48
- eq(userAccounts.accountId, accountId)
49
- )
50
- )
51
- .limit(1)
52
-
53
- const membership = membershipResults.at(0)
42
+ const membership = await queryOne<{ role: Role }>(
43
+ accountDb,
44
+ `SELECT role FROM user_accounts WHERE user_id = ? AND account_id = ? LIMIT 1`,
45
+ [user.id, accountId]
46
+ )
54
47
  if (!membership) {
55
48
  throw new HTTPException(403, {
56
49
  message: 'Forbidden: User does not have access to this account',