sovrium 0.0.2

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 (527) hide show
  1. package/CHANGELOG.md +3497 -0
  2. package/LICENSE.md +147 -0
  3. package/LICENSE_EE.md +297 -0
  4. package/README.md +321 -0
  5. package/drizzle/0000_melted_kabuki.sql +163 -0
  6. package/drizzle/meta/0000_snapshot.json +1216 -0
  7. package/drizzle/meta/_journal.json +13 -0
  8. package/package.json +167 -0
  9. package/schemas/0.0.1/app.openapi.json +70 -0
  10. package/schemas/0.0.1/app.schema.json +7961 -0
  11. package/schemas/0.0.2/app.openapi.json +80 -0
  12. package/schemas/0.0.2/app.schema.json +8829 -0
  13. package/schemas/development/app.openapi.json +70 -0
  14. package/schemas/development/app.schema.json +7456 -0
  15. package/src/application/errors/app-validation-error.ts +14 -0
  16. package/src/application/errors/static-generation-error.ts +16 -0
  17. package/src/application/metadata/favicon-transformer.ts +127 -0
  18. package/src/application/models/server.ts +27 -0
  19. package/src/application/ports/models/user-metadata.ts +36 -0
  20. package/src/application/ports/models/user-session.ts +34 -0
  21. package/src/application/ports/repositories/activity-log-repository.ts +68 -0
  22. package/src/application/ports/repositories/activity-repository.ts +49 -0
  23. package/src/application/ports/repositories/analytics-repository.ts +164 -0
  24. package/src/application/ports/repositories/auth-repository.ts +33 -0
  25. package/src/application/ports/repositories/batch-repository.ts +86 -0
  26. package/src/application/ports/repositories/comment-repository.ts +150 -0
  27. package/src/application/ports/repositories/index.ts +41 -0
  28. package/src/application/ports/repositories/table-repository.ts +139 -0
  29. package/src/application/ports/services/css-compiler.ts +55 -0
  30. package/src/application/ports/services/index.ts +16 -0
  31. package/src/application/ports/services/page-renderer.ts +79 -0
  32. package/src/application/ports/services/server-factory.ts +80 -0
  33. package/src/application/ports/services/static-site-generator.ts +82 -0
  34. package/src/application/use-cases/activity/programs.ts +66 -0
  35. package/src/application/use-cases/analytics/collect-page-view.ts +114 -0
  36. package/src/application/use-cases/analytics/purge-old-data.ts +40 -0
  37. package/src/application/use-cases/analytics/query-campaigns.ts +43 -0
  38. package/src/application/use-cases/analytics/query-devices.ts +36 -0
  39. package/src/application/use-cases/analytics/query-overview.ts +50 -0
  40. package/src/application/use-cases/analytics/query-pages.ts +40 -0
  41. package/src/application/use-cases/analytics/query-referrers.ts +43 -0
  42. package/src/application/use-cases/analytics/ua-parser.ts +89 -0
  43. package/src/application/use-cases/analytics/visitor-hash.ts +77 -0
  44. package/src/application/use-cases/auth/bootstrap-admin.ts +270 -0
  45. package/src/application/use-cases/list-activity-logs.ts +123 -0
  46. package/src/application/use-cases/server/generate-static-helpers.ts +374 -0
  47. package/src/application/use-cases/server/generate-static.ts +287 -0
  48. package/src/application/use-cases/server/start-server.ts +118 -0
  49. package/src/application/use-cases/server/startup-error-handler.ts +69 -0
  50. package/src/application/use-cases/server/static-content-generators.ts +182 -0
  51. package/src/application/use-cases/server/static-language-generators.ts +181 -0
  52. package/src/application/use-cases/server/static-url-rewriter.ts +237 -0
  53. package/src/application/use-cases/server/translation-replacer.ts +164 -0
  54. package/src/application/use-cases/tables/activity-programs.ts +93 -0
  55. package/src/application/use-cases/tables/batch-operations.ts +156 -0
  56. package/src/application/use-cases/tables/comment-programs.ts +436 -0
  57. package/src/application/use-cases/tables/permissions/permissions.ts +25 -0
  58. package/src/application/use-cases/tables/programs.ts +435 -0
  59. package/src/application/use-cases/tables/table-operations.ts +412 -0
  60. package/src/application/use-cases/tables/user-role.ts +52 -0
  61. package/src/application/use-cases/tables/utils/display-formatter.ts +471 -0
  62. package/src/application/use-cases/tables/utils/field-read-filter.ts +189 -0
  63. package/src/application/use-cases/tables/utils/list-helpers.ts +122 -0
  64. package/src/application/use-cases/tables/utils/record-transformer.ts +319 -0
  65. package/src/cli.ts +370 -0
  66. package/src/domain/errors/create-tagged-error.ts +36 -0
  67. package/src/domain/errors/index.ts +78 -0
  68. package/src/domain/models/api/analytics.ts +179 -0
  69. package/src/domain/models/api/auth.ts +231 -0
  70. package/src/domain/models/api/common.ts +60 -0
  71. package/src/domain/models/api/error.ts +89 -0
  72. package/src/domain/models/api/health.ts +38 -0
  73. package/src/domain/models/api/index.ts +42 -0
  74. package/src/domain/models/api/request.ts +132 -0
  75. package/src/domain/models/api/tables.ts +444 -0
  76. package/src/domain/models/app/analytics/index.ts +129 -0
  77. package/src/domain/models/app/auth/config.ts +116 -0
  78. package/src/domain/models/app/auth/index.ts +230 -0
  79. package/src/domain/models/app/auth/methods/email-and-password.ts +67 -0
  80. package/src/domain/models/app/auth/methods/index.ts +11 -0
  81. package/src/domain/models/app/auth/methods/magic-link.ts +54 -0
  82. package/src/domain/models/app/auth/oauth/index.ts +8 -0
  83. package/src/domain/models/app/auth/oauth/providers.ts +105 -0
  84. package/src/domain/models/app/auth/plugins/admin.ts +130 -0
  85. package/src/domain/models/app/auth/plugins/index.ts +74 -0
  86. package/src/domain/models/app/auth/plugins/two-factor.ts +63 -0
  87. package/src/domain/models/app/auth/roles.ts +179 -0
  88. package/src/domain/models/app/auth/strategies.ts +191 -0
  89. package/src/domain/models/app/auth/validation.ts +127 -0
  90. package/src/domain/models/app/common/branded-ids.ts +200 -0
  91. package/src/domain/models/app/common/definitions.ts +187 -0
  92. package/src/domain/models/app/component/common/component-children.ts +119 -0
  93. package/src/domain/models/app/component/common/component-props.ts +89 -0
  94. package/src/domain/models/app/component/common/component-reference.ts +170 -0
  95. package/src/domain/models/app/component/component.ts +81 -0
  96. package/src/domain/models/app/components.ts +65 -0
  97. package/src/domain/models/app/description.ts +83 -0
  98. package/src/domain/models/app/index.ts +258 -0
  99. package/src/domain/models/app/language/language-config.ts +200 -0
  100. package/src/domain/models/app/languages.ts +205 -0
  101. package/src/domain/models/app/name.ts +66 -0
  102. package/src/domain/models/app/page/common/interactions/click-interaction.ts +116 -0
  103. package/src/domain/models/app/page/common/interactions/entrance-animation.ts +84 -0
  104. package/src/domain/models/app/page/common/interactions/hover-interaction.ts +144 -0
  105. package/src/domain/models/app/page/common/interactions/interactions.ts +64 -0
  106. package/src/domain/models/app/page/common/interactions/scroll-interaction.ts +93 -0
  107. package/src/domain/models/app/page/common/responsive.ts +114 -0
  108. package/src/domain/models/app/page/common/url.ts +35 -0
  109. package/src/domain/models/app/page/common/variable-reference.ts +53 -0
  110. package/src/domain/models/app/page/id.ts +44 -0
  111. package/src/domain/models/app/page/index.ts +270 -0
  112. package/src/domain/models/app/page/meta/analytics.ts +248 -0
  113. package/src/domain/models/app/page/meta/custom-elements.ts +180 -0
  114. package/src/domain/models/app/page/meta/dns-prefetch.ts +77 -0
  115. package/src/domain/models/app/page/meta/favicon-set.ts +203 -0
  116. package/src/domain/models/app/page/meta/favicon.ts +50 -0
  117. package/src/domain/models/app/page/meta/favicons-config.ts +73 -0
  118. package/src/domain/models/app/page/meta/index.ts +278 -0
  119. package/src/domain/models/app/page/meta/open-graph.ts +166 -0
  120. package/src/domain/models/app/page/meta/preload.ts +190 -0
  121. package/src/domain/models/app/page/meta/structured-data/article.ts +211 -0
  122. package/src/domain/models/app/page/meta/structured-data/breadcrumb.ts +115 -0
  123. package/src/domain/models/app/page/meta/structured-data/common-fields.ts +201 -0
  124. package/src/domain/models/app/page/meta/structured-data/education-event.ts +256 -0
  125. package/src/domain/models/app/page/meta/structured-data/faq-page.ts +127 -0
  126. package/src/domain/models/app/page/meta/structured-data/index.ts +95 -0
  127. package/src/domain/models/app/page/meta/structured-data/local-business.ts +247 -0
  128. package/src/domain/models/app/page/meta/structured-data/organization.ts +171 -0
  129. package/src/domain/models/app/page/meta/structured-data/person.ts +138 -0
  130. package/src/domain/models/app/page/meta/structured-data/postal-address.ts +106 -0
  131. package/src/domain/models/app/page/meta/structured-data/product.ts +214 -0
  132. package/src/domain/models/app/page/meta/twitter-card.ts +217 -0
  133. package/src/domain/models/app/page/name.ts +38 -0
  134. package/src/domain/models/app/page/path.ts +21 -0
  135. package/src/domain/models/app/page/scripts/external-scripts.ts +163 -0
  136. package/src/domain/models/app/page/scripts/features.ts +135 -0
  137. package/src/domain/models/app/page/scripts/inline-scripts.ts +114 -0
  138. package/src/domain/models/app/page/scripts/scripts.ts +102 -0
  139. package/src/domain/models/app/page/sections.ts +298 -0
  140. package/src/domain/models/app/pages.ts +61 -0
  141. package/src/domain/models/app/permissions/index.ts +61 -0
  142. package/src/domain/models/app/permissions/resource-action.ts +114 -0
  143. package/src/domain/models/app/permissions/roles.ts +120 -0
  144. package/src/domain/models/app/table/check-constraints.ts +105 -0
  145. package/src/domain/models/app/table/cycle-detection.ts +124 -0
  146. package/src/domain/models/app/table/database-identifier.ts +153 -0
  147. package/src/domain/models/app/table/field-name.ts +36 -0
  148. package/src/domain/models/app/table/field-types/advanced/array-field.ts +33 -0
  149. package/src/domain/models/app/table/field-types/advanced/autonumber-field.ts +54 -0
  150. package/src/domain/models/app/table/field-types/advanced/button-field.ts +56 -0
  151. package/src/domain/models/app/table/field-types/advanced/color-field.ts +57 -0
  152. package/src/domain/models/app/table/field-types/advanced/count-field.ts +54 -0
  153. package/src/domain/models/app/table/field-types/advanced/formula-field.ts +58 -0
  154. package/src/domain/models/app/table/field-types/advanced/geolocation-field.ts +49 -0
  155. package/src/domain/models/app/table/field-types/advanced/index.ts +16 -0
  156. package/src/domain/models/app/table/field-types/advanced/json-field.ts +25 -0
  157. package/src/domain/models/app/table/field-types/advanced/unknown-field.ts +85 -0
  158. package/src/domain/models/app/table/field-types/base-field.ts +42 -0
  159. package/src/domain/models/app/table/field-types/date-time/created-at-field.ts +49 -0
  160. package/src/domain/models/app/table/field-types/date-time/date-field.ts +95 -0
  161. package/src/domain/models/app/table/field-types/date-time/deleted-at-field.ts +56 -0
  162. package/src/domain/models/app/table/field-types/date-time/duration-field.ts +73 -0
  163. package/src/domain/models/app/table/field-types/date-time/index.ts +12 -0
  164. package/src/domain/models/app/table/field-types/date-time/updated-at-field.ts +50 -0
  165. package/src/domain/models/app/table/field-types/index.ts +19 -0
  166. package/src/domain/models/app/table/field-types/media/barcode-field.ts +58 -0
  167. package/src/domain/models/app/table/field-types/media/index.ts +10 -0
  168. package/src/domain/models/app/table/field-types/media/multiple-attachments-field.ts +80 -0
  169. package/src/domain/models/app/table/field-types/media/single-attachment-field.ts +81 -0
  170. package/src/domain/models/app/table/field-types/numeric/currency-field.ts +144 -0
  171. package/src/domain/models/app/table/field-types/numeric/decimal-field.ts +113 -0
  172. package/src/domain/models/app/table/field-types/numeric/index.ts +13 -0
  173. package/src/domain/models/app/table/field-types/numeric/integer-field.ts +98 -0
  174. package/src/domain/models/app/table/field-types/numeric/percentage-field.ts +115 -0
  175. package/src/domain/models/app/table/field-types/numeric/progress-field.ts +71 -0
  176. package/src/domain/models/app/table/field-types/numeric/rating-field.ts +74 -0
  177. package/src/domain/models/app/table/field-types/relational/index.ts +10 -0
  178. package/src/domain/models/app/table/field-types/relational/lookup-field.ts +46 -0
  179. package/src/domain/models/app/table/field-types/relational/relationship-field.ts +112 -0
  180. package/src/domain/models/app/table/field-types/relational/rollup-field.ts +58 -0
  181. package/src/domain/models/app/table/field-types/selection/checkbox-field.ts +51 -0
  182. package/src/domain/models/app/table/field-types/selection/index.ts +11 -0
  183. package/src/domain/models/app/table/field-types/selection/multi-select-field.ts +68 -0
  184. package/src/domain/models/app/table/field-types/selection/single-select-field.ts +54 -0
  185. package/src/domain/models/app/table/field-types/selection/status-field.ts +37 -0
  186. package/src/domain/models/app/table/field-types/text/email-field.ts +80 -0
  187. package/src/domain/models/app/table/field-types/text/index.ts +13 -0
  188. package/src/domain/models/app/table/field-types/text/long-text-field.ts +77 -0
  189. package/src/domain/models/app/table/field-types/text/phone-number-field.ts +82 -0
  190. package/src/domain/models/app/table/field-types/text/rich-text-field.ts +66 -0
  191. package/src/domain/models/app/table/field-types/text/single-line-text-field.ts +79 -0
  192. package/src/domain/models/app/table/field-types/text/url-field.ts +81 -0
  193. package/src/domain/models/app/table/field-types/user/created-by-field.ts +50 -0
  194. package/src/domain/models/app/table/field-types/user/deleted-by-field.ts +57 -0
  195. package/src/domain/models/app/table/field-types/user/index.ts +11 -0
  196. package/src/domain/models/app/table/field-types/user/updated-by-field.ts +51 -0
  197. package/src/domain/models/app/table/field-types/user/user-field.ts +52 -0
  198. package/src/domain/models/app/table/field-types/validation-utils.ts +166 -0
  199. package/src/domain/models/app/table/fields.ts +216 -0
  200. package/src/domain/models/app/table/foreign-keys.ts +111 -0
  201. package/src/domain/models/app/table/formula-keywords.ts +326 -0
  202. package/src/domain/models/app/table/id.ts +31 -0
  203. package/src/domain/models/app/table/index.ts +290 -0
  204. package/src/domain/models/app/table/indexes.ts +80 -0
  205. package/src/domain/models/app/table/name.ts +37 -0
  206. package/src/domain/models/app/table/permissions/field-permission.ts +83 -0
  207. package/src/domain/models/app/table/permissions/index.ts +167 -0
  208. package/src/domain/models/app/table/permissions/permission-evaluator.ts +372 -0
  209. package/src/domain/models/app/table/permissions/permission.ts +49 -0
  210. package/src/domain/models/app/table/primary-key.ts +62 -0
  211. package/src/domain/models/app/table/table-formula-validation.ts +168 -0
  212. package/src/domain/models/app/table/table-indexes-validation.ts +38 -0
  213. package/src/domain/models/app/table/table-permissions-validation.ts +77 -0
  214. package/src/domain/models/app/table/table-primary-key-validation.ts +49 -0
  215. package/src/domain/models/app/table/table-views-validation.ts +408 -0
  216. package/src/domain/models/app/table/unique-constraints.ts +79 -0
  217. package/src/domain/models/app/table/views/fields.ts +28 -0
  218. package/src/domain/models/app/table/views/filters.ts +162 -0
  219. package/src/domain/models/app/table/views/group-by.ts +32 -0
  220. package/src/domain/models/app/table/views/id.ts +50 -0
  221. package/src/domain/models/app/table/views/index.ts +177 -0
  222. package/src/domain/models/app/table/views/name.ts +32 -0
  223. package/src/domain/models/app/table/views/permissions.ts +98 -0
  224. package/src/domain/models/app/table/views/sorts.ts +31 -0
  225. package/src/domain/models/app/tables.ts +695 -0
  226. package/src/domain/models/app/theme/animations.ts +208 -0
  227. package/src/domain/models/app/theme/border-radius.ts +58 -0
  228. package/src/domain/models/app/theme/breakpoints.ts +62 -0
  229. package/src/domain/models/app/theme/colors.ts +110 -0
  230. package/src/domain/models/app/theme/fonts.ts +164 -0
  231. package/src/domain/models/app/theme/shadows.ts +61 -0
  232. package/src/domain/models/app/theme/spacing.ts +115 -0
  233. package/src/domain/models/app/theme.ts +66 -0
  234. package/src/domain/models/app/version.ts +87 -0
  235. package/src/domain/models/record-comment.ts +91 -0
  236. package/src/domain/utils/content-parsing.ts +49 -0
  237. package/src/domain/utils/format-detection.ts +69 -0
  238. package/src/domain/utils/index.ts +9 -0
  239. package/src/domain/utils/route-matcher.ts +184 -0
  240. package/src/domain/utils/translation-resolver.ts +170 -0
  241. package/src/index.ts +208 -0
  242. package/src/infrastructure/analytics/tracking-script.ts +48 -0
  243. package/src/infrastructure/auth/better-auth/auth.ts +216 -0
  244. package/src/infrastructure/auth/better-auth/email-handlers.ts +162 -0
  245. package/src/infrastructure/auth/better-auth/index.ts +16 -0
  246. package/src/infrastructure/auth/better-auth/layer.ts +97 -0
  247. package/src/infrastructure/auth/better-auth/plugins/admin.ts +56 -0
  248. package/src/infrastructure/auth/better-auth/plugins/magic-link.ts +31 -0
  249. package/src/infrastructure/auth/better-auth/plugins/two-factor.ts +19 -0
  250. package/src/infrastructure/auth/better-auth/schema.ts +152 -0
  251. package/src/infrastructure/auth/index.ts +27 -0
  252. package/src/infrastructure/css/cache/css-cache-service.ts +130 -0
  253. package/src/infrastructure/css/compiler.ts +210 -0
  254. package/src/infrastructure/css/css-compiler-live.ts +20 -0
  255. package/src/infrastructure/css/index.ts +25 -0
  256. package/src/infrastructure/css/styles/animation-styles-generator.ts +177 -0
  257. package/src/infrastructure/css/styles/click-animations.ts +147 -0
  258. package/src/infrastructure/css/styles/component-layer-generators.ts +147 -0
  259. package/src/infrastructure/css/theme/theme-generators.ts +130 -0
  260. package/src/infrastructure/css/theme/theme-layer-generators.ts +219 -0
  261. package/src/infrastructure/css/theme/theme-token-resolver.ts +76 -0
  262. package/src/infrastructure/database/activity-queries.ts +111 -0
  263. package/src/infrastructure/database/auth/auth-validation.ts +101 -0
  264. package/src/infrastructure/database/drizzle/db-bun.ts +17 -0
  265. package/src/infrastructure/database/drizzle/db.ts +17 -0
  266. package/src/infrastructure/database/drizzle/index.ts +16 -0
  267. package/src/infrastructure/database/drizzle/layer.ts +34 -0
  268. package/src/infrastructure/database/drizzle/migrate.ts +77 -0
  269. package/src/infrastructure/database/drizzle/schema/activity-log.ts +111 -0
  270. package/src/infrastructure/database/drizzle/schema/analytics-page-views.ts +116 -0
  271. package/src/infrastructure/database/drizzle/schema/migration-audit.ts +68 -0
  272. package/src/infrastructure/database/drizzle/schema/record-comments.ts +79 -0
  273. package/src/infrastructure/database/drizzle/schema.ts +12 -0
  274. package/src/infrastructure/database/field-utils.ts +87 -0
  275. package/src/infrastructure/database/filter-operators.ts +136 -0
  276. package/src/infrastructure/database/formula/formula-trigger-generators.ts +114 -0
  277. package/src/infrastructure/database/formula/formula-utils.ts +440 -0
  278. package/src/infrastructure/database/generators/index-generators.ts +152 -0
  279. package/src/infrastructure/database/generators/trigger-generators.ts +154 -0
  280. package/src/infrastructure/database/index.ts +35 -0
  281. package/src/infrastructure/database/lookup/lookup-expression-generators.ts +356 -0
  282. package/src/infrastructure/database/lookup/lookup-expressions.ts +116 -0
  283. package/src/infrastructure/database/lookup/lookup-view-generators.ts +403 -0
  284. package/src/infrastructure/database/lookup/lookup-view-helpers.ts +65 -0
  285. package/src/infrastructure/database/lookup/lookup-view-triggers.ts +121 -0
  286. package/src/infrastructure/database/migration-audit-trail.ts +375 -0
  287. package/src/infrastructure/database/repositories/activity-log-repository-live.ts +99 -0
  288. package/src/infrastructure/database/repositories/activity-repository-live.ts +21 -0
  289. package/src/infrastructure/database/repositories/analytics-repository-live.ts +316 -0
  290. package/src/infrastructure/database/repositories/auth-repository-live.ts +42 -0
  291. package/src/infrastructure/database/repositories/batch-repository-live.ts +29 -0
  292. package/src/infrastructure/database/repositories/comment-repository-live.ts +39 -0
  293. package/src/infrastructure/database/repositories/table-repository-live.ts +38 -0
  294. package/src/infrastructure/database/schema/schema-dependency-sorting.ts +142 -0
  295. package/src/infrastructure/database/schema/schema-initializer.ts +598 -0
  296. package/src/infrastructure/database/schema-migration/column-detection.ts +286 -0
  297. package/src/infrastructure/database/schema-migration/constants.ts +31 -0
  298. package/src/infrastructure/database/schema-migration/constraint-sync.ts +288 -0
  299. package/src/infrastructure/database/schema-migration/index-sync.ts +108 -0
  300. package/src/infrastructure/database/schema-migration/index.ts +66 -0
  301. package/src/infrastructure/database/schema-migration/migration-statements.ts +106 -0
  302. package/src/infrastructure/database/schema-migration/rename-detection.ts +87 -0
  303. package/src/infrastructure/database/schema-migration/table-operations.ts +65 -0
  304. package/src/infrastructure/database/schema-migration/type-utils.ts +98 -0
  305. package/src/infrastructure/database/schema-migration/types.ts +14 -0
  306. package/src/infrastructure/database/schema-migration-helpers.ts +53 -0
  307. package/src/infrastructure/database/session-context.ts +20 -0
  308. package/src/infrastructure/database/sql/sql-check-constraints.ts +252 -0
  309. package/src/infrastructure/database/sql/sql-column-generators.ts +174 -0
  310. package/src/infrastructure/database/sql/sql-execution.ts +245 -0
  311. package/src/infrastructure/database/sql/sql-field-predicates.ts +81 -0
  312. package/src/infrastructure/database/sql/sql-generators.ts +91 -0
  313. package/src/infrastructure/database/sql/sql-junction-tables.ts +79 -0
  314. package/src/infrastructure/database/sql/sql-key-constraints.ts +210 -0
  315. package/src/infrastructure/database/sql/sql-type-mappings.ts +106 -0
  316. package/src/infrastructure/database/sql/sql-utils.ts +53 -0
  317. package/src/infrastructure/database/table-live-layers.ts +30 -0
  318. package/src/infrastructure/database/table-operations/column-generators.ts +82 -0
  319. package/src/infrastructure/database/table-operations/create-table-sql.ts +81 -0
  320. package/src/infrastructure/database/table-operations/index.ts +55 -0
  321. package/src/infrastructure/database/table-operations/migration-utils.ts +157 -0
  322. package/src/infrastructure/database/table-operations/table-effects.ts +234 -0
  323. package/src/infrastructure/database/table-operations/table-features.ts +96 -0
  324. package/src/infrastructure/database/table-operations/type-compatibility.ts +58 -0
  325. package/src/infrastructure/database/table-operations.ts +47 -0
  326. package/src/infrastructure/database/table-queries/batch/batch-create.ts +80 -0
  327. package/src/infrastructure/database/table-queries/batch/batch-delete.ts +212 -0
  328. package/src/infrastructure/database/table-queries/batch/batch-helpers.ts +124 -0
  329. package/src/infrastructure/database/table-queries/batch/batch-restore.ts +161 -0
  330. package/src/infrastructure/database/table-queries/batch/batch-update.ts +146 -0
  331. package/src/infrastructure/database/table-queries/batch/batch-upsert.ts +357 -0
  332. package/src/infrastructure/database/table-queries/batch/batch.ts +14 -0
  333. package/src/infrastructure/database/table-queries/crud/crud-read.ts +351 -0
  334. package/src/infrastructure/database/table-queries/crud/crud-write.ts +399 -0
  335. package/src/infrastructure/database/table-queries/crud/crud.ts +16 -0
  336. package/src/infrastructure/database/table-queries/index.ts +11 -0
  337. package/src/infrastructure/database/table-queries/mutation-helpers/authorship-helpers.ts +152 -0
  338. package/src/infrastructure/database/table-queries/mutation-helpers/create-record-helpers.ts +90 -0
  339. package/src/infrastructure/database/table-queries/mutation-helpers/delete-helpers.ts +163 -0
  340. package/src/infrastructure/database/table-queries/mutation-helpers/record-fetch-helpers.ts +79 -0
  341. package/src/infrastructure/database/table-queries/mutation-helpers/update-helpers.ts +74 -0
  342. package/src/infrastructure/database/table-queries/query-helpers/activity-log-helpers.ts +53 -0
  343. package/src/infrastructure/database/table-queries/query-helpers/activity-queries.ts +106 -0
  344. package/src/infrastructure/database/table-queries/query-helpers/aggregation-helpers.ts +314 -0
  345. package/src/infrastructure/database/table-queries/query-helpers/comment-queries.ts +414 -0
  346. package/src/infrastructure/database/table-queries/query-helpers/record-validation-queries.ts +126 -0
  347. package/src/infrastructure/database/table-queries/query-helpers/trash-helpers.ts +58 -0
  348. package/src/infrastructure/database/table-queries/shared/error-handling.ts +47 -0
  349. package/src/infrastructure/database/table-queries/shared/typed-execute.ts +27 -0
  350. package/src/infrastructure/database/table-queries/shared/user-join-helpers.ts +38 -0
  351. package/src/infrastructure/database/table-queries/shared/validation.ts +39 -0
  352. package/src/infrastructure/database/views/view-generators.ts +258 -0
  353. package/src/infrastructure/devtools/devtools-layer.ts +43 -0
  354. package/src/infrastructure/devtools/index.ts +8 -0
  355. package/src/infrastructure/email/email-config.ts +103 -0
  356. package/src/infrastructure/email/email-service.ts +152 -0
  357. package/src/infrastructure/email/index.ts +107 -0
  358. package/src/infrastructure/email/nodemailer.ts +125 -0
  359. package/src/infrastructure/email/templates.ts +244 -0
  360. package/src/infrastructure/errors/auth-config-required-error.ts +21 -0
  361. package/src/infrastructure/errors/auth-error.ts +16 -0
  362. package/src/infrastructure/errors/css-compilation-error.ts +14 -0
  363. package/src/infrastructure/errors/index.ts +26 -0
  364. package/src/infrastructure/errors/schema-initialization-error.ts +19 -0
  365. package/src/infrastructure/errors/server-creation-error.ts +14 -0
  366. package/src/infrastructure/filesystem/copy-directory.ts +136 -0
  367. package/src/infrastructure/layers/app-layer.ts +61 -0
  368. package/src/infrastructure/layers/page-renderer-layer.ts +41 -0
  369. package/src/infrastructure/logging/index.ts +8 -0
  370. package/src/infrastructure/logging/logger.ts +204 -0
  371. package/src/infrastructure/schema/file-loader.ts +53 -0
  372. package/src/infrastructure/schema/index.ts +15 -0
  373. package/src/infrastructure/schema/remote-loader.ts +48 -0
  374. package/src/infrastructure/server/index.ts +26 -0
  375. package/src/infrastructure/server/language-detection.ts +87 -0
  376. package/src/infrastructure/server/lifecycle.ts +67 -0
  377. package/src/infrastructure/server/route-setup/api-routes.ts +310 -0
  378. package/src/infrastructure/server/route-setup/auth-route-utils.ts +399 -0
  379. package/src/infrastructure/server/route-setup/auth-routes.ts +245 -0
  380. package/src/infrastructure/server/route-setup/openapi-routes.ts +45 -0
  381. package/src/infrastructure/server/route-setup/openapi-schema.ts +120 -0
  382. package/src/infrastructure/server/route-setup/page-routes.ts +219 -0
  383. package/src/infrastructure/server/route-setup/static-assets.ts +191 -0
  384. package/src/infrastructure/server/server-factory-live.ts +45 -0
  385. package/src/infrastructure/server/server.ts +275 -0
  386. package/src/infrastructure/server/ssg-adapter.ts +196 -0
  387. package/src/infrastructure/server/static-site-generator-live.ts +20 -0
  388. package/src/infrastructure/utils/accept-language-parser.ts +106 -0
  389. package/src/infrastructure/utils/glob-matcher.ts +50 -0
  390. package/src/presentation/api/client.ts +114 -0
  391. package/src/presentation/api/middleware/auth.ts +233 -0
  392. package/src/presentation/api/middleware/table.ts +155 -0
  393. package/src/presentation/api/middleware/validation.ts +88 -0
  394. package/src/presentation/api/routes/activity/get-activity-by-id-handler.ts +77 -0
  395. package/src/presentation/api/routes/activity/index.ts +28 -0
  396. package/src/presentation/api/routes/activity.ts +339 -0
  397. package/src/presentation/api/routes/analytics.ts +328 -0
  398. package/src/presentation/api/routes/auth.ts +169 -0
  399. package/src/presentation/api/routes/index.ts +11 -0
  400. package/src/presentation/api/routes/tables/activity-handlers.ts +57 -0
  401. package/src/presentation/api/routes/tables/batch-permission-helpers.ts +163 -0
  402. package/src/presentation/api/routes/tables/batch-routes.ts +355 -0
  403. package/src/presentation/api/routes/tables/comment-handlers.ts +377 -0
  404. package/src/presentation/api/routes/tables/create-record-helpers.ts +179 -0
  405. package/src/presentation/api/routes/tables/effect-runner.ts +58 -0
  406. package/src/presentation/api/routes/tables/error-handlers.ts +53 -0
  407. package/src/presentation/api/routes/tables/field-permission-validation.ts +167 -0
  408. package/src/presentation/api/routes/tables/filter-parser.ts +75 -0
  409. package/src/presentation/api/routes/tables/formula-parser.ts +118 -0
  410. package/src/presentation/api/routes/tables/index.ts +113 -0
  411. package/src/presentation/api/routes/tables/list-records-filter.ts +54 -0
  412. package/src/presentation/api/routes/tables/param-parsers.ts +59 -0
  413. package/src/presentation/api/routes/tables/record-handlers.ts +484 -0
  414. package/src/presentation/api/routes/tables/record-routes.ts +53 -0
  415. package/src/presentation/api/routes/tables/record-update-handler.ts +200 -0
  416. package/src/presentation/api/routes/tables/sort-validation.ts +85 -0
  417. package/src/presentation/api/routes/tables/table-routes.ts +76 -0
  418. package/src/presentation/api/routes/tables/timezone-validation.ts +41 -0
  419. package/src/presentation/api/routes/tables/upsert-helpers.ts +471 -0
  420. package/src/presentation/api/routes/tables/utils.ts +159 -0
  421. package/src/presentation/api/routes/tables/view-routes.ts +51 -0
  422. package/src/presentation/api/routes/tables.ts +9 -0
  423. package/src/presentation/api/utils/context-helpers.ts +43 -0
  424. package/src/presentation/api/utils/error-sanitizer.ts +235 -0
  425. package/src/presentation/api/utils/field-permission-validator.ts +53 -0
  426. package/src/presentation/api/utils/filter-field-validator.ts +90 -0
  427. package/src/presentation/api/utils/index.ts +13 -0
  428. package/src/presentation/api/utils/run-effect.ts +94 -0
  429. package/src/presentation/api/utils/validate-request.ts +89 -0
  430. package/src/presentation/api/validation/index.ts +29 -0
  431. package/src/presentation/api/validation/rules/field-rules.ts +158 -0
  432. package/src/presentation/api/validation/rules/record-rules.ts +73 -0
  433. package/src/presentation/cli/index.ts +19 -0
  434. package/src/presentation/cli/schema-loader.ts +172 -0
  435. package/src/presentation/hooks/use-breakpoint.ts +155 -0
  436. package/src/presentation/rendering/render-error-pages.tsx +60 -0
  437. package/src/presentation/rendering/render-homepage.tsx +137 -0
  438. package/src/presentation/scripts/script-renderers.ts +112 -0
  439. package/src/presentation/styling/animation-composer.ts +117 -0
  440. package/src/presentation/styling/index.ts +13 -0
  441. package/src/presentation/styling/parse-style.ts +243 -0
  442. package/src/presentation/styling/style-utils.ts +50 -0
  443. package/src/presentation/styling/theme-colors.ts +53 -0
  444. package/src/presentation/translations/component-utils.ts +54 -0
  445. package/src/presentation/translations/index.ts +16 -0
  446. package/src/presentation/translations/translation-resolver.ts +22 -0
  447. package/src/presentation/ui/languages/language-switcher.tsx +119 -0
  448. package/src/presentation/ui/metadata/analytics-builders.tsx +174 -0
  449. package/src/presentation/ui/metadata/analytics-head.tsx +39 -0
  450. package/src/presentation/ui/metadata/custom-elements-builders.tsx +157 -0
  451. package/src/presentation/ui/metadata/extract-component-meta.ts +108 -0
  452. package/src/presentation/ui/metadata/head-elements.tsx +164 -0
  453. package/src/presentation/ui/metadata/index.tsx +35 -0
  454. package/src/presentation/ui/metadata/meta-utils.tsx +42 -0
  455. package/src/presentation/ui/metadata/open-graph-meta.tsx +57 -0
  456. package/src/presentation/ui/metadata/structured-data-from-component.tsx +134 -0
  457. package/src/presentation/ui/metadata/structured-data.tsx +88 -0
  458. package/src/presentation/ui/metadata/twitter-card-meta.tsx +80 -0
  459. package/src/presentation/ui/pages/DefaultHomePage.tsx +43 -0
  460. package/src/presentation/ui/pages/DefaultPageConfigs.ts +220 -0
  461. package/src/presentation/ui/pages/DynamicPage.tsx +307 -0
  462. package/src/presentation/ui/pages/ErrorPage.tsx +25 -0
  463. package/src/presentation/ui/pages/NotFoundPage.tsx +25 -0
  464. package/src/presentation/ui/pages/PageBodyScripts.tsx +242 -0
  465. package/src/presentation/ui/pages/PageBodyStyles.ts +52 -0
  466. package/src/presentation/ui/pages/PageHead.tsx +380 -0
  467. package/src/presentation/ui/pages/PageLangResolver.ts +58 -0
  468. package/src/presentation/ui/pages/PageMain.tsx +58 -0
  469. package/src/presentation/ui/pages/PageMetadata.ts +168 -0
  470. package/src/presentation/ui/pages/PageMetadataI18n.ts +169 -0
  471. package/src/presentation/ui/pages/PageScripts.ts +78 -0
  472. package/src/presentation/ui/pages/SectionRenderer.tsx +67 -0
  473. package/src/presentation/ui/pages/SectionSpacing.tsx +131 -0
  474. package/src/presentation/ui/sections/component-renderer.tsx +426 -0
  475. package/src/presentation/ui/sections/component-renderer.types.ts +33 -0
  476. package/src/presentation/ui/sections/components/component-reference-handler.tsx +74 -0
  477. package/src/presentation/ui/sections/components/component-resolution.ts +65 -0
  478. package/src/presentation/ui/sections/components/index.ts +9 -0
  479. package/src/presentation/ui/sections/hero.tsx +394 -0
  480. package/src/presentation/ui/sections/props/component-builder.ts +183 -0
  481. package/src/presentation/ui/sections/props/element-props.ts +179 -0
  482. package/src/presentation/ui/sections/props/index.ts +9 -0
  483. package/src/presentation/ui/sections/props/prop-conversion.ts +171 -0
  484. package/src/presentation/ui/sections/props/props-builder-config.ts +42 -0
  485. package/src/presentation/ui/sections/props/props-builder.ts +296 -0
  486. package/src/presentation/ui/sections/renderers/element-renderers/html-element-renderer.tsx +124 -0
  487. package/src/presentation/ui/sections/renderers/element-renderers/index.ts +59 -0
  488. package/src/presentation/ui/sections/renderers/element-renderers/interactive-renderers.tsx +231 -0
  489. package/src/presentation/ui/sections/renderers/element-renderers/media-renderers.tsx +102 -0
  490. package/src/presentation/ui/sections/renderers/element-renderers/text-content-renderers.tsx +42 -0
  491. package/src/presentation/ui/sections/renderers/element-renderers.ts +53 -0
  492. package/src/presentation/ui/sections/renderers/html-element-helpers.ts +100 -0
  493. package/src/presentation/ui/sections/renderers/specialized-renderers.tsx +212 -0
  494. package/src/presentation/ui/sections/rendering/component-dispatch-config.ts +31 -0
  495. package/src/presentation/ui/sections/rendering/component-registry/index.ts +39 -0
  496. package/src/presentation/ui/sections/rendering/component-registry/interactive-components.ts +54 -0
  497. package/src/presentation/ui/sections/rendering/component-registry/media-components.ts +36 -0
  498. package/src/presentation/ui/sections/rendering/component-registry/special-components.tsx +153 -0
  499. package/src/presentation/ui/sections/rendering/component-registry/structural-components.ts +215 -0
  500. package/src/presentation/ui/sections/rendering/component-registry/text-components.ts +57 -0
  501. package/src/presentation/ui/sections/rendering/component-registry-helpers.tsx +29 -0
  502. package/src/presentation/ui/sections/rendering/component-registry.tsx +21 -0
  503. package/src/presentation/ui/sections/rendering/component-type-dispatcher.tsx +33 -0
  504. package/src/presentation/ui/sections/rendering/index.ts +9 -0
  505. package/src/presentation/ui/sections/responsive/responsive-children-builder.tsx +96 -0
  506. package/src/presentation/ui/sections/responsive/responsive-content-builder.tsx +95 -0
  507. package/src/presentation/ui/sections/responsive/responsive-props-merger.ts +195 -0
  508. package/src/presentation/ui/sections/responsive/responsive-resolver.ts +213 -0
  509. package/src/presentation/ui/sections/styling/animation-composer-wrapper.ts +65 -0
  510. package/src/presentation/ui/sections/styling/class-builders.ts +45 -0
  511. package/src/presentation/ui/sections/styling/color-resolver.ts +43 -0
  512. package/src/presentation/ui/sections/styling/hover-interaction-handler.ts +107 -0
  513. package/src/presentation/ui/sections/styling/index.ts +9 -0
  514. package/src/presentation/ui/sections/styling/interaction-props-builder.ts +55 -0
  515. package/src/presentation/ui/sections/styling/shadow-resolver.ts +83 -0
  516. package/src/presentation/ui/sections/styling/spacing-resolver.ts +104 -0
  517. package/src/presentation/ui/sections/styling/style-processor.ts +170 -0
  518. package/src/presentation/ui/sections/styling/theme-tokens.ts +184 -0
  519. package/src/presentation/ui/sections/translations/i18n-content-resolver.ts +198 -0
  520. package/src/presentation/ui/sections/translations/index.ts +9 -0
  521. package/src/presentation/ui/sections/translations/translation-handler.ts +143 -0
  522. package/src/presentation/ui/sections/translations/variable-substitution.ts +225 -0
  523. package/src/presentation/ui/sections/utils/time-parser.ts +82 -0
  524. package/src/presentation/utils/link-attributes.ts +50 -0
  525. package/src/presentation/utils/string-utils.ts +58 -0
  526. package/src/presentation/utils/styles.ts +50 -0
  527. package/tsconfig.json +46 -0
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Copyright (c) 2025 ESSENTIAL SERVICES
3
+ *
4
+ * This source code is licensed under the Business Source License 1.1
5
+ * found in the LICENSE.md file in the root directory of this source tree.
6
+ */
7
+
8
+ import { Effect } from 'effect'
9
+ import { GetActivityById } from '@/application/use-cases/activity/programs'
10
+ import {
11
+ type ActivityLogOutput,
12
+ ListActivityLogs,
13
+ ListActivityLogsLayer,
14
+ } from '@/application/use-cases/list-activity-logs'
15
+ import { getUserRole } from '@/application/use-cases/tables/user-role'
16
+ import { DatabaseLive } from '@/infrastructure/database'
17
+ import { getSessionContext } from '@/presentation/api/utils/context-helpers'
18
+ import { sanitizeError, getStatusCode } from '@/presentation/api/utils/error-sanitizer'
19
+ import type { Context, Hono } from 'hono'
20
+
21
+ /**
22
+ * User metadata in activity log API response
23
+ */
24
+ interface ActivityLogResponseUser {
25
+ readonly id: string
26
+ readonly name: string
27
+ readonly email: string
28
+ }
29
+
30
+ /**
31
+ * Activity log API response type
32
+ *
33
+ * Maps application ActivityLogOutput to API JSON response format.
34
+ * Uses camelCase for all fields per API conventions.
35
+ * user is null for system-logged activities (no user_id).
36
+ */
37
+ interface ActivityLogResponse {
38
+ readonly id: string
39
+ readonly createdAt: string
40
+ readonly userId: string | undefined
41
+ readonly action: 'create' | 'update' | 'delete' | 'restore'
42
+ readonly tableName: string
43
+ readonly recordId: string
44
+ readonly user: ActivityLogResponseUser | null
45
+ }
46
+
47
+ /**
48
+ * Pagination metadata for list responses
49
+ */
50
+ interface PaginationMeta {
51
+ readonly total: number
52
+ readonly page: number
53
+ readonly pageSize: number
54
+ readonly totalPages: number
55
+ }
56
+
57
+ /**
58
+ * Parsed and validated pagination parameters
59
+ */
60
+ interface PaginationParams {
61
+ readonly page: number
62
+ readonly pageSize: number
63
+ }
64
+
65
+ /**
66
+ * Map ActivityLogOutput to API response format
67
+ */
68
+ function mapToApiResponse(log: ActivityLogOutput): ActivityLogResponse {
69
+ return {
70
+ id: log.id,
71
+ createdAt: log.createdAt,
72
+ userId: log.userId,
73
+ action: log.action,
74
+ tableName: log.tableName,
75
+ recordId: log.recordId,
76
+ user: log.user,
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Parse and validate pagination query parameters
82
+ *
83
+ * Returns undefined if parameters are invalid.
84
+ */
85
+ function parsePaginationParams(
86
+ pageParam: string | undefined,
87
+ pageSizeParam: string | undefined
88
+ ): PaginationParams | undefined {
89
+ const page = pageParam === undefined ? 1 : parseInt(pageParam, 10)
90
+ const pageSize = pageSizeParam === undefined ? 50 : parseInt(pageSizeParam, 10)
91
+
92
+ if (isNaN(page) || page < 1) return undefined
93
+ if (isNaN(pageSize) || pageSize < 1 || pageSize > 100) return undefined
94
+
95
+ return { page, pageSize }
96
+ }
97
+
98
+ /**
99
+ * Build paginated response from activity log list
100
+ */
101
+ function buildPaginatedResponse(
102
+ logs: readonly ActivityLogOutput[],
103
+ page: number,
104
+ pageSize: number
105
+ ): { activities: readonly ActivityLogResponse[]; pagination: PaginationMeta } {
106
+ const total = logs.length
107
+ const totalPages = Math.ceil(total / pageSize)
108
+ const start = (page - 1) * pageSize
109
+ const paginatedLogs = logs.slice(start, start + pageSize)
110
+ const pagination: PaginationMeta = { total, page, pageSize, totalPages }
111
+ return { activities: paginatedLogs.map(mapToApiResponse), pagination }
112
+ }
113
+
114
+ /**
115
+ * Handle GET /api/activity/:activityId - Get activity log details
116
+ */
117
+ async function handleGetActivityById(c: Context) {
118
+ const activityId = c.req.param('activityId')
119
+
120
+ const program = GetActivityById(activityId).pipe(Effect.provide(DatabaseLive))
121
+
122
+ const result = await Effect.runPromise(program.pipe(Effect.either))
123
+
124
+ if (result._tag === 'Left') {
125
+ const error = result.left
126
+
127
+ if (error._tag === 'InvalidActivityIdError') {
128
+ return c.json(
129
+ { success: false, message: 'Invalid activity ID format', code: 'INVALID_ACTIVITY_ID' },
130
+ 400
131
+ )
132
+ }
133
+
134
+ if (error._tag === 'ActivityNotFoundError') {
135
+ return c.json(
136
+ { success: false, message: 'Activity not found', code: 'ACTIVITY_NOT_FOUND' },
137
+ 404
138
+ )
139
+ }
140
+
141
+ return c.json(
142
+ { success: false, message: 'Failed to fetch activity', code: 'DATABASE_ERROR' },
143
+ 500
144
+ )
145
+ }
146
+
147
+ return c.json(result.right, 200)
148
+ }
149
+
150
+ /**
151
+ * Valid activity action types
152
+ */
153
+ const VALID_ACTIONS = ['create', 'update', 'delete', 'restore'] as const
154
+
155
+ /**
156
+ * Parse and validate action filter parameter
157
+ *
158
+ * Returns undefined if no filter, null if invalid value.
159
+ */
160
+ function parseActionFilter(
161
+ action: string | undefined
162
+ ): 'create' | 'update' | 'delete' | 'restore' | undefined | null {
163
+ if (action === undefined) return undefined
164
+ if (VALID_ACTIONS.includes(action as (typeof VALID_ACTIONS)[number])) {
165
+ return action as 'create' | 'update' | 'delete' | 'restore'
166
+ }
167
+ // eslint-disable-next-line unicorn/no-null -- Null signals invalid action (vs undefined = no filter)
168
+ return null
169
+ }
170
+
171
+ /**
172
+ * Check if a user is authorized to filter by the given userId
173
+ *
174
+ * Admins can filter by any userId.
175
+ * Non-admin users can only filter by their own userId.
176
+ * Returns true if authorized, false if forbidden.
177
+ */
178
+ async function isAuthorizedForUserIdFilter(
179
+ sessionUserId: string,
180
+ userIdFilter: string | undefined
181
+ ): Promise<boolean> {
182
+ if (userIdFilter === undefined) return true
183
+ if (userIdFilter === sessionUserId) return true
184
+ const role = await getUserRole(sessionUserId)
185
+ return role === 'admin'
186
+ }
187
+
188
+ /**
189
+ * Filter options for activity log queries
190
+ */
191
+ interface ActivityFilters {
192
+ readonly tableName?: string
193
+ readonly action?: 'create' | 'update' | 'delete' | 'restore'
194
+ readonly userId?: string
195
+ readonly startDate?: Date
196
+ }
197
+
198
+ /**
199
+ * Apply tableName, action, userId, and startDate filters to activity logs
200
+ */
201
+ function applyFilters(
202
+ logs: readonly ActivityLogOutput[],
203
+ filters: ActivityFilters
204
+ ): readonly ActivityLogOutput[] {
205
+ return logs.filter(
206
+ (log) =>
207
+ (filters.tableName === undefined || log.tableName === filters.tableName) &&
208
+ (filters.action === undefined || log.action === filters.action) &&
209
+ (filters.userId === undefined || log.userId === filters.userId) &&
210
+ (filters.startDate === undefined || new Date(log.createdAt) >= filters.startDate)
211
+ )
212
+ }
213
+
214
+ /**
215
+ * Parse query filter parameters from request context
216
+ *
217
+ * Returns undefined for tableName/userId if not provided,
218
+ * null for action if invalid value provided.
219
+ */
220
+ function parseQueryFilters(c: Context): {
221
+ tableName: string | undefined
222
+ action: 'create' | 'update' | 'delete' | 'restore' | undefined | null
223
+ userId: string | undefined
224
+ startDate: Date | undefined
225
+ } {
226
+ return {
227
+ tableName: c.req.query('tableName'),
228
+ action: parseActionFilter(c.req.query('action')),
229
+ userId: c.req.query('userId'),
230
+ startDate:
231
+ c.req.query('startDate') !== undefined ? new Date(c.req.query('startDate')!) : undefined,
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Validation error response from list activity request validation
237
+ */
238
+ interface ListActivityValidationError {
239
+ readonly status: number
240
+ readonly body: { success: false; message: string; code: string }
241
+ }
242
+
243
+ /**
244
+ * Validate list activity request parameters
245
+ *
246
+ * Returns error response object if invalid, or undefined if valid.
247
+ */
248
+ async function validateListActivityRequest(
249
+ c: Context,
250
+ sessionUserId: string
251
+ ): Promise<ListActivityValidationError | undefined> {
252
+ const params = parsePaginationParams(c.req.query('page'), c.req.query('pageSize'))
253
+ if (params === undefined) {
254
+ return {
255
+ status: 400,
256
+ body: { success: false, message: 'Invalid pagination parameters', code: 'INVALID_PARAMETER' },
257
+ }
258
+ }
259
+
260
+ const { action } = parseQueryFilters(c)
261
+ if (action === null) {
262
+ return {
263
+ status: 400,
264
+ body: { success: false, message: 'Invalid action filter', code: 'INVALID_PARAMETER' },
265
+ }
266
+ }
267
+
268
+ const userIdFilter = c.req.query('userId')
269
+ const authorized = await isAuthorizedForUserIdFilter(sessionUserId, userIdFilter)
270
+ if (!authorized) {
271
+ return {
272
+ status: 403,
273
+ body: {
274
+ success: false,
275
+ message: 'Forbidden: cannot view other users activities',
276
+ code: 'FORBIDDEN',
277
+ },
278
+ }
279
+ }
280
+
281
+ return undefined
282
+ }
283
+
284
+ /**
285
+ * Handle GET /api/activity - List activity logs with pagination
286
+ */
287
+ async function handleListActivityLogs(c: Context) {
288
+ const session = getSessionContext(c)
289
+ if (!session) {
290
+ return c.json({ success: false, message: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
291
+ }
292
+
293
+ const validationError = await validateListActivityRequest(c, session.userId)
294
+ if (validationError !== undefined) {
295
+ return c.json(validationError.body, validationError.status as 400 | 403)
296
+ }
297
+
298
+ const params = parsePaginationParams(c.req.query('page'), c.req.query('pageSize'))!
299
+ const { tableName, action, userId, startDate } = parseQueryFilters(c)
300
+
301
+ const result = await Effect.runPromise(
302
+ ListActivityLogs({ userId: session.userId }).pipe(
303
+ Effect.provide(ListActivityLogsLayer),
304
+ Effect.either
305
+ )
306
+ )
307
+
308
+ if (result._tag === 'Left') {
309
+ const sanitized = sanitizeError(result.left, crypto.randomUUID())
310
+ return c.json(
311
+ { success: false, message: sanitized.message ?? sanitized.error, code: sanitized.code },
312
+ getStatusCode(sanitized.code)
313
+ )
314
+ }
315
+
316
+ const filtered = applyFilters(result.right, {
317
+ tableName,
318
+ action: action ?? undefined,
319
+ userId,
320
+ startDate,
321
+ })
322
+ return c.json(buildPaginatedResponse(filtered, params.page, params.pageSize), 200)
323
+ }
324
+
325
+ /**
326
+ * Chain activity routes onto a Hono app
327
+ *
328
+ * Provides:
329
+ * - GET /api/activity/:activityId - Get activity log details
330
+ * - GET /api/activity - List activity logs (admin/member only)
331
+ *
332
+ * @param honoApp - Hono instance to chain routes onto
333
+ * @returns Hono app with activity routes chained
334
+ */
335
+ export function chainActivityRoutes<T extends Hono>(honoApp: T): T {
336
+ return honoApp
337
+ .get('/api/activity/:activityId', handleGetActivityById)
338
+ .get('/api/activity', handleListActivityLogs) as T
339
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Copyright (c) 2025 ESSENTIAL SERVICES
3
+ *
4
+ * This source code is licensed under the Business Source License 1.1
5
+ * found in the LICENSE.md file in the root directory of this source tree.
6
+ */
7
+
8
+ import { zValidator } from '@hono/zod-validator'
9
+ import { Effect } from 'effect'
10
+ import { collectPageView } from '@/application/use-cases/analytics/collect-page-view'
11
+ import { purgeOldAnalyticsData } from '@/application/use-cases/analytics/purge-old-data'
12
+ import { queryCampaigns } from '@/application/use-cases/analytics/query-campaigns'
13
+ import { queryDevices } from '@/application/use-cases/analytics/query-devices'
14
+ import { queryOverview } from '@/application/use-cases/analytics/query-overview'
15
+ import { queryPages } from '@/application/use-cases/analytics/query-pages'
16
+ import { queryReferrers } from '@/application/use-cases/analytics/query-referrers'
17
+ import { analyticsCollectSchema, analyticsQuerySchema } from '@/domain/models/api/analytics'
18
+ import { AnalyticsRepositoryLive } from '@/infrastructure/database/repositories/analytics-repository-live'
19
+ import { matchesAnyGlobPattern } from '@/infrastructure/utils/glob-matcher'
20
+ import { getSessionContext } from '@/presentation/api/utils/context-helpers'
21
+ import type { Context, Hono } from 'hono'
22
+
23
+ /**
24
+ * Extract client IP from request headers
25
+ */
26
+ function extractClientIp(xForwardedFor: string | undefined): string {
27
+ if (xForwardedFor) {
28
+ const first = xForwardedFor.split(',')[0]
29
+ return first?.trim() ?? 'unknown'
30
+ }
31
+ return 'unknown'
32
+ }
33
+
34
+ /**
35
+ * Parse and validate analytics query parameters from request
36
+ */
37
+ function parseAnalyticsQuery(
38
+ c: Context,
39
+ appName: string
40
+ ):
41
+ | {
42
+ readonly appName: string
43
+ readonly from: Date
44
+ readonly to: Date
45
+ readonly granularity: 'hour' | 'day' | 'week' | 'month'
46
+ }
47
+ | undefined {
48
+ const fromStr = c.req.query('from')
49
+ const toStr = c.req.query('to')
50
+ const validGranularities = new Set(['hour', 'day', 'week', 'month'])
51
+ const rawGranularity = c.req.query('granularity') ?? 'day'
52
+ const granularity = (validGranularities.has(rawGranularity) ? rawGranularity : 'day') as
53
+ | 'hour'
54
+ | 'day'
55
+ | 'week'
56
+ | 'month'
57
+
58
+ if (!fromStr || !toStr) return undefined
59
+
60
+ const parsed = analyticsQuerySchema.safeParse({ from: fromStr, to: toStr, granularity })
61
+ if (!parsed.success) return undefined
62
+
63
+ // Round `to` up to the end of its second so that the query range captures
64
+ // all events within the same wall-clock second. Without this, sub-second
65
+ // differences between the client timestamp and server-side NOW() can exclude
66
+ // rows that logically fall within the requested window.
67
+ const toDate = new Date(parsed.data.to)
68
+ const toEndOfSecond = new Date(Math.ceil(toDate.getTime() / 1000) * 1000 + 999)
69
+
70
+ return {
71
+ appName,
72
+ from: new Date(parsed.data.from),
73
+ to: toEndOfSecond,
74
+ granularity: parsed.data.granularity,
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Handle POST /api/analytics/collect — public endpoint, no auth required
80
+ *
81
+ * Records a page view with privacy-safe visitor hashing.
82
+ * Also triggers retention cleanup (fire-and-forget) to purge stale records.
83
+ * Returns 204 No Content for fastest response.
84
+ */
85
+ // eslint-disable-next-line max-params -- All parameters are configured per-app and forwarded from route registration
86
+ async function handleCollect(
87
+ c: Context,
88
+ appName: string,
89
+ retentionDays?: number,
90
+ excludedPaths?: readonly string[],
91
+ respectDoNotTrack?: boolean
92
+ ): Promise<Response> {
93
+ const body = c.req.valid('json' as never)
94
+ const pagePath = (body as { readonly p: string }).p
95
+
96
+ // Check if path is excluded - return 204 without recording
97
+ if (matchesAnyGlobPattern(excludedPaths, pagePath)) {
98
+ // eslint-disable-next-line unicorn/no-null
99
+ return c.body(null, 204)
100
+ }
101
+
102
+ // Check Do Not Track header when respectDoNotTrack is enabled
103
+ const dntHeader = c.req.header('DNT')
104
+ if (respectDoNotTrack && dntHeader === '1') {
105
+ // eslint-disable-next-line unicorn/no-null
106
+ return c.body(null, 204)
107
+ }
108
+
109
+ const ip = extractClientIp(c.req.header('x-forwarded-for'))
110
+ const userAgent = c.req.header('user-agent') ?? ''
111
+ const acceptLanguage = c.req.header('accept-language') ?? ''
112
+
113
+ // Fire-and-forget: record page view and purge stale data asynchronously
114
+ // eslint-disable-next-line functional/no-expression-statements
115
+ void Effect.runPromise(
116
+ Effect.all(
117
+ [
118
+ collectPageView({
119
+ appName,
120
+ pagePath,
121
+ pageTitle: (body as { readonly t?: string }).t,
122
+ referrerUrl: (body as { readonly r?: string }).r,
123
+ ip,
124
+ userAgent,
125
+ acceptLanguage,
126
+ screenWidth: (body as { readonly sw?: number }).sw,
127
+ screenHeight: (body as { readonly sh?: number }).sh,
128
+ utmSource: (body as { readonly us?: string }).us,
129
+ utmMedium: (body as { readonly um?: string }).um,
130
+ utmCampaign: (body as { readonly uc?: string }).uc,
131
+ utmContent: (body as { readonly ux?: string }).ux,
132
+ utmTerm: (body as { readonly ut?: string }).ut,
133
+ }),
134
+ purgeOldAnalyticsData(appName, retentionDays),
135
+ ],
136
+ { concurrency: 'unbounded' }
137
+ ).pipe(
138
+ Effect.provide(AnalyticsRepositoryLive),
139
+ Effect.catchAll(() => Effect.void)
140
+ )
141
+ )
142
+
143
+ // eslint-disable-next-line unicorn/no-null
144
+ return c.body(null, 204)
145
+ }
146
+
147
+ /**
148
+ * Handle GET /api/analytics/overview — requires auth
149
+ */
150
+ async function handleOverview(c: Context, appName: string): Promise<Response> {
151
+ const session = getSessionContext(c)
152
+ if (!session) {
153
+ return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
154
+ }
155
+
156
+ const params = parseAnalyticsQuery(c, appName)
157
+ if (!params) {
158
+ return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
159
+ }
160
+
161
+ const result = await Effect.runPromise(
162
+ queryOverview({
163
+ appName: params.appName,
164
+ from: params.from,
165
+ to: params.to,
166
+ granularity: params.granularity,
167
+ }).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
168
+ )
169
+
170
+ if (result._tag === 'Left') {
171
+ return c.json({ error: 'Failed to query analytics', code: 'INTERNAL_ERROR' }, 500)
172
+ }
173
+
174
+ return c.json(result.right, 200)
175
+ }
176
+
177
+ /**
178
+ * Handle GET /api/analytics/pages — requires auth
179
+ */
180
+ async function handlePages(c: Context, appName: string): Promise<Response> {
181
+ const session = getSessionContext(c)
182
+ if (!session) {
183
+ return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
184
+ }
185
+
186
+ const params = parseAnalyticsQuery(c, appName)
187
+ if (!params) {
188
+ return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
189
+ }
190
+
191
+ const result = await Effect.runPromise(
192
+ queryPages({
193
+ appName: params.appName,
194
+ from: params.from,
195
+ to: params.to,
196
+ }).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
197
+ )
198
+
199
+ if (result._tag === 'Left') {
200
+ return c.json({ error: 'Failed to query pages', code: 'INTERNAL_ERROR' }, 500)
201
+ }
202
+
203
+ return c.json(result.right, 200)
204
+ }
205
+
206
+ /**
207
+ * Handle GET /api/analytics/referrers — requires auth
208
+ */
209
+ async function handleReferrers(c: Context, appName: string): Promise<Response> {
210
+ const session = getSessionContext(c)
211
+ if (!session) {
212
+ return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
213
+ }
214
+
215
+ const params = parseAnalyticsQuery(c, appName)
216
+ if (!params) {
217
+ return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
218
+ }
219
+
220
+ const result = await Effect.runPromise(
221
+ queryReferrers({
222
+ appName: params.appName,
223
+ from: params.from,
224
+ to: params.to,
225
+ }).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
226
+ )
227
+
228
+ if (result._tag === 'Left') {
229
+ return c.json({ error: 'Failed to query referrers', code: 'INTERNAL_ERROR' }, 500)
230
+ }
231
+
232
+ return c.json(result.right, 200)
233
+ }
234
+
235
+ /**
236
+ * Handle GET /api/analytics/devices — requires auth
237
+ */
238
+ async function handleDevices(c: Context, appName: string): Promise<Response> {
239
+ const session = getSessionContext(c)
240
+ if (!session) {
241
+ return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
242
+ }
243
+
244
+ const params = parseAnalyticsQuery(c, appName)
245
+ if (!params) {
246
+ return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
247
+ }
248
+
249
+ const result = await Effect.runPromise(
250
+ queryDevices({
251
+ appName: params.appName,
252
+ from: params.from,
253
+ to: params.to,
254
+ }).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
255
+ )
256
+
257
+ if (result._tag === 'Left') {
258
+ return c.json({ error: 'Failed to query devices', code: 'INTERNAL_ERROR' }, 500)
259
+ }
260
+
261
+ return c.json(result.right, 200)
262
+ }
263
+
264
+ /**
265
+ * Handle GET /api/analytics/campaigns — requires auth
266
+ */
267
+ async function handleCampaigns(c: Context, appName: string): Promise<Response> {
268
+ const session = getSessionContext(c)
269
+ if (!session) {
270
+ return c.json({ error: 'Authentication required', code: 'UNAUTHORIZED' }, 401)
271
+ }
272
+
273
+ const params = parseAnalyticsQuery(c, appName)
274
+ if (!params) {
275
+ return c.json({ error: 'Missing or invalid from/to parameters', code: 'VALIDATION_ERROR' }, 400)
276
+ }
277
+
278
+ const result = await Effect.runPromise(
279
+ queryCampaigns({
280
+ appName: params.appName,
281
+ from: params.from,
282
+ to: params.to,
283
+ }).pipe(Effect.provide(AnalyticsRepositoryLive), Effect.either)
284
+ )
285
+
286
+ if (result._tag === 'Left') {
287
+ return c.json({ error: 'Failed to query campaigns', code: 'INTERNAL_ERROR' }, 500)
288
+ }
289
+
290
+ return c.json(result.right, 200)
291
+ }
292
+
293
+ /**
294
+ * Chain analytics routes onto a Hono app
295
+ *
296
+ * Provides:
297
+ * - POST /api/analytics/collect - Record page view (public, no auth)
298
+ * - GET /api/analytics/overview - Summary + time series (auth required)
299
+ * - GET /api/analytics/pages - Top pages (auth required)
300
+ * - GET /api/analytics/referrers - Top referrers (auth required)
301
+ * - GET /api/analytics/devices - Device breakdown (auth required)
302
+ * - GET /api/analytics/campaigns - UTM campaigns (auth required)
303
+ *
304
+ * @param honoApp - Hono instance to chain routes onto
305
+ * @param appName - Application name for multi-tenant analytics
306
+ * @param retentionDays - Number of days to retain analytics data (triggers cleanup on collect)
307
+ * @param excludedPaths - Glob patterns for paths excluded from tracking
308
+ * @param respectDoNotTrack - Whether to honor Do Not Track (DNT:1) header
309
+ * @returns Hono app with analytics routes chained
310
+ */
311
+ // eslint-disable-next-line max-params -- All parameters are configured per-app and forwarded from route setup
312
+ export function chainAnalyticsRoutes<T extends Hono>(
313
+ honoApp: T,
314
+ appName: string,
315
+ retentionDays?: number,
316
+ excludedPaths?: readonly string[],
317
+ respectDoNotTrack?: boolean
318
+ ): T {
319
+ return honoApp
320
+ .post('/api/analytics/collect', zValidator('json', analyticsCollectSchema), (c) =>
321
+ handleCollect(c, appName, retentionDays, excludedPaths, respectDoNotTrack)
322
+ )
323
+ .get('/api/analytics/overview', (c) => handleOverview(c, appName))
324
+ .get('/api/analytics/pages', (c) => handlePages(c, appName))
325
+ .get('/api/analytics/referrers', (c) => handleReferrers(c, appName))
326
+ .get('/api/analytics/devices', (c) => handleDevices(c, appName))
327
+ .get('/api/analytics/campaigns', (c) => handleCampaigns(c, appName)) as T
328
+ }