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,399 @@
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, UniqueConstraintViolationError } from '@/infrastructure/database'
11
+ import {
12
+ injectCreateAuthorship,
13
+ injectUpdateAuthorship,
14
+ } from '../mutation-helpers/authorship-helpers'
15
+ import {
16
+ buildInsertClauses,
17
+ isUniqueConstraintViolation,
18
+ } from '../mutation-helpers/create-record-helpers'
19
+ import {
20
+ cascadeSoftDelete,
21
+ executeSoftDelete,
22
+ executeHardDelete,
23
+ checkDeletedAtColumn,
24
+ } from '../mutation-helpers/delete-helpers'
25
+ import { fetchRecordById } from '../mutation-helpers/record-fetch-helpers'
26
+ import {
27
+ validateFieldsNotEmpty,
28
+ buildUpdateSetClauseCRUD,
29
+ executeRecordUpdateCRUD,
30
+ } from '../mutation-helpers/update-helpers'
31
+ import { logActivity } from '../query-helpers/activity-log-helpers'
32
+ import { wrapDatabaseError } from '../shared/error-handling'
33
+ import { typedExecute } from '../shared/typed-execute'
34
+ import { validateTableName } from '../shared/validation'
35
+ import type { App } from '@/domain/models/app'
36
+ import type { Session } from '@/infrastructure/auth/better-auth/schema'
37
+
38
+ /**
39
+ * Create a new record
40
+ *
41
+ * @param session - Better Auth session
42
+ * @param tableName - Name of the table
43
+ * @param fields - Record fields
44
+ * @returns Effect resolving to created record
45
+ */
46
+ export function createRecord(
47
+ session: Readonly<Session>,
48
+ tableName: string,
49
+ fields: Readonly<Record<string, unknown>>
50
+ ): Effect.Effect<Record<string, unknown>, SessionContextError | UniqueConstraintViolationError> {
51
+ return Effect.gen(function* () {
52
+ const record = yield* Effect.tryPromise({
53
+ try: () =>
54
+ db.transaction(async (tx) => {
55
+ validateTableName(tableName)
56
+
57
+ // Validate we have fields to insert
58
+ if (Object.keys(fields).length === 0) {
59
+ // eslint-disable-next-line functional/no-throw-statements -- Required for transaction error handling
60
+ throw new SessionContextError('Cannot create record with no fields', undefined)
61
+ }
62
+
63
+ // Inject authorship metadata (created_by, updated_by) from session
64
+ const fieldsWithAuthorship = await injectCreateAuthorship(
65
+ fields,
66
+ session.userId,
67
+ tx,
68
+ tableName
69
+ )
70
+
71
+ // Build INSERT query
72
+ const { columnsClause, valuesClause } = buildInsertClauses(fieldsWithAuthorship)
73
+
74
+ // Execute INSERT directly (avoid Effect.runPromise which wraps errors in FiberFailure)
75
+ const insertResult = (await tx.execute(
76
+ sql`INSERT INTO ${sql.identifier(tableName)} (${columnsClause}) VALUES (${valuesClause}) RETURNING *`
77
+ )) as readonly Record<string, unknown>[]
78
+ return insertResult[0] ?? {}
79
+ }),
80
+ catch: (error) => {
81
+ if (error instanceof SessionContextError) return error
82
+ if (error instanceof UniqueConstraintViolationError) return error
83
+ if (isUniqueConstraintViolation(error)) {
84
+ return new UniqueConstraintViolationError('Unique constraint violation', error)
85
+ }
86
+ return new SessionContextError(`Failed to create record in ${tableName}`, error)
87
+ },
88
+ })
89
+
90
+ // Log activity for record creation
91
+ yield* logActivity({
92
+ session,
93
+ tableName,
94
+ action: 'create',
95
+ recordId: String(record.id),
96
+ changes: { after: record },
97
+ })
98
+
99
+ return record
100
+ })
101
+ }
102
+
103
+ /**
104
+ * Log activity for record update
105
+ */
106
+ function logRecordUpdateActivity(config: {
107
+ readonly session: Readonly<Session>
108
+ readonly tableName: string
109
+ readonly recordId: string
110
+ readonly changes: {
111
+ readonly before: Record<string, unknown> | undefined
112
+ readonly after: Record<string, unknown>
113
+ }
114
+ readonly app?: App
115
+ }): Effect.Effect<void, never> {
116
+ const { session, tableName, recordId, changes, app } = config
117
+ return logActivity({
118
+ session,
119
+ tableName,
120
+ action: 'update',
121
+ recordId,
122
+ changes,
123
+ app,
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Update a record
129
+ *
130
+ * @param session - Better Auth session
131
+ * @param tableName - Name of the table
132
+ * @param recordId - Record ID
133
+ * @param params - Update parameters
134
+ * @returns Effect resolving to updated record
135
+ */
136
+ export function updateRecord(
137
+ session: Readonly<Session>,
138
+ tableName: string,
139
+ recordId: string,
140
+ params: {
141
+ readonly fields: Readonly<Record<string, unknown>>
142
+ readonly app?: App
143
+ }
144
+ ): Effect.Effect<Record<string, unknown>, SessionContextError> {
145
+ const { fields, app } = params
146
+ return Effect.gen(function* () {
147
+ const { recordBefore, updatedRecord } = yield* Effect.tryPromise({
148
+ try: () =>
149
+ db.transaction(async (tx) => {
150
+ validateTableName(tableName)
151
+
152
+ // Inject updated_by from session
153
+ const fieldsWithUpdatedBy = await injectUpdateAuthorship(
154
+ fields,
155
+ session.userId,
156
+ tx,
157
+ tableName
158
+ )
159
+
160
+ const entries = await validateFieldsNotEmpty(fieldsWithUpdatedBy)
161
+ const before = await fetchRecordById(tx, tableName, recordId)
162
+ const setClause = buildUpdateSetClauseCRUD(entries)
163
+ const updated = await executeRecordUpdateCRUD(tx, tableName, recordId, setClause)
164
+ return { recordBefore: before, updatedRecord: updated }
165
+ }),
166
+ catch: wrapDatabaseError(`Failed to update record in ${tableName}`),
167
+ })
168
+
169
+ yield* logRecordUpdateActivity({
170
+ session,
171
+ tableName,
172
+ recordId,
173
+ changes: {
174
+ before: recordBefore,
175
+ after: updatedRecord,
176
+ },
177
+ app,
178
+ })
179
+
180
+ return updatedRecord
181
+ })
182
+ }
183
+
184
+ /**
185
+ * Delete a record (soft delete if deleted_at field exists)
186
+ *
187
+ * Implements soft delete pattern:
188
+ * - If table has deleted_at field: Sets deleted_at to NOW() (soft delete)
189
+ * - If no deleted_at field: Performs hard delete
190
+ * - Permissions applied via application layer
191
+ * - Cascade soft delete to related records if configured with onDelete: 'cascade'
192
+ * - Activity logging captures record state before deletion (non-blocking)
193
+ *
194
+ * @param session - Better Auth session
195
+ * @param tableName - Name of the table
196
+ * @param recordId - Record ID
197
+ * @param app - App schema (optional, for cascade delete logic)
198
+ * @returns Effect resolving to success boolean
199
+ */
200
+ // eslint-disable-next-line max-lines-per-function -- Delete logic requires soft-delete check, cascade, and hard-delete fallback
201
+ export function deleteRecord(
202
+ session: Readonly<Session>,
203
+ tableName: string,
204
+ recordId: string,
205
+ app?: {
206
+ readonly tables?: ReadonlyArray<{
207
+ readonly name: string
208
+ readonly fields: ReadonlyArray<{
209
+ readonly name: string
210
+ readonly type: string
211
+ readonly relatedTable?: string
212
+ readonly onDelete?: string
213
+ }>
214
+ }>
215
+ }
216
+ ): Effect.Effect<boolean, SessionContextError> {
217
+ return Effect.gen(function* () {
218
+ const result = yield* Effect.tryPromise({
219
+ try: () =>
220
+ db.transaction(async (tx) => {
221
+ validateTableName(tableName)
222
+
223
+ // Check if table supports soft delete
224
+ const hasSoftDelete = await checkDeletedAtColumn(tx, tableName)
225
+
226
+ // Fetch record before deletion for activity logging
227
+ const recordBeforeData = await fetchRecordById(tx, tableName, recordId)
228
+
229
+ if (hasSoftDelete) {
230
+ // Execute soft delete
231
+ const success = await executeSoftDelete(tx, tableName, recordId, session.userId)
232
+
233
+ if (!success) {
234
+ return { success: false, recordBeforeData: undefined }
235
+ }
236
+
237
+ // Cascade to related records if configured
238
+ if (app) {
239
+ // eslint-disable-next-line functional/no-expression-statements -- Required for cascade operation
240
+ await cascadeSoftDelete(tx, tableName, recordId, app, session.userId)
241
+ }
242
+
243
+ return { success: true, recordBeforeData }
244
+ } else {
245
+ // Execute hard delete
246
+ const success = await executeHardDelete(tx, tableName, recordId)
247
+ return { success, recordBeforeData: undefined }
248
+ }
249
+ }),
250
+ catch: wrapDatabaseError(`Failed to delete record from ${tableName}`),
251
+ })
252
+
253
+ // Log activity for soft delete (outside transaction)
254
+ if (result.success && result.recordBeforeData) {
255
+ yield* logActivity({
256
+ session,
257
+ tableName,
258
+ action: 'delete',
259
+ recordId,
260
+ changes: { before: result.recordBeforeData },
261
+ })
262
+ }
263
+
264
+ return result.success
265
+ })
266
+ }
267
+
268
+ /**
269
+ * Permanently delete a record (hard delete)
270
+ *
271
+ * Permanently removes the record from the database, regardless of deleted_at field.
272
+ * This operation is irreversible and should only be allowed for admin roles.
273
+ * Permissions applied via application layer.
274
+ * Activity logging captures record state before deletion (non-blocking).
275
+ *
276
+ * @param session - Better Auth session
277
+ * @param tableName - Name of the table
278
+ * @param recordId - Record ID
279
+ * @returns Effect resolving to success boolean
280
+ */
281
+ export function permanentlyDeleteRecord(
282
+ session: Readonly<Session>,
283
+ tableName: string,
284
+ recordId: string
285
+ ): Effect.Effect<boolean, SessionContextError> {
286
+ return Effect.gen(function* () {
287
+ const result = yield* Effect.tryPromise({
288
+ try: () =>
289
+ db.transaction(async (tx) => {
290
+ validateTableName(tableName)
291
+
292
+ // Fetch record before deletion for activity logging
293
+ const recordBeforeData = await fetchRecordById(tx, tableName, recordId)
294
+
295
+ // Execute hard delete
296
+ const success = await executeHardDelete(tx, tableName, recordId)
297
+
298
+ return { success, recordBeforeData: success ? recordBeforeData : undefined }
299
+ }),
300
+ catch: wrapDatabaseError(`Failed to permanently delete record from ${tableName}`),
301
+ })
302
+
303
+ // Log activity for permanent delete (outside transaction)
304
+ if (result.success && result.recordBeforeData) {
305
+ yield* logActivity({
306
+ session,
307
+ tableName,
308
+ action: 'delete',
309
+ recordId,
310
+ changes: { before: result.recordBeforeData },
311
+ })
312
+ }
313
+
314
+ return result.success
315
+ })
316
+ }
317
+
318
+ /**
319
+ * Restore a soft-deleted record
320
+ *
321
+ * Clears the deleted_at timestamp to restore a soft-deleted record.
322
+ * Returns error if record doesn't exist or is not soft-deleted.
323
+ * Permissions applied via application layer.
324
+ *
325
+ * @param session - Better Auth session
326
+ * @param tableName - Name of the table
327
+ * @param recordId - Record ID
328
+ * @returns Effect resolving to restored record or null
329
+ */
330
+ // eslint-disable-next-line max-lines-per-function -- Restore logic requires deleted_by column check and conditional restore
331
+ export function restoreRecord(
332
+ session: Readonly<Session>,
333
+ tableName: string,
334
+ recordId: string
335
+ ): Effect.Effect<Record<string, unknown> | null, SessionContextError> {
336
+ return Effect.gen(function* () {
337
+ const restoredRecord = yield* Effect.tryPromise({
338
+ try: () =>
339
+ db.transaction(async (tx) => {
340
+ validateTableName(tableName)
341
+ const tableIdent = sql.identifier(tableName)
342
+
343
+ // Check if record exists (including soft-deleted records)
344
+ const checkResult = await typedExecute(
345
+ tx,
346
+ sql`SELECT id, deleted_at FROM ${tableIdent} WHERE id = ${recordId} LIMIT 1`
347
+ )
348
+
349
+ if (checkResult.length === 0) {
350
+ // eslint-disable-next-line unicorn/no-null -- Null is intentional for non-existent records
351
+ return null // Record not found
352
+ }
353
+
354
+ const record = checkResult[0]
355
+
356
+ // Check if record is soft-deleted
357
+ if (!record?.deleted_at) {
358
+ // Record exists but is not deleted - return error via special marker
359
+ return { _error: 'not_deleted' } as Record<string, unknown>
360
+ }
361
+
362
+ // Check if table has deleted_by column
363
+ const deletedByCheck = await typedExecute(
364
+ tx,
365
+ sql`SELECT column_name FROM information_schema.columns WHERE table_name = ${tableName} AND column_name = 'deleted_by'`
366
+ )
367
+
368
+ const hasDeletedBy = deletedByCheck.length > 0
369
+
370
+ // Restore record by clearing deleted_at and deleted_by (if column exists)
371
+ const result = hasDeletedBy
372
+ ? await typedExecute(
373
+ tx,
374
+ sql`UPDATE ${tableIdent} SET deleted_at = NULL, deleted_by = NULL WHERE id = ${recordId} RETURNING *`
375
+ )
376
+ : await typedExecute(
377
+ tx,
378
+ sql`UPDATE ${tableIdent} SET deleted_at = NULL WHERE id = ${recordId} RETURNING *`
379
+ )
380
+
381
+ return result[0] ?? {}
382
+ }),
383
+ catch: wrapDatabaseError(`Failed to restore record ${recordId} from ${tableName}`),
384
+ })
385
+
386
+ // Log activity for record restoration (outside transaction)
387
+ if (restoredRecord && !('_error' in restoredRecord)) {
388
+ yield* logActivity({
389
+ session,
390
+ tableName,
391
+ action: 'restore',
392
+ recordId,
393
+ changes: { after: restoredRecord },
394
+ })
395
+ }
396
+
397
+ return restoredRecord
398
+ })
399
+ }
@@ -0,0 +1,16 @@
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 CRUD operations from modular files
9
+ export { listRecords, computeAggregations, listTrash, getRecord } from './crud-read'
10
+ export {
11
+ createRecord,
12
+ updateRecord,
13
+ deleteRecord,
14
+ permanentlyDeleteRecord,
15
+ restoreRecord,
16
+ } from './crud-write'
@@ -0,0 +1,11 @@
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 table query functions from modular files
9
+ export * from './shared/validation'
10
+ export * from './crud/crud'
11
+ export * from './batch/batch'
@@ -0,0 +1,152 @@
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 { typedExecute } from '../shared/typed-execute'
10
+ import type { DrizzleTransaction } from '@/infrastructure/database'
11
+
12
+ /**
13
+ * Authorship field names used across the system
14
+ */
15
+ export const AUTHORSHIP_FIELDS = {
16
+ CREATED_BY: 'created_by',
17
+ UPDATED_BY: 'updated_by',
18
+ DELETED_BY: 'deleted_by',
19
+ } as const
20
+
21
+ /**
22
+ * Check if table has specific authorship columns
23
+ * Queries information_schema once per transaction for efficiency
24
+ *
25
+ * @param tx - Database transaction
26
+ * @param tableName - Table name
27
+ * @param columnNames - Column names to check
28
+ * @returns Set of existing column names
29
+ */
30
+ async function checkAuthorshipColumns(
31
+ tx: Readonly<DrizzleTransaction>,
32
+ tableName: string,
33
+ columnNames: readonly string[]
34
+ ): Promise<Set<string>> {
35
+ // Query information_schema for column existence
36
+ const rows = await typedExecute<{ column_name: string }>(
37
+ tx,
38
+ sql.raw(
39
+ `SELECT column_name FROM information_schema.columns WHERE table_name = '${tableName}' AND column_name IN (${columnNames.map((name) => `'${name}'`).join(', ')})`
40
+ )
41
+ )
42
+
43
+ return new Set(rows.map((row) => row.column_name))
44
+ }
45
+
46
+ /**
47
+ * Normalize user ID for database storage
48
+ * Converts 'guest' to NULL for unauthenticated users
49
+ *
50
+ * @param userId - User ID from session
51
+ * @returns Normalized user ID or NULL for guest users
52
+ */
53
+ function normalizeUserIdForDb(userId: string | undefined): string | null {
54
+ // eslint-disable-next-line unicorn/no-null -- NULL is intentional for database columns when no auth configured
55
+ if (!userId || userId === 'guest') return null
56
+ return userId
57
+ }
58
+
59
+ /**
60
+ * Inject authorship fields into record for INSERT operations
61
+ * Sets both created_by and updated_by to the same user ID on creation
62
+ *
63
+ * @param fields - Original record fields
64
+ * @param userId - User ID from session
65
+ * @param tx - Database transaction
66
+ * @param tableName - Table name
67
+ * @returns Fields with authorship metadata injected
68
+ */
69
+ export async function injectCreateAuthorship(
70
+ fields: Readonly<Record<string, unknown>>,
71
+ userId: string | undefined,
72
+ tx: Readonly<DrizzleTransaction>,
73
+ tableName: string
74
+ ): Promise<Record<string, unknown>> {
75
+ // Check which authorship columns exist
76
+ const existingColumns = await checkAuthorshipColumns(tx, tableName, [
77
+ AUTHORSHIP_FIELDS.CREATED_BY,
78
+ AUTHORSHIP_FIELDS.UPDATED_BY,
79
+ ])
80
+
81
+ // Normalize user ID for database
82
+ const authorUserId = normalizeUserIdForDb(userId)
83
+
84
+ // Build fields object with authorship metadata (immutable)
85
+ return {
86
+ ...fields,
87
+ ...(existingColumns.has(AUTHORSHIP_FIELDS.CREATED_BY)
88
+ ? { [AUTHORSHIP_FIELDS.CREATED_BY]: authorUserId }
89
+ : {}),
90
+ ...(existingColumns.has(AUTHORSHIP_FIELDS.UPDATED_BY)
91
+ ? { [AUTHORSHIP_FIELDS.UPDATED_BY]: authorUserId }
92
+ : {}),
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Inject updated_by field for UPDATE operations
98
+ *
99
+ * @param fields - Original record fields
100
+ * @param userId - User ID from session
101
+ * @param tx - Database transaction
102
+ * @param tableName - Table name
103
+ * @returns Fields with updated_by injected
104
+ */
105
+ export async function injectUpdateAuthorship(
106
+ fields: Readonly<Record<string, unknown>>,
107
+ userId: string | undefined,
108
+ tx: Readonly<DrizzleTransaction>,
109
+ tableName: string
110
+ ): Promise<Record<string, unknown>> {
111
+ // Check if updated_by column exists
112
+ const existingColumns = await checkAuthorshipColumns(tx, tableName, [
113
+ AUTHORSHIP_FIELDS.UPDATED_BY,
114
+ ])
115
+
116
+ const authorUserId = normalizeUserIdForDb(userId)
117
+
118
+ return {
119
+ ...fields,
120
+ ...(existingColumns.has(AUTHORSHIP_FIELDS.UPDATED_BY)
121
+ ? { [AUTHORSHIP_FIELDS.UPDATED_BY]: authorUserId }
122
+ : {}),
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Check if table has deleted_by column for soft delete authorship tracking
128
+ *
129
+ * @param tx - Database transaction
130
+ * @param tableName - Table name
131
+ * @returns True if deleted_by column exists
132
+ */
133
+ export async function hasDeletedByColumn(
134
+ tx: Readonly<DrizzleTransaction>,
135
+ tableName: string
136
+ ): Promise<boolean> {
137
+ const existingColumns = await checkAuthorshipColumns(tx, tableName, [
138
+ AUTHORSHIP_FIELDS.DELETED_BY,
139
+ ])
140
+ return existingColumns.has(AUTHORSHIP_FIELDS.DELETED_BY)
141
+ }
142
+
143
+ /**
144
+ * Get normalized user ID for soft delete operations
145
+ * Returns NULL for guest users, user ID otherwise
146
+ *
147
+ * @param userId - User ID from session
148
+ * @returns Normalized user ID or NULL
149
+ */
150
+ export function getDeletedByValue(userId: string | undefined): string | null {
151
+ return normalizeUserIdForDb(userId)
152
+ }
@@ -0,0 +1,90 @@
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
+ SessionContextError,
12
+ UniqueConstraintViolationError,
13
+ type DrizzleTransaction,
14
+ } from '@/infrastructure/database'
15
+ import { validateColumnName } from '../shared/validation'
16
+
17
+ /**
18
+ * Shape of PostgreSQL driver error objects that may contain unique constraint info.
19
+ * Compatible with bun:sql, postgres.js, and pg error objects.
20
+ */
21
+ interface PostgresErrorLike {
22
+ readonly code?: string
23
+ readonly constraint?: string
24
+ readonly message?: string
25
+ readonly cause?: PostgresErrorLike
26
+ }
27
+
28
+ /**
29
+ * Check if an object has PostgreSQL unique constraint violation markers
30
+ * (code 23505, constraint name, or 'unique constraint' in message)
31
+ */
32
+ function hasUniqueViolationMarkers(obj: PostgresErrorLike | null | undefined): boolean {
33
+ return obj?.code === '23505' || !!obj?.constraint || !!obj?.message?.includes('unique constraint')
34
+ }
35
+
36
+ /**
37
+ * Check if an error is a PostgreSQL unique constraint violation (code 23505)
38
+ * Checks the error itself and its cause for violation markers.
39
+ */
40
+ export function isUniqueConstraintViolation(error: unknown): boolean {
41
+ const err = error as PostgresErrorLike | null | undefined
42
+ return hasUniqueViolationMarkers(err) || hasUniqueViolationMarkers(err?.cause)
43
+ }
44
+
45
+ /**
46
+ * Build SQL columns and values for INSERT query
47
+ */
48
+ export function buildInsertClauses(
49
+ fields: Readonly<Record<string, unknown>>
50
+ ): Readonly<{ columnsClause: unknown; valuesClause: unknown }> {
51
+ const entries = Object.entries(fields)
52
+
53
+ // Build column identifiers and values
54
+ const columnIdentifiers = entries.map(([key]) => {
55
+ validateColumnName(key)
56
+ return sql.identifier(key)
57
+ })
58
+ const valueParams = entries.map(([, value]) => sql`${value}`)
59
+
60
+ // Build INSERT query using sql.join for columns and values
61
+ const columnsClause = sql.join(columnIdentifiers, sql.raw(', '))
62
+ const valuesClause = sql.join(valueParams, sql.raw(', '))
63
+
64
+ return { columnsClause, valuesClause }
65
+ }
66
+
67
+ /**
68
+ * Execute INSERT query and handle errors
69
+ */
70
+ export function executeInsert(
71
+ tableName: string,
72
+ columnsClause: unknown,
73
+ valuesClause: unknown,
74
+ tx: Readonly<DrizzleTransaction>
75
+ ): Effect.Effect<Record<string, unknown>, SessionContextError | UniqueConstraintViolationError> {
76
+ return Effect.tryPromise({
77
+ try: async () => {
78
+ const insertResult = (await tx.execute(
79
+ sql`INSERT INTO ${sql.identifier(tableName)} (${columnsClause}) VALUES (${valuesClause}) RETURNING *`
80
+ )) as readonly Record<string, unknown>[]
81
+ return insertResult[0] ?? {}
82
+ },
83
+ catch: (error) => {
84
+ if (isUniqueConstraintViolation(error)) {
85
+ return new UniqueConstraintViolationError('Unique constraint violation', error)
86
+ }
87
+ return new SessionContextError(`Failed to create record in ${tableName}`, error)
88
+ },
89
+ })
90
+ }