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,161 @@
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 { sql } from 'drizzle-orm'
9
+ import { Effect } from 'effect'
10
+ import { db, SessionContextError, type DrizzleTransaction } from '@/infrastructure/database'
11
+ import { logActivity } from '../query-helpers/activity-log-helpers'
12
+ import { wrapDatabaseError } from '../shared/error-handling'
13
+ import { validateTableName } from '../shared/validation'
14
+ import { runEffectInTx } from './batch-helpers'
15
+ import type { Session } from '@/infrastructure/auth/better-auth/schema'
16
+
17
+ /**
18
+ * Validate records exist and filter to only soft-deleted ones
19
+ * Returns array of record IDs that are actually soft-deleted
20
+ * Throws error if any record is not found (404)
21
+ */
22
+ async function validateAndFilterRecordsForRestore(
23
+ tx: Readonly<DrizzleTransaction>,
24
+ tableIdent: Readonly<ReturnType<typeof sql.identifier>>,
25
+ recordIds: readonly string[]
26
+ ): Promise<readonly string[]> {
27
+ const validationResults = await Promise.all(
28
+ recordIds.map(async (recordId) => {
29
+ const checkResult = (await tx.execute(
30
+ sql`SELECT id, deleted_at FROM ${tableIdent} WHERE id = ${recordId} LIMIT 1`
31
+ )) as readonly Record<string, unknown>[]
32
+
33
+ if (checkResult.length === 0) return { recordId, error: 'not found', isDeleted: false }
34
+
35
+ const record = checkResult[0]
36
+ const isDeleted = Boolean(record?.deleted_at)
37
+
38
+ return { recordId, error: undefined, isDeleted }
39
+ })
40
+ )
41
+
42
+ // Check for "not found" errors first (these should return 404)
43
+ const notFoundError = validationResults.find((result) => result.error === 'not found')
44
+ if (notFoundError) {
45
+ // eslint-disable-next-line functional/no-throw-statements -- Required for Effect.tryPromise error handling
46
+ throw new Error(`Record ${notFoundError.recordId} not found`)
47
+ }
48
+
49
+ // Filter to only records that are actually soft-deleted (skip active records)
50
+ return validationResults.filter((result) => result.isDeleted).map((result) => result.recordId)
51
+ }
52
+
53
+ /**
54
+ * Validate and filter records for restore with Effect error handling
55
+ * Returns array of record IDs that are actually soft-deleted
56
+ */
57
+ function validateAndFilterRecordsWithEffect(
58
+ tx: Readonly<DrizzleTransaction>,
59
+ tableIdent: Readonly<ReturnType<typeof sql.identifier>>,
60
+ recordIds: readonly string[]
61
+ ): Effect.Effect<readonly string[], SessionContextError> {
62
+ return Effect.tryPromise({
63
+ try: () => validateAndFilterRecordsForRestore(tx, tableIdent, recordIds),
64
+ catch: (error) => {
65
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
66
+ return new SessionContextError(`Validation failed: ${errorMessage}`, error)
67
+ },
68
+ })
69
+ }
70
+
71
+ /**
72
+ * Execute restore query using parameterized IN clause
73
+ */
74
+ function executeRestoreQuery(
75
+ tx: Readonly<DrizzleTransaction>,
76
+ tableIdent: Readonly<ReturnType<typeof sql.identifier>>,
77
+ tableName: string,
78
+ recordIds: readonly string[]
79
+ ): Effect.Effect<readonly Record<string, unknown>[], SessionContextError> {
80
+ return Effect.tryPromise({
81
+ try: async () => {
82
+ const idParams = sql.join(
83
+ recordIds.map((id) => sql`${id}`),
84
+ sql.raw(', ')
85
+ )
86
+ const result = (await tx.execute(
87
+ sql`UPDATE ${tableIdent} SET deleted_at = NULL WHERE id IN (${idParams}) RETURNING *`
88
+ )) as readonly Record<string, unknown>[]
89
+ return result
90
+ },
91
+ catch: (error) => new SessionContextError(`Failed to restore records in ${tableName}`, error),
92
+ })
93
+ }
94
+
95
+ /**
96
+ * Log restore activities for all restored records
97
+ */
98
+ function logRestoreActivities(
99
+ session: Readonly<Session>,
100
+ tableName: string,
101
+ restoredRecords: readonly Record<string, unknown>[]
102
+ ): Effect.Effect<void, never> {
103
+ return Effect.forEach(restoredRecords, (record) =>
104
+ logActivity({
105
+ session,
106
+ tableName,
107
+ action: 'restore',
108
+ recordId: String(record.id),
109
+ changes: { after: record },
110
+ })
111
+ ).pipe(Effect.asVoid)
112
+ }
113
+
114
+ /**
115
+ * Batch restore soft-deleted records
116
+ *
117
+ * Restores multiple soft-deleted records in a transaction.
118
+ * Validates all records exist and are soft-deleted before restoring any.
119
+ * Rolls back if any record fails validation.
120
+ * Permissions enforced at the presentation layer (route handler).
121
+ *
122
+ * @param session - Better Auth session
123
+ * @param tableName - Name of the table
124
+ * @param recordIds - Array of record IDs to restore
125
+ * @returns Effect resolving to number of restored records or error
126
+ */
127
+ export function batchRestoreRecords(
128
+ session: Readonly<Session>,
129
+ tableName: string,
130
+ recordIds: readonly string[]
131
+ ): Effect.Effect<number, SessionContextError> {
132
+ return Effect.gen(function* () {
133
+ const restoredRecords = yield* Effect.tryPromise({
134
+ try: () =>
135
+ db.transaction(async (tx) => {
136
+ validateTableName(tableName)
137
+ const tableIdent = sql.identifier(tableName)
138
+
139
+ // Validate and filter to only soft-deleted records
140
+ const deletedRecordIds = await runEffectInTx(
141
+ validateAndFilterRecordsWithEffect(tx, tableIdent, recordIds)
142
+ )
143
+
144
+ // If no records to restore, return empty array
145
+ if (deletedRecordIds.length === 0) {
146
+ return []
147
+ }
148
+
149
+ // Restore only the filtered soft-deleted records
150
+ return await runEffectInTx(
151
+ executeRestoreQuery(tx, tableIdent, tableName, deletedRecordIds)
152
+ )
153
+ }),
154
+ catch: wrapDatabaseError(`Failed to restore records in ${tableName}`),
155
+ })
156
+
157
+ yield* logRestoreActivities(session, tableName, restoredRecords)
158
+
159
+ return restoredRecords.length
160
+ })
161
+ }
@@ -0,0 +1,146 @@
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 { sql } from 'drizzle-orm'
9
+ import { Effect } from 'effect'
10
+ import {
11
+ db,
12
+ ValidationError,
13
+ type DrizzleTransaction,
14
+ type SessionContextError,
15
+ } from '@/infrastructure/database'
16
+ import { injectUpdateAuthorship } from '../mutation-helpers/authorship-helpers'
17
+ import { fetchRecordByIdEffect } from '../mutation-helpers/record-fetch-helpers'
18
+ import { buildUpdateSetClauseCRUD } from '../mutation-helpers/update-helpers'
19
+ import { logActivity } from '../query-helpers/activity-log-helpers'
20
+ import { wrapDatabaseErrorWithValidation } from '../shared/error-handling'
21
+ import { validateTableName } from '../shared/validation'
22
+ import { runEffectInTx } from './batch-helpers'
23
+ import type { Session } from '@/infrastructure/auth/better-auth/schema'
24
+
25
+ /**
26
+ * Extract fields from update object (requires nested format)
27
+ */
28
+ function extractFieldsFromUpdate(update: {
29
+ readonly id: string
30
+ readonly fields?: Readonly<Record<string, unknown>>
31
+ }): Readonly<Record<string, unknown>> {
32
+ // Return fields property or empty object if not provided
33
+ return update.fields ?? {}
34
+ }
35
+
36
+ /**
37
+ * Execute UPDATE query and return updated record
38
+ */
39
+ function executeRecordUpdate(
40
+ tx: Readonly<DrizzleTransaction>,
41
+ tableName: string,
42
+ recordId: string,
43
+ setClause: Readonly<ReturnType<typeof sql.join>>
44
+ ): Effect.Effect<Record<string, unknown> | undefined, ValidationError> {
45
+ return Effect.tryPromise({
46
+ try: async () => {
47
+ const result = (await tx.execute(
48
+ sql`UPDATE ${sql.identifier(tableName)} SET ${setClause} WHERE id = ${recordId} RETURNING *`
49
+ )) as readonly Record<string, unknown>[]
50
+ return result[0]
51
+ },
52
+ catch: (error) => {
53
+ // PostgreSQL error codes: https://www.postgresql.org/docs/current/errcodes-appendix.html
54
+ // 23502 = not_null_violation
55
+ const pgError = error as { code?: string; message?: string; constraint?: string }
56
+ if (pgError.code === '23502' || pgError.message?.includes('null value in column')) {
57
+ // Extract field name from error message if possible
58
+ const fieldMatch = pgError.message?.match(/column "([^"]+)"/)
59
+ const fieldName: string = fieldMatch?.[1] ?? 'unknown'
60
+ return new ValidationError(`Cannot set required field '${fieldName}' to null`, [
61
+ { record: 0, field: fieldName, error: 'Required field cannot be null' },
62
+ ])
63
+ }
64
+ // For other errors, return generic validation error
65
+ const errorMessage: string =
66
+ pgError.message !== undefined
67
+ ? pgError.message
68
+ : 'Update failed due to constraint violation'
69
+ return new ValidationError(errorMessage, [])
70
+ },
71
+ })
72
+ }
73
+
74
+ /**
75
+ * Update a single record within a batch operation
76
+ */
77
+ function updateSingleRecordInBatch(
78
+ tx: Readonly<DrizzleTransaction>,
79
+ tableName: string,
80
+ session: Readonly<Session>,
81
+ update: { readonly id: string; readonly fields?: Record<string, unknown> }
82
+ ): Effect.Effect<Record<string, unknown> | undefined, ValidationError> {
83
+ return Effect.gen(function* () {
84
+ const fieldsToUpdate = extractFieldsFromUpdate(update)
85
+
86
+ if (Object.keys(fieldsToUpdate).length === 0) return undefined
87
+
88
+ // Inject updated_by authorship metadata
89
+ const fieldsWithAuthorship = yield* Effect.promise(() =>
90
+ injectUpdateAuthorship(fieldsToUpdate, session.userId, tx, tableName)
91
+ )
92
+
93
+ const entries = Object.entries(fieldsWithAuthorship)
94
+ const recordBefore = yield* fetchRecordByIdEffect(tx, tableName, update.id)
95
+ const setClause = buildUpdateSetClauseCRUD(entries)
96
+ const updatedRecord = yield* executeRecordUpdate(tx, tableName, update.id, setClause)
97
+
98
+ if (updatedRecord) {
99
+ yield* logActivity({
100
+ session,
101
+ tableName,
102
+ action: 'update',
103
+ recordId: String(update.id),
104
+ changes: { before: recordBefore, after: updatedRecord },
105
+ })
106
+ return updatedRecord
107
+ }
108
+
109
+ return undefined
110
+ })
111
+ }
112
+
113
+ /**
114
+ * Batch update records
115
+ *
116
+ * Updates multiple records in a transaction with permission enforcement.
117
+ * Only records the user has permission to update will be affected.
118
+ * Records without permission are silently skipped.
119
+ *
120
+ * @param session - Better Auth session
121
+ * @param tableName - Name of the table
122
+ * @param updates - Array of records with id and fields to update (requires nested format)
123
+ * @returns Effect resolving to array of updated records
124
+ */
125
+ export function batchUpdateRecords(
126
+ session: Readonly<Session>,
127
+ tableName: string,
128
+ updates: readonly { readonly id: string; readonly fields?: Record<string, unknown> }[]
129
+ ): Effect.Effect<readonly Record<string, unknown>[], SessionContextError | ValidationError> {
130
+ return Effect.tryPromise({
131
+ try: () =>
132
+ db.transaction(async (tx) => {
133
+ validateTableName(tableName)
134
+
135
+ return await runEffectInTx(
136
+ // Process updates sequentially with immutable array building
137
+ Effect.reduce(updates, [] as readonly Record<string, unknown>[], (acc, update) =>
138
+ updateSingleRecordInBatch(tx, tableName, session, update).pipe(
139
+ Effect.map((record) => (record ? [...acc, record] : acc))
140
+ )
141
+ )
142
+ )
143
+ }),
144
+ catch: wrapDatabaseErrorWithValidation(`Failed to batch update records in ${tableName}`),
145
+ })
146
+ }
@@ -0,0 +1,357 @@
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 { sql } from 'drizzle-orm'
9
+ import { Effect } from 'effect'
10
+ import {
11
+ db,
12
+ SessionContextError,
13
+ ValidationError,
14
+ type DrizzleTransaction,
15
+ } from '@/infrastructure/database'
16
+ import { logActivity } from '../query-helpers/activity-log-helpers'
17
+ import { typedExecute } from '../shared/typed-execute'
18
+ import { validateTableName, validateColumnName } from '../shared/validation'
19
+ import { BatchValidationError, runEffectInTx, createSingleRecord } from './batch-helpers'
20
+ import type { Session } from '@/infrastructure/auth/better-auth/schema'
21
+
22
+ /**
23
+ * Helper to update existing record
24
+ */
25
+ async function updateSingleRecord(
26
+ tx: Readonly<DrizzleTransaction>,
27
+ tableName: string,
28
+ recordId: string,
29
+ params: { readonly fields: Record<string, unknown>; readonly fieldsToMergeOn: readonly string[] }
30
+ ): Promise<Record<string, unknown> | undefined> {
31
+ const updateEntries = Object.entries(params.fields).filter(
32
+ ([key]) => !params.fieldsToMergeOn.includes(key)
33
+ )
34
+ if (updateEntries.length === 0) return undefined
35
+
36
+ const setExpressions = updateEntries.map(([key, value]) => {
37
+ validateColumnName(key)
38
+ return sql`${sql.identifier(key)} = ${value}`
39
+ })
40
+ const setClause = sql.join(setExpressions, sql.raw(', '))
41
+
42
+ const result = (await tx.execute(
43
+ sql`UPDATE ${sql.identifier(tableName)} SET ${setClause} WHERE id = ${recordId} RETURNING *`
44
+ )) as readonly Record<string, unknown>[]
45
+
46
+ return result[0] ?? undefined
47
+ }
48
+
49
+ type UpsertResult = {
50
+ readonly records: readonly Record<string, unknown>[]
51
+ readonly created: number
52
+ readonly updated: number
53
+ }
54
+
55
+ /**
56
+ * Check if record exists based on merge fields
57
+ */
58
+ function findExistingRecord(
59
+ tx: Readonly<DrizzleTransaction>,
60
+ tableName: string,
61
+ fields: Readonly<Record<string, unknown>>,
62
+ fieldsToMergeOn: readonly string[]
63
+ ): Effect.Effect<Readonly<Record<string, unknown>> | undefined, SessionContextError> {
64
+ const whereConditions = fieldsToMergeOn.map((field) => {
65
+ validateColumnName(field)
66
+ return sql`${sql.identifier(field)} = ${fields[field]}`
67
+ })
68
+ const whereClause = sql.join(whereConditions, sql.raw(' AND '))
69
+
70
+ return Effect.tryPromise({
71
+ try: async () => {
72
+ const result = (await tx.execute(
73
+ sql`SELECT * FROM ${sql.identifier(tableName)} WHERE ${whereClause} LIMIT 1`
74
+ )) as readonly Record<string, unknown>[]
75
+ return result[0]
76
+ },
77
+ catch: (error) =>
78
+ new SessionContextError(`Failed to check existing record in ${tableName}`, error),
79
+ })
80
+ }
81
+
82
+ /**
83
+ * Handle update path in upsert
84
+ */
85
+ function handleUpsertUpdate(
86
+ tx: Readonly<DrizzleTransaction>,
87
+ params: {
88
+ readonly session: Readonly<Session>
89
+ readonly tableName: string
90
+ readonly fields: Record<string, unknown>
91
+ readonly fieldsToMergeOn: readonly string[]
92
+ readonly existing: Record<string, unknown>
93
+ readonly acc: UpsertResult
94
+ }
95
+ ): Effect.Effect<UpsertResult, SessionContextError> {
96
+ return Effect.gen(function* () {
97
+ const recordId = String(params.existing.id)
98
+ const updated = yield* Effect.tryPromise({
99
+ try: async () =>
100
+ updateSingleRecord(tx, params.tableName, recordId, {
101
+ fields: params.fields,
102
+ fieldsToMergeOn: params.fieldsToMergeOn,
103
+ }),
104
+ catch: (error) =>
105
+ new SessionContextError(`Failed to update record in ${params.tableName}`, error),
106
+ })
107
+
108
+ if (!updated) {
109
+ return {
110
+ records: [...params.acc.records, params.existing],
111
+ created: params.acc.created,
112
+ updated: params.acc.updated + 1,
113
+ }
114
+ }
115
+
116
+ yield* logActivity({
117
+ session: params.session,
118
+ tableName: params.tableName,
119
+ action: 'update',
120
+ recordId,
121
+ changes: { before: params.existing, after: updated },
122
+ })
123
+
124
+ return {
125
+ records: [...params.acc.records, updated],
126
+ created: params.acc.created,
127
+ updated: params.acc.updated + 1,
128
+ }
129
+ })
130
+ }
131
+
132
+ /**
133
+ * Handle create path in upsert
134
+ */
135
+ function handleUpsertCreate(
136
+ tx: Readonly<DrizzleTransaction>,
137
+ params: {
138
+ readonly session: Readonly<Session>
139
+ readonly tableName: string
140
+ readonly fields: Record<string, unknown>
141
+ readonly acc: UpsertResult
142
+ }
143
+ ): Effect.Effect<UpsertResult, SessionContextError | ValidationError> {
144
+ return Effect.gen(function* () {
145
+ const created = yield* Effect.tryPromise({
146
+ try: async () => createSingleRecord(tx, params.tableName, params.fields),
147
+ catch: (error) => {
148
+ // If this is a ValidationError, propagate it as-is
149
+ if (error instanceof ValidationError) {
150
+ return error
151
+ }
152
+ return new SessionContextError(`Failed to create record in ${params.tableName}`, error)
153
+ },
154
+ })
155
+
156
+ if (!created) return params.acc
157
+
158
+ yield* logActivity({
159
+ session: params.session,
160
+ tableName: params.tableName,
161
+ action: 'create',
162
+ recordId: String(created.id),
163
+ changes: { after: created },
164
+ })
165
+
166
+ return {
167
+ records: [...params.acc.records, created],
168
+ created: params.acc.created + 1,
169
+ updated: params.acc.updated,
170
+ }
171
+ })
172
+ }
173
+
174
+ /**
175
+ * Process single upsert operation
176
+ */
177
+ function processSingleUpsert(
178
+ tx: Readonly<DrizzleTransaction>,
179
+ params: {
180
+ readonly session: Readonly<Session>
181
+ readonly tableName: string
182
+ readonly fields: Record<string, unknown>
183
+ readonly fieldsToMergeOn: readonly string[]
184
+ readonly acc: UpsertResult
185
+ }
186
+ ): Effect.Effect<UpsertResult, SessionContextError | ValidationError> {
187
+ return Effect.gen(function* () {
188
+ const existing = yield* findExistingRecord(
189
+ tx,
190
+ params.tableName,
191
+ params.fields,
192
+ params.fieldsToMergeOn
193
+ )
194
+
195
+ if (existing) {
196
+ return yield* handleUpsertUpdate(tx, { ...params, existing })
197
+ }
198
+
199
+ return yield* handleUpsertCreate(tx, params)
200
+ })
201
+ }
202
+
203
+ /**
204
+ * Validate merge fields are present in all records
205
+ */
206
+ function validateMergeFieldsPresent(
207
+ recordsData: readonly Record<string, unknown>[],
208
+ fieldsToMergeOn: readonly string[]
209
+ ): Effect.Effect<void, BatchValidationError> {
210
+ const errors = recordsData.flatMap((record, index) => {
211
+ const missingFields = fieldsToMergeOn.filter((field) => !(field in record))
212
+ return missingFields.length > 0
213
+ ? [`Record ${index}: Missing merge field(s) ${missingFields.join(', ')}`]
214
+ : []
215
+ })
216
+
217
+ if (errors.length > 0) {
218
+ return Effect.fail(
219
+ new BatchValidationError({
220
+ message: 'Batch validation failed',
221
+ details: errors,
222
+ })
223
+ )
224
+ }
225
+
226
+ return Effect.void
227
+ }
228
+
229
+ /**
230
+ * Validate required fields are present in record (for creates)
231
+ * This prevents database NOT NULL constraint violations
232
+ */
233
+ async function validateRequiredFieldsInRecord(
234
+ tx: Readonly<DrizzleTransaction>,
235
+ tableName: string,
236
+ record: Readonly<Record<string, unknown>>,
237
+ recordIndex: number
238
+ ): Promise<readonly string[]> {
239
+ // Query table schema to get required fields
240
+ const schemaQuery = await typedExecute<{ column_name: string }>(
241
+ tx,
242
+ sql`
243
+ SELECT column_name, is_nullable, column_default
244
+ FROM information_schema.columns
245
+ WHERE table_name = ${tableName}
246
+ AND table_schema = 'public'
247
+ AND is_nullable = 'NO'
248
+ AND column_default IS NULL
249
+ `
250
+ )
251
+
252
+ const requiredFields = schemaQuery.map((row) => row.column_name)
253
+
254
+ // System fields that are auto-generated (exclude from validation)
255
+ const autoFields = new Set(['id', 'created_at', 'updated_at'])
256
+
257
+ const missingFields = requiredFields.filter(
258
+ (field) => !autoFields.has(field) && !(field in record)
259
+ )
260
+
261
+ if (missingFields.length > 0) {
262
+ return [`Record ${recordIndex}: Missing required field(s) ${missingFields.join(', ')}`]
263
+ }
264
+
265
+ return []
266
+ }
267
+
268
+ /**
269
+ * Validate all records have required fields BEFORE processing
270
+ */
271
+ function validateAllRecordsHaveRequiredFields(
272
+ tx: Readonly<DrizzleTransaction>,
273
+ tableName: string,
274
+ recordsData: readonly Record<string, unknown>[]
275
+ ): Effect.Effect<void, BatchValidationError> {
276
+ return Effect.gen(function* () {
277
+ const allErrors = yield* Effect.reduce(
278
+ recordsData,
279
+ [] as readonly string[],
280
+ (acc, record, index) =>
281
+ Effect.tryPromise({
282
+ try: () => validateRequiredFieldsInRecord(tx, tableName, record, index),
283
+ catch: (error) =>
284
+ new BatchValidationError({
285
+ message: 'Failed to validate record',
286
+ details: [String(error)],
287
+ }),
288
+ }).pipe(Effect.map((recordErrors) => [...acc, ...recordErrors]))
289
+ )
290
+
291
+ if (allErrors.length > 0) {
292
+ return yield* new BatchValidationError({
293
+ message: 'Batch validation failed',
294
+ details: allErrors,
295
+ })
296
+ }
297
+ })
298
+ }
299
+
300
+ /**
301
+ * Upsert records (create or update based on merge fields)
302
+ */
303
+ export function upsertRecords(
304
+ session: Readonly<Session>,
305
+ tableName: string,
306
+ recordsData: readonly Record<string, unknown>[],
307
+ fieldsToMergeOn: readonly string[]
308
+ ): Effect.Effect<UpsertResult, SessionContextError | BatchValidationError | ValidationError> {
309
+ return Effect.gen(function* () {
310
+ validateTableName(tableName)
311
+
312
+ if (recordsData.length === 0) {
313
+ return yield* Effect.fail(
314
+ new SessionContextError('Cannot upsert batch with no records', undefined)
315
+ )
316
+ }
317
+
318
+ if (fieldsToMergeOn.length === 0) {
319
+ return yield* Effect.fail(
320
+ new SessionContextError('Cannot upsert without merge fields', undefined)
321
+ )
322
+ }
323
+
324
+ fieldsToMergeOn.forEach((field) => validateColumnName(field))
325
+
326
+ // Validate merge fields are present in all records BEFORE processing
327
+ yield* validateMergeFieldsPresent(recordsData, fieldsToMergeOn)
328
+
329
+ // Execute upsert in a transaction
330
+ const result = yield* Effect.tryPromise({
331
+ try: () =>
332
+ db.transaction(async (tx) => {
333
+ // eslint-disable-next-line functional/no-expression-statements -- Required for transaction validation
334
+ await runEffectInTx(validateAllRecordsHaveRequiredFields(tx, tableName, recordsData))
335
+
336
+ return await runEffectInTx(
337
+ Effect.reduce(
338
+ recordsData,
339
+ { records: [], created: 0, updated: 0 } as UpsertResult,
340
+ (acc, fields) =>
341
+ processSingleUpsert(tx, { session, tableName, fields, fieldsToMergeOn, acc })
342
+ )
343
+ )
344
+ }),
345
+ catch: (error) => {
346
+ if (error instanceof SessionContextError) return error
347
+ if (error instanceof BatchValidationError) {
348
+ // Re-wrap BatchValidationError as SessionContextError to match return type
349
+ return new SessionContextError(error.message, error)
350
+ }
351
+ return new SessionContextError(`Failed to upsert records in ${tableName}`, error)
352
+ },
353
+ })
354
+
355
+ return result
356
+ })
357
+ }
@@ -0,0 +1,14 @@
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
+ // Re-export all batch operations from modular files
9
+ export { BatchValidationError } from './batch-helpers'
10
+ export { batchCreateRecords } from './batch-create'
11
+ export { upsertRecords } from './batch-upsert'
12
+ export { batchRestoreRecords } from './batch-restore'
13
+ export { batchUpdateRecords } from './batch-update'
14
+ export { batchDeleteRecords } from './batch-delete'