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,471 @@
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
+ hasCreatePermission,
12
+ hasUpdatePermission,
13
+ } from '@/application/use-cases/tables/permissions/permissions'
14
+ import { filterReadableFields } from '@/application/use-cases/tables/utils/field-read-filter'
15
+ import { db } from '@/infrastructure/database/drizzle'
16
+ import { validateFieldWritePermissions } from '@/presentation/api/utils/field-permission-validator'
17
+ import { validateRequiredFieldsForRecord } from './create-record-helpers'
18
+ import type { App } from '@/domain/models/app'
19
+ import type { Context } from 'hono'
20
+
21
+ /**
22
+ * Validate required fields for upsert records
23
+ * Records come from schema in nested format: { fields: {...} }
24
+ */
25
+ export async function validateUpsertRequiredFields(
26
+ table: NonNullable<App['tables']>[number] | undefined,
27
+ records: readonly { fields: Record<string, unknown> }[]
28
+ ): Promise<Array<{ record: number; field: string; error: string }>> {
29
+ return records.flatMap((record, index) => {
30
+ // Extract fields from nested format
31
+ const missingFields = validateRequiredFieldsForRecord(table, record.fields)
32
+ return missingFields.map((field: string) => ({
33
+ record: index,
34
+ field,
35
+ error: 'Required field is missing',
36
+ }))
37
+ })
38
+ }
39
+
40
+ /**
41
+ * Check if any records exist in database based on merge fields
42
+ */
43
+ export async function checkForExistingRecords(
44
+ tableName: string,
45
+ records: readonly { fields: Record<string, unknown> }[],
46
+ fieldsToMergeOn: readonly string[]
47
+ ): Promise<boolean> {
48
+ // Build WHERE clause - skip records missing merge fields (will fail validation)
49
+ const mergeConditions = records
50
+ .filter((record) =>
51
+ fieldsToMergeOn.every((fieldName) => record.fields[fieldName] !== undefined)
52
+ )
53
+ .map((record) => {
54
+ const conditions = fieldsToMergeOn.map((fieldName) => {
55
+ const value = record.fields[fieldName]
56
+ return sql`${sql.identifier(fieldName)} = ${value}`
57
+ })
58
+ return conditions.length > 0 ? sql.join(conditions, sql` AND `) : sql`1=0`
59
+ })
60
+
61
+ // If no valid records to check, return false
62
+ if (mergeConditions.length === 0) return false
63
+
64
+ const whereClause = sql.join(mergeConditions, sql` OR `)
65
+ const existingRecords = (await db.execute(
66
+ sql`SELECT COUNT(*) as count FROM ${sql.identifier(tableName)} WHERE ${whereClause}`
67
+ )) as readonly Record<string, unknown>[]
68
+
69
+ const firstRecord = existingRecords[0]
70
+ return firstRecord !== undefined && Number(firstRecord.count) > 0
71
+ }
72
+
73
+ /**
74
+ * Validate field-level write permissions for records
75
+ */
76
+ export function checkFieldPermissions(config: {
77
+ readonly app: App
78
+ readonly tableName: string
79
+ readonly userRole: string
80
+ readonly records: readonly { fields: Record<string, unknown> }[]
81
+ readonly c: Context
82
+ }): { allowed: true } | { allowed: false; response: Response } {
83
+ const { app, tableName, userRole, records, c } = config
84
+
85
+ const allForbiddenFields = records
86
+ .map((record) => validateFieldWritePermissions(app, tableName, userRole, record.fields))
87
+ .filter((fields) => fields.length > 0)
88
+
89
+ if (allForbiddenFields.length > 0) {
90
+ const uniqueForbiddenFields = [...new Set(allForbiddenFields.flat())]
91
+ const firstForbiddenField = uniqueForbiddenFields[0]
92
+ return {
93
+ allowed: false,
94
+ response: c.json(
95
+ {
96
+ success: false,
97
+ message: `Cannot write to field '${firstForbiddenField}': insufficient permissions`,
98
+ code: 'FORBIDDEN',
99
+ },
100
+ 403
101
+ ),
102
+ }
103
+ }
104
+
105
+ return { allowed: true }
106
+ }
107
+
108
+ /**
109
+ * Check upsert permissions including update permission check
110
+ * This function determines if records will be created or updated, then checks appropriate permissions
111
+ * Note: Field-level permissions should be checked separately before calling this function
112
+ */
113
+
114
+ export async function checkUpsertPermissionsWithUpdateCheck(config: {
115
+ readonly app: App
116
+ readonly tableName: string
117
+ readonly userRole: string
118
+ readonly records: readonly { fields: Record<string, unknown> }[]
119
+ readonly fieldsToMergeOn: readonly string[]
120
+ readonly c: Context
121
+ }): Promise<{ allowed: true } | { allowed: false; response: Response }> {
122
+ const { app, tableName, userRole, records, fieldsToMergeOn, c } = config
123
+ const table = app.tables?.find((t) => t.name === tableName)
124
+
125
+ // Check if any records will be updated
126
+ const hasExistingRecords = await checkForExistingRecords(tableName, records, fieldsToMergeOn)
127
+
128
+ // If records will be updated, check update permission
129
+ if (hasExistingRecords && !hasUpdatePermission(table, userRole, app.tables)) {
130
+ return {
131
+ allowed: false,
132
+ response: c.json(
133
+ {
134
+ success: false,
135
+ message: 'You do not have permission to update records in this table',
136
+ code: 'FORBIDDEN',
137
+ },
138
+ 403
139
+ ),
140
+ }
141
+ }
142
+
143
+ // Check table-level create permission (for new records)
144
+ if (!hasCreatePermission(table, userRole, app.tables)) {
145
+ return {
146
+ allowed: false,
147
+ response: c.json(
148
+ {
149
+ success: false,
150
+ message: 'You do not have permission to create records in this table',
151
+ code: 'FORBIDDEN',
152
+ },
153
+ 403
154
+ ),
155
+ }
156
+ }
157
+
158
+ return { allowed: true }
159
+ }
160
+
161
+ /**
162
+ * Check if a field type is readonly (cannot be set by users)
163
+ */
164
+ export function isReadonlyFieldType(fieldType: string): boolean {
165
+ const readonlyTypes = new Set(['created-at', 'updated-at', 'auto-number'])
166
+ return readonlyTypes.has(fieldType)
167
+ }
168
+
169
+ /**
170
+ * Validate that no readonly fields are being set
171
+ * Returns error response if readonly fields detected, undefined otherwise
172
+ */
173
+ export function validateReadonlyFields(
174
+ table:
175
+ | {
176
+ readonly fields: ReadonlyArray<{
177
+ readonly name: string
178
+ readonly type: string
179
+ }>
180
+ }
181
+ | undefined,
182
+ records: readonly { fields: Record<string, unknown> }[],
183
+ c: Context
184
+ ) {
185
+ // Check for 'id' field (always readonly)
186
+ const recordWithId = records.find((record) => 'id' in record.fields)
187
+ if (recordWithId) {
188
+ return c.json(
189
+ {
190
+ success: false,
191
+ message: "Cannot write to readonly field 'id'",
192
+ code: 'VALIDATION_ERROR',
193
+ },
194
+ 400
195
+ )
196
+ }
197
+
198
+ // Check for readonly field types (created-at, updated-at, auto-number)
199
+ if (table) {
200
+ const readonlyFieldNames = new Set(
201
+ table.fields.filter((field) => isReadonlyFieldType(field.type)).map((field) => field.name)
202
+ )
203
+
204
+ const attemptedReadonlyField = records
205
+ .flatMap((record) => Object.keys(record.fields))
206
+ .find((fieldName) => readonlyFieldNames.has(fieldName))
207
+
208
+ if (attemptedReadonlyField) {
209
+ return c.json(
210
+ {
211
+ success: false,
212
+ message: `Cannot write to readonly field '${attemptedReadonlyField}'`,
213
+ code: 'VALIDATION_ERROR',
214
+ },
215
+ 400
216
+ )
217
+ }
218
+ }
219
+
220
+ return undefined
221
+ }
222
+
223
+ /**
224
+ * Strip protected fields that user cannot write from records
225
+ * This prevents 403 errors for fields user doesn't have write access to
226
+ */
227
+ export function stripUnwritableFields<T extends { fields: Record<string, unknown> }>(
228
+ app: App,
229
+ tableName: string,
230
+ userRole: string,
231
+ records: readonly T[]
232
+ ): T[] {
233
+ return records.map((record) => {
234
+ const forbiddenFields = validateFieldWritePermissions(app, tableName, userRole, record.fields)
235
+ if (forbiddenFields.length === 0) {
236
+ return record
237
+ }
238
+
239
+ // Remove forbidden fields from the record
240
+ const filteredFields = Object.keys(record.fields).reduce<Record<string, unknown>>(
241
+ (acc, key) => {
242
+ if (!forbiddenFields.includes(key)) {
243
+ return { ...acc, [key]: record.fields[key] }
244
+ }
245
+ return acc
246
+ },
247
+ {}
248
+ )
249
+
250
+ return { ...record, fields: filteredFields } as T
251
+ })
252
+ }
253
+
254
+ type UpsertResponse = {
255
+ readonly created: number
256
+ readonly updated: number
257
+ readonly records?: ReadonlyArray<{ readonly fields: Record<string, unknown> }>
258
+ }
259
+
260
+ /**
261
+ * Apply field-level read filtering to upsert response
262
+ */
263
+ export function applyReadFiltering<E, R>(config: {
264
+ readonly program: Effect.Effect<UpsertResponse, E, R>
265
+ readonly app: App
266
+ readonly tableName: string
267
+ readonly userRole: string
268
+ readonly userId: string
269
+ }): Effect.Effect<UpsertResponse, E, R> {
270
+ const { program, app, tableName, userRole, userId } = config
271
+
272
+ return program.pipe(
273
+ Effect.map((response) => {
274
+ if (!response.records) return response
275
+
276
+ const filteredRecords = response.records.map(
277
+ (record) =>
278
+ ({
279
+ ...record,
280
+ fields: filterReadableFields({
281
+ app,
282
+ tableName,
283
+ userRole,
284
+ userId,
285
+ record: record.fields,
286
+ }),
287
+ }) as { readonly fields: Record<string, unknown> }
288
+ )
289
+
290
+ return {
291
+ created: response.created,
292
+ updated: response.updated,
293
+ records: filteredRecords as ReadonlyArray<{ readonly fields: Record<string, unknown> }>,
294
+ }
295
+ })
296
+ )
297
+ }
298
+
299
+ /**
300
+ * Create 403 response for protected field write attempt
301
+ */
302
+ function createForbiddenFieldResponse(c: Context, forbiddenField: string): Response {
303
+ return c.json(
304
+ {
305
+ success: false,
306
+ message: `Cannot write to field '${forbiddenField}': insufficient permissions`,
307
+ code: 'FORBIDDEN',
308
+ },
309
+ 403
310
+ )
311
+ }
312
+
313
+ /**
314
+ * Check if single-record upsert contains protected fields
315
+ * Single-record upserts reject if ANY protected fields present
316
+ */
317
+ function checkSingleRecordProtectedFields(config: {
318
+ readonly c: Context
319
+ readonly app: App
320
+ readonly tableName: string
321
+ readonly userRole: string
322
+ readonly records: readonly { fields: Record<string, unknown> }[]
323
+ }): { success: true } | { success: false; response: Response } {
324
+ const { c, app, tableName, userRole, records } = config
325
+
326
+ if (records.length !== 1) {
327
+ return { success: true }
328
+ }
329
+
330
+ const allForbiddenFields = records
331
+ .map((record) => validateFieldWritePermissions(app, tableName, userRole, record.fields))
332
+ .filter((fields) => fields.length > 0)
333
+
334
+ if (allForbiddenFields.length > 0) {
335
+ const uniqueForbiddenFields = [...new Set(allForbiddenFields.flat())]
336
+ const firstForbiddenField = uniqueForbiddenFields[0]
337
+ return {
338
+ success: false,
339
+ response: createForbiddenFieldResponse(c, firstForbiddenField!),
340
+ }
341
+ }
342
+
343
+ return { success: true }
344
+ }
345
+
346
+ /**
347
+ * Check if all fields were stripped from records (user tried to write only protected fields)
348
+ */
349
+ function checkAllFieldsStripped(config: {
350
+ readonly c: Context
351
+ readonly app: App
352
+ readonly tableName: string
353
+ readonly userRole: string
354
+ readonly records: readonly { fields: Record<string, unknown> }[]
355
+ readonly strippedRecords: ReadonlyArray<{ fields: Record<string, unknown> }>
356
+ }): { success: true } | { success: false; response: Response } {
357
+ const { c, app, tableName, userRole, records, strippedRecords } = config
358
+
359
+ const hasWritableFields = strippedRecords.some((record) => Object.keys(record.fields).length > 0)
360
+ if (hasWritableFields) {
361
+ return { success: true }
362
+ }
363
+
364
+ // All fields were stripped
365
+ const allForbiddenFields = records
366
+ .map((record) => validateFieldWritePermissions(app, tableName, userRole, record.fields))
367
+ .filter((fields) => fields.length > 0)
368
+ const uniqueForbiddenFields = [...new Set(allForbiddenFields.flat())]
369
+ const firstForbiddenField = uniqueForbiddenFields[0]
370
+
371
+ return {
372
+ success: false,
373
+ response: createForbiddenFieldResponse(c, firstForbiddenField!),
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Check required field validation
379
+ */
380
+ async function checkRequiredFields(
381
+ table: NonNullable<App['tables']>[number] | undefined,
382
+ strippedRecords: ReadonlyArray<{ fields: Record<string, unknown> }>,
383
+ c: Context
384
+ ): Promise<{ success: true } | { success: false; response: Response }> {
385
+ const validationErrors = await validateUpsertRequiredFields(table, strippedRecords)
386
+ if (validationErrors.length === 0) {
387
+ return { success: true }
388
+ }
389
+
390
+ return {
391
+ success: false,
392
+ response: c.json(
393
+ {
394
+ success: false,
395
+ message: 'Validation failed: one or more records have invalid data',
396
+ code: 'VALIDATION_ERROR',
397
+ details: validationErrors,
398
+ },
399
+ 400
400
+ ),
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Validate upsert request (permissions and required fields)
406
+ *
407
+ * Upsert behavior for protected fields:
408
+ * - Single-record upserts: Reject with 403 if ANY protected fields present
409
+ * - Multi-record upserts (batch): Strip protected fields, succeed if any writable fields remain
410
+ * - Filter protected fields from response
411
+ */
412
+ export async function validateUpsertRequest(config: {
413
+ readonly c: Context
414
+ readonly app: App
415
+ readonly tableName: string
416
+ readonly userRole: string
417
+ readonly records: readonly { fields: Record<string, unknown> }[]
418
+ readonly fieldsToMergeOn: readonly string[]
419
+ }) {
420
+ const { c, app, tableName, userRole, records, fieldsToMergeOn } = config
421
+ const table = app.tables?.find((t) => t.name === tableName)
422
+
423
+ // Single-record upsert: reject if ANY protected fields present
424
+ const singleRecordCheck = checkSingleRecordProtectedFields({
425
+ c,
426
+ app,
427
+ tableName,
428
+ userRole,
429
+ records,
430
+ })
431
+ if (!singleRecordCheck.success) {
432
+ return singleRecordCheck
433
+ }
434
+
435
+ // For multi-record upserts, strip unwritable fields
436
+ const strippedRecords = stripUnwritableFields(app, tableName, userRole, records)
437
+
438
+ // Check if all fields were stripped
439
+ const stripCheck = checkAllFieldsStripped({
440
+ c,
441
+ app,
442
+ tableName,
443
+ userRole,
444
+ records,
445
+ strippedRecords,
446
+ })
447
+ if (!stripCheck.success) {
448
+ return stripCheck
449
+ }
450
+
451
+ // Check table-level permissions (create/update)
452
+ const permissionCheck = await checkUpsertPermissionsWithUpdateCheck({
453
+ app,
454
+ tableName,
455
+ userRole,
456
+ records: strippedRecords,
457
+ fieldsToMergeOn,
458
+ c,
459
+ })
460
+ if (!permissionCheck.allowed) {
461
+ return { success: false as const, response: permissionCheck.response }
462
+ }
463
+
464
+ // Validate required fields
465
+ const requiredCheck = await checkRequiredFields(table, strippedRecords, c)
466
+ if (!requiredCheck.success) {
467
+ return { success: false as const, response: requiredCheck.response }
468
+ }
469
+
470
+ return { success: true as const, strippedRecords }
471
+ }
@@ -0,0 +1,159 @@
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 { getSessionContext } from '@/presentation/api/utils/context-helpers'
9
+ import type { Session } from '@/application/ports/models/user-session'
10
+ import type { App } from '@/domain/models/app'
11
+ import type { Context } from 'hono'
12
+
13
+ // ============================================================================
14
+ // Type Re-exports
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Re-export Session type for sibling files in this directory
19
+ */
20
+ export type { Session }
21
+
22
+ // ============================================================================
23
+ // Constants
24
+ // ============================================================================
25
+
26
+ const AUTH_KEYWORDS = ['not found', 'access denied'] as const
27
+
28
+ // ============================================================================
29
+ // Table ID Resolution
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Get table name from tableId parameter
34
+ *
35
+ * Looks up the table in the app schema by either:
36
+ * - Table ID (numeric or string match)
37
+ * - Table name (exact match)
38
+ *
39
+ * @param app - Application configuration containing tables
40
+ * @param tableId - Table identifier from route parameter
41
+ * @returns Table name if found, undefined otherwise
42
+ */
43
+ export const getTableNameFromId = (app: App, tableId: string): string | undefined => {
44
+ const table = app.tables?.find((t) => String(t.id) === tableId || t.name === tableId)
45
+ return table?.name
46
+ }
47
+
48
+ /**
49
+ * Check if a string contains authorization-related keywords
50
+ */
51
+ const containsAuthKeywords = (text: string): boolean =>
52
+ AUTH_KEYWORDS.some((keyword) => text.includes(keyword))
53
+
54
+ /**
55
+ * Extract error details from an error object
56
+ * Centralizes error information extraction logic
57
+ */
58
+ const extractErrorDetails = (
59
+ error: unknown
60
+ ): { message: string; name: string; causeMessage: string; errorString: string } => {
61
+ const errorMessage = error instanceof Error ? error.message : ''
62
+ const errorName = error instanceof Error ? error.name : ''
63
+ const errorString = String(error)
64
+ const causeMessage =
65
+ error instanceof Error && 'cause' in error && error.cause instanceof Error
66
+ ? error.cause.message
67
+ : ''
68
+
69
+ return { message: errorMessage, name: errorName, causeMessage, errorString }
70
+ }
71
+
72
+ /**
73
+ * Check if error is an authorization error (SessionContextError or access denied)
74
+ *
75
+ * @param error - The error to check
76
+ * @returns true if error is authorization-related (should return 404 instead of 500)
77
+ */
78
+ export const isAuthorizationError = (error: unknown): boolean => {
79
+ const { message, name, causeMessage, errorString } = extractErrorDetails(error)
80
+
81
+ return (
82
+ containsAuthKeywords(message) ||
83
+ containsAuthKeywords(causeMessage) ||
84
+ name.includes('SessionContextError') ||
85
+ errorString.includes('SessionContextError')
86
+ )
87
+ }
88
+
89
+ /**
90
+ * Handle batch restore errors with appropriate HTTP responses
91
+ */
92
+ export const handleBatchRestoreError = (c: Context, error: unknown) => {
93
+ // Handle ForbiddenError (viewer role attempting write operation)
94
+ // Use name check to handle multiple import paths resolving to different class instances
95
+ if (error instanceof Error && error.name === 'ForbiddenError') {
96
+ return c.json(
97
+ {
98
+ error: 'Forbidden',
99
+ message: error.message,
100
+ },
101
+ 403
102
+ )
103
+ }
104
+
105
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
106
+
107
+ // Handle "Record X not found" errors (404)
108
+ if (errorMessage.includes('not found')) {
109
+ const recordIdMatch = errorMessage.match(/Record (\S+) not found/)
110
+ const recordId = recordIdMatch?.[1] ? Number.parseInt(recordIdMatch[1]) : undefined
111
+ return c.json(
112
+ {
113
+ success: false,
114
+ message: 'Resource not found',
115
+ code: 'NOT_FOUND',
116
+ recordId,
117
+ },
118
+ 404
119
+ )
120
+ }
121
+
122
+ // Handle "Record X is not deleted" errors (400)
123
+ if (errorMessage.includes('is not deleted')) {
124
+ const recordIdMatch = errorMessage.match(/Record (\S+) is not deleted/)
125
+ const recordId = recordIdMatch?.[1] ? Number.parseInt(recordIdMatch[1]) : undefined
126
+ return c.json(
127
+ {
128
+ error: 'Bad Request',
129
+ message: 'Record is not deleted',
130
+ recordId,
131
+ },
132
+ 400
133
+ )
134
+ }
135
+
136
+ return c.json({ error: 'Internal server error', message: errorMessage }, 500)
137
+ }
138
+
139
+ // ============================================================================
140
+ // Validation Helper Functions
141
+ // ============================================================================
142
+
143
+ /**
144
+ * Extract session from Hono context
145
+ * Returns undefined if no session exists
146
+ *
147
+ * @deprecated Use getSessionContext from @/presentation/api/utils/context-helpers instead
148
+ */
149
+ export const getSessionFromContext = (c: Context): Readonly<Session> | undefined => {
150
+ return getSessionContext(c)
151
+ }
152
+
153
+ /**
154
+ * Validate table exists and return table name
155
+ * Returns undefined if table not found
156
+ */
157
+ export const validateAndGetTableName = (app: App, tableId: string): string | undefined => {
158
+ return getTableNameFromId(app, tableId)
159
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Copyright (c) 2025 ESSENTIAL SERVICES
3
+ *
4
+ * This source code is licensed under the Business Source License 1.1
5
+ * found in the LICENSE.md file in the root directory of this source tree.
6
+ */
7
+
8
+ import { Effect } from 'effect'
9
+ import {
10
+ listViewsProgram,
11
+ getViewProgram,
12
+ getViewRecordsProgram,
13
+ } from '@/application/use-cases/tables/programs'
14
+ import { getViewResponseSchema, getViewRecordsResponseSchema } from '@/domain/models/api/tables'
15
+ import { runEffect } from '@/presentation/api/utils'
16
+ import { getTableContext } from '@/presentation/api/utils/context-helpers'
17
+ import { provideTableLive } from './effect-runner'
18
+ import type { App } from '@/domain/models/app'
19
+ import type { Hono } from 'hono'
20
+
21
+ export function chainViewRoutesMethods<T extends Hono>(honoApp: T, app: App) {
22
+ return honoApp
23
+ .get('/api/tables/:tableId/views', async (c) => {
24
+ // Session, tableId, and userRole are guaranteed by middleware chain
25
+ const { tableId, userRole } = getTableContext(c)
26
+
27
+ const program = Effect.gen(function* () {
28
+ const result = yield* listViewsProgram(tableId, app, userRole)
29
+ // Return the views array directly (unwrapped) to match test expectations
30
+ // No schema validation - test expects minimal view objects without timestamps
31
+ return result
32
+ })
33
+
34
+ return runEffect(c, program)
35
+ })
36
+ .get('/api/tables/:tableId/views/:viewId', async (c) =>
37
+ runEffect(
38
+ c,
39
+ getViewProgram(c.req.param('tableId'), c.req.param('viewId'), app),
40
+ getViewResponseSchema
41
+ )
42
+ )
43
+ .get('/api/tables/:tableId/views/:viewId/records', async (c) => {
44
+ const { session, tableId, userRole } = getTableContext(c)
45
+ const viewId = c.req.param('viewId')
46
+
47
+ const program = getViewRecordsProgram({ tableId, viewId, app, userRole, session })
48
+
49
+ return runEffect(c, provideTableLive(program), getViewRecordsResponseSchema)
50
+ })
51
+ }
@@ -0,0 +1,9 @@
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 from modular structure
9
+ export { chainTableRoutes } from './tables/'