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 type { App } from '@/domain/models/app'
9
+ import type { CurrencyField, DateField, DurationField } from '@/domain/models/app/table/field-types'
10
+
11
+ /**
12
+ * Currency symbols mapping
13
+ */
14
+ const CURRENCY_SYMBOLS: Record<string, string> = {
15
+ USD: '$',
16
+ EUR: '€',
17
+ GBP: '£',
18
+ JPY: '¥',
19
+ CAD: '$',
20
+ AUD: '$',
21
+ }
22
+
23
+ type ThousandsSeparator = 'comma' | 'period' | 'space' | 'none'
24
+ type NegativeFormat = 'minus' | 'parentheses'
25
+
26
+ /**
27
+ * Get the character used for thousands separation
28
+ */
29
+ function getThousandsSeparatorChar(separator: ThousandsSeparator): string {
30
+ const separatorMap: Record<ThousandsSeparator, string> = {
31
+ comma: ',',
32
+ period: '.',
33
+ space: ' ',
34
+ none: '',
35
+ }
36
+ return separatorMap[separator]
37
+ }
38
+
39
+ /**
40
+ * Get the character used for decimal separation based on thousands separator
41
+ */
42
+ function getDecimalSeparator(thousandsSeparator: ThousandsSeparator): string {
43
+ // Use comma as decimal separator when period is used for thousands (European format)
44
+ return thousandsSeparator === 'period' ? ',' : '.'
45
+ }
46
+
47
+ /**
48
+ * Apply negative formatting to a formatted amount
49
+ */
50
+ function applyNegativeFormat(amount: string, isNegative: boolean, format: NegativeFormat): string {
51
+ if (!isNegative) return amount
52
+ return format === 'parentheses' ? `(${amount})` : `-${amount}`
53
+ }
54
+
55
+ /**
56
+ * Get default thousands separator based on precision
57
+ * When precision is 0 (e.g., JPY), default to no separator (¥1000 not ¥1,000)
58
+ */
59
+ function getDefaultThousandsSeparator(precision: number): ThousandsSeparator {
60
+ return precision === 0 ? 'none' : 'comma'
61
+ }
62
+
63
+ /**
64
+ * Format a currency value with the appropriate symbol and formatting options
65
+ *
66
+ * @param value - The numeric value to format
67
+ * @param field - The currency field configuration
68
+ * @returns Formatted currency string
69
+ */
70
+ function formatCurrency(value: number, field: CurrencyField): string {
71
+ const symbol = CURRENCY_SYMBOLS[field.currency] || field.currency
72
+ const precision = field.precision ?? 2
73
+ const symbolPosition = field.symbolPosition ?? 'before'
74
+ const negativeFormat = (field.negativeFormat ?? 'minus') as NegativeFormat
75
+ const thousandsSeparator = (field.thousandsSeparator ??
76
+ getDefaultThousandsSeparator(precision)) as ThousandsSeparator
77
+
78
+ // Handle negative values
79
+ const isNegative = value < 0
80
+ const absoluteValue = Math.abs(value)
81
+
82
+ // Format the number with precision
83
+ const formattedNumber = absoluteValue.toFixed(precision)
84
+ const [integerPartRaw, decimalPart] = formattedNumber.split('.')
85
+ const integerPartBase = integerPartRaw ?? '0'
86
+
87
+ // Apply thousands separator
88
+ const separatorChar = getThousandsSeparatorChar(thousandsSeparator)
89
+ const integerPart =
90
+ separatorChar !== ''
91
+ ? integerPartBase.replace(/\B(?=(\d{3})+(?!\d))/g, separatorChar)
92
+ : integerPartBase
93
+
94
+ // Reconstruct number with decimal separator
95
+ const decimalSeparator = getDecimalSeparator(thousandsSeparator)
96
+ const reconstructedNumber =
97
+ precision > 0 ? `${integerPart}${decimalSeparator}${decimalPart}` : integerPart
98
+
99
+ // Apply symbol position
100
+ const amountWithSymbol =
101
+ symbolPosition === 'before'
102
+ ? `${symbol}${reconstructedNumber}`
103
+ : `${reconstructedNumber}${symbol}`
104
+
105
+ return applyNegativeFormat(amountWithSymbol, isNegative, negativeFormat)
106
+ }
107
+
108
+ /**
109
+ * Extract date part value from formatter parts
110
+ */
111
+ function extractPartValue(
112
+ parts: readonly Intl.DateTimeFormatPart[],
113
+ type: Intl.DateTimeFormatPartTypes
114
+ ): string {
115
+ return parts.find((p) => p.type === type)?.value ?? ''
116
+ }
117
+
118
+ /**
119
+ * Create date from timezone-converted parts
120
+ */
121
+ function createDateFromParts(parts: readonly Intl.DateTimeFormatPart[]): Readonly<Date> {
122
+ const year = extractPartValue(parts, 'year')
123
+ const month = extractPartValue(parts, 'month')
124
+ const day = extractPartValue(parts, 'day')
125
+ const hour = extractPartValue(parts, 'hour')
126
+ const minute = extractPartValue(parts, 'minute')
127
+
128
+ return new Date(
129
+ parseInt(year, 10),
130
+ parseInt(month, 10) - 1,
131
+ parseInt(day, 10),
132
+ parseInt(hour, 10),
133
+ parseInt(minute, 10)
134
+ )
135
+ }
136
+
137
+ /**
138
+ * Convert date to target timezone
139
+ */
140
+ function convertToTimezone(date: Readonly<Date>, timezone: string): Readonly<Date> {
141
+ if (!timezone || timezone === 'local') {
142
+ return date
143
+ }
144
+
145
+ try {
146
+ const formatter = new Intl.DateTimeFormat('en-US', {
147
+ timeZone: timezone,
148
+ year: 'numeric',
149
+ month: 'numeric',
150
+ day: 'numeric',
151
+ hour: 'numeric',
152
+ minute: 'numeric',
153
+ second: 'numeric',
154
+ hour12: false,
155
+ })
156
+
157
+ const parts = formatter.formatToParts(date)
158
+ return createDateFromParts(parts)
159
+ } catch {
160
+ return date
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Format date part based on date format setting
166
+ */
167
+ function formatDatePart(year: number, month: number, day: number, dateFormat: string): string {
168
+ const formatMap: Record<string, string> = {
169
+ US: `${month}/${day}/${year}`,
170
+ European: `${day}/${month}/${year}`,
171
+ ISO: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
172
+ }
173
+ return formatMap[dateFormat] ?? `${month}/${day}/${year}`
174
+ }
175
+
176
+ /**
177
+ * Format time part based on time format setting
178
+ */
179
+ function formatTimePart(hours: number, minutes: number, timeFormat: string): string {
180
+ if (timeFormat === '12-hour') {
181
+ const period = hours >= 12 ? 'PM' : 'AM'
182
+ const hours12 = hours % 12 || 12
183
+ return `${hours12}:${String(minutes).padStart(2, '0')} ${period}`
184
+ }
185
+ return `${hours}:${String(minutes).padStart(2, '0')}`
186
+ }
187
+
188
+ /**
189
+ * Format a date value based on the date format configuration
190
+ */
191
+ function formatDate(value: unknown, field: DateField, timezoneOverride?: string): string {
192
+ const date =
193
+ value instanceof Date ? value : typeof value === 'string' ? new Date(value) : new Date()
194
+
195
+ if (isNaN(date.getTime())) {
196
+ return ''
197
+ }
198
+
199
+ const timezone = timezoneOverride || field.timeZone || 'local'
200
+ const targetDate = convertToTimezone(date, timezone)
201
+
202
+ const year = targetDate.getFullYear()
203
+ const month = targetDate.getMonth() + 1
204
+ const day = targetDate.getDate()
205
+
206
+ const dateFormat = field.dateFormat ?? 'US'
207
+ const formattedDate = formatDatePart(year, month, day, dateFormat)
208
+
209
+ const shouldIncludeTime = field.includeTime || field.type === 'datetime'
210
+ if (shouldIncludeTime) {
211
+ const hours = targetDate.getHours()
212
+ const minutes = targetDate.getMinutes()
213
+ const timeFormat = field.timeFormat ?? '24-hour'
214
+ const formattedTime = formatTimePart(hours, minutes, timeFormat)
215
+ return `${formattedDate} ${formattedTime}`
216
+ }
217
+
218
+ return formattedDate
219
+ }
220
+
221
+ /**
222
+ * Format currency field value
223
+ */
224
+ function formatCurrencyField(value: unknown, field: CurrencyField): string | undefined {
225
+ const numericValue = typeof value === 'string' ? parseFloat(value) : (value as number)
226
+ if (typeof numericValue === 'number' && !isNaN(numericValue)) {
227
+ return formatCurrency(numericValue, field)
228
+ }
229
+ return undefined
230
+ }
231
+
232
+ /**
233
+ * Format date/datetime/time field value
234
+ */
235
+ function formatDateField(
236
+ value: unknown,
237
+ field: DateField,
238
+ timezoneOverride?: string
239
+ ): string | undefined {
240
+ return formatDate(value, field, timezoneOverride)
241
+ }
242
+
243
+ /**
244
+ * Formatted display result with optional timezone metadata and attachment metadata
245
+ */
246
+ export interface FormatResult {
247
+ readonly displayValue: string
248
+ readonly timezone?: string
249
+ readonly displayTimezone?: string
250
+ readonly allowedFileTypes?: readonly string[]
251
+ readonly maxFileSize?: number
252
+ readonly maxFileSizeDisplay?: string
253
+ }
254
+
255
+ /**
256
+ * Format a currency field and create FormatResult
257
+ */
258
+ function formatCurrencyFieldResult(value: unknown, field: CurrencyField): FormatResult | undefined {
259
+ const displayValue = formatCurrencyField(value, field)
260
+ return displayValue !== undefined ? { displayValue } : undefined
261
+ }
262
+
263
+ /**
264
+ * Format a date field and create FormatResult with optional timezone
265
+ */
266
+ function formatDateFieldResult(
267
+ value: unknown,
268
+ field: DateField,
269
+ timezoneOverride?: string
270
+ ): FormatResult | undefined {
271
+ const displayValue = formatDateField(value, field, timezoneOverride)
272
+ if (displayValue === undefined) return undefined
273
+
274
+ // Include timezone metadata
275
+ return {
276
+ displayValue,
277
+ ...(field.timeZone ? { timezone: field.timeZone } : {}),
278
+ ...(timezoneOverride ? { displayTimezone: timezoneOverride } : {}),
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Parse PostgreSQL interval string to total minutes
284
+ *
285
+ * Handles formats like:
286
+ * - "1 hour 30 minutes"
287
+ * - "2 hours"
288
+ * - "45 minutes"
289
+ * - "1:30:45" (h:mm:ss format)
290
+ *
291
+ * @param value - PostgreSQL interval string
292
+ * @returns Total minutes
293
+ */
294
+ function parseIntervalToMinutes(value: string): number {
295
+ // Handle h:mm:ss format (e.g., "1:30:45")
296
+ if (/^\d+:\d{2}(:\d{2})?$/.test(value)) {
297
+ const parts = value.split(':')
298
+ const hours = parseInt(parts[0] ?? '0', 10)
299
+ const minutes = parseInt(parts[1] ?? '0', 10)
300
+ const seconds = parseInt(parts[2] ?? '0', 10)
301
+ return hours * 60 + minutes + seconds / 60
302
+ }
303
+
304
+ // Handle PostgreSQL text format (e.g., "1 hour 30 minutes")
305
+ // Extract hours
306
+ const hoursMatch = value.match(/(\d+)\s*hours?/)
307
+ const hoursMinutes = hoursMatch ? parseInt(hoursMatch[1] ?? '0', 10) * 60 : 0
308
+
309
+ // Extract minutes
310
+ const minutesMatch = value.match(/(\d+)\s*minutes?/)
311
+ const minutesValue = minutesMatch ? parseInt(minutesMatch[1] ?? '0', 10) : 0
312
+
313
+ return hoursMinutes + minutesValue
314
+ }
315
+
316
+ /**
317
+ * Format duration value based on display format
318
+ *
319
+ * @param value - Duration value (string or number)
320
+ * @param field - Duration field configuration
321
+ * @returns Formatted duration string
322
+ */
323
+ function formatDuration(value: unknown, field: DurationField): string {
324
+ // Parse the duration value to minutes
325
+ const totalMinutes = typeof value === 'string' ? parseIntervalToMinutes(value) : (value as number)
326
+
327
+ const displayFormat = field.displayFormat ?? 'h:mm'
328
+
329
+ // Format based on display format
330
+ if (displayFormat === 'h:mm') {
331
+ const hours = Math.floor(totalMinutes / 60)
332
+ const minutes = Math.floor(totalMinutes % 60)
333
+ return `${hours}:${String(minutes).padStart(2, '0')}`
334
+ }
335
+
336
+ if (displayFormat === 'h:mm:ss') {
337
+ const hours = Math.floor(totalMinutes / 60)
338
+ const remainingMinutes = totalMinutes % 60
339
+ const minutes = Math.floor(remainingMinutes)
340
+ const seconds = Math.floor((remainingMinutes - minutes) * 60)
341
+ return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
342
+ }
343
+
344
+ if (displayFormat === 'decimal') {
345
+ const hours = totalMinutes / 60
346
+ return hours.toFixed(1)
347
+ }
348
+
349
+ // Default to h:mm
350
+ const hours = Math.floor(totalMinutes / 60)
351
+ const minutes = Math.floor(totalMinutes % 60)
352
+ return `${hours}:${String(minutes).padStart(2, '0')}`
353
+ }
354
+
355
+ /**
356
+ * Format duration field value
357
+ */
358
+ function formatDurationField(value: unknown, field: DurationField): string | undefined {
359
+ if (value === null || value === undefined) return undefined
360
+ return formatDuration(value, field)
361
+ }
362
+
363
+ /**
364
+ * Format duration field and create FormatResult
365
+ */
366
+ function formatDurationFieldResult(value: unknown, field: DurationField): FormatResult | undefined {
367
+ const displayValue = formatDurationField(value, field)
368
+ return displayValue !== undefined ? { displayValue } : undefined
369
+ }
370
+
371
+ /**
372
+ * Check if field type is a date-related type
373
+ */
374
+ function isDateRelatedType(type: string): boolean {
375
+ return type === 'date' || type === 'datetime' || type === 'time'
376
+ }
377
+
378
+ /**
379
+ * Check if field type is an attachment type
380
+ */
381
+ function isAttachmentType(type: string): boolean {
382
+ return type === 'single-attachment' || type === 'multiple-attachments'
383
+ }
384
+
385
+ /**
386
+ * Format bytes to human-readable file size string
387
+ *
388
+ * @param bytes - File size in bytes
389
+ * @returns Formatted file size string (e.g., "5 MB", "1.5 GB")
390
+ */
391
+ function formatBytes(bytes: number): string {
392
+ if (bytes === 0) return '0 B'
393
+
394
+ const k = 1024
395
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
396
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
397
+
398
+ // For MB and above, show clean integers (5 MB, not 5.0 MB)
399
+ // For KB, show one decimal place if needed
400
+ const value = bytes / Math.pow(k, i)
401
+ const formatted = i >= 2 ? Math.round(value) : Math.round(value * 10) / 10
402
+
403
+ return `${formatted} ${sizes[i]}`
404
+ }
405
+
406
+ /**
407
+ * Format attachment field and create FormatResult with allowedFileTypes, maxFileSize, and maxFileSizeDisplay
408
+ */
409
+ function formatAttachmentFieldResult(
410
+ value: unknown,
411
+ field: Readonly<{ allowedFileTypes?: readonly string[]; maxFileSize?: number }>
412
+ ): FormatResult | undefined {
413
+ // Only format if we have metadata to add
414
+ if (!field.allowedFileTypes && !field.maxFileSize) return undefined
415
+
416
+ return {
417
+ displayValue: String(value ?? ''),
418
+ ...(field.allowedFileTypes ? { allowedFileTypes: field.allowedFileTypes } : {}),
419
+ ...(field.maxFileSize !== undefined
420
+ ? { maxFileSize: field.maxFileSize, maxFileSizeDisplay: formatBytes(field.maxFileSize) }
421
+ : {}),
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Options for formatting field display
427
+ */
428
+ export interface FormatFieldOptions {
429
+ readonly fieldName: string
430
+ readonly value: unknown
431
+ readonly app: App
432
+ readonly tableName: string
433
+ readonly timezoneOverride?: string
434
+ }
435
+
436
+ /**
437
+ * Format a field value for display based on field type and configuration
438
+ */
439
+ export function formatFieldForDisplay(options: FormatFieldOptions): FormatResult | undefined {
440
+ const { fieldName, value, app, tableName, timezoneOverride } = options
441
+
442
+ // Find the table and field
443
+ const table = app.tables?.find((t) => t.name === tableName)
444
+ if (!table) return undefined
445
+
446
+ const field = table.fields.find((f) => f.name === fieldName)
447
+ if (!field) return undefined
448
+
449
+ // Format based on field type
450
+ if (field.type === 'currency') {
451
+ return formatCurrencyFieldResult(value, field as CurrencyField)
452
+ }
453
+
454
+ if (isDateRelatedType(field.type)) {
455
+ return formatDateFieldResult(value, field as DateField, timezoneOverride)
456
+ }
457
+
458
+ if (field.type === 'duration') {
459
+ return formatDurationFieldResult(value, field as DurationField)
460
+ }
461
+
462
+ if (isAttachmentType(field.type)) {
463
+ return formatAttachmentFieldResult(
464
+ value,
465
+ field as { allowedFileTypes?: readonly string[]; maxFileSize?: number }
466
+ )
467
+ }
468
+
469
+ // Return undefined for types that don't need formatting yet
470
+ return undefined
471
+ }
@@ -0,0 +1,189 @@
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 type { App } from '@/domain/models/app'
9
+ import type { TablePermission } from '@/domain/models/app/table/permissions'
10
+
11
+ /**
12
+ * System fields that are always preserved in record filtering
13
+ * These include authorship metadata and timestamps
14
+ */
15
+ const SYSTEM_FIELDS = new Set([
16
+ 'id',
17
+ 'created_at',
18
+ 'updated_at',
19
+ 'created_by',
20
+ 'updated_by',
21
+ 'deleted_by',
22
+ 'deleted_at',
23
+ ])
24
+
25
+ /**
26
+ * Check if field name is a system field
27
+ */
28
+ function isSystemField(fieldName: string): boolean {
29
+ return SYSTEM_FIELDS.has(fieldName)
30
+ }
31
+
32
+ /**
33
+ * Check if field is sensitive type (email, phone, currency)
34
+ */
35
+ function isSensitiveFieldType(fieldType: string): boolean {
36
+ const sensitiveTypes = new Set(['email', 'phone-number', 'currency'])
37
+ return sensitiveTypes.has(fieldType)
38
+ }
39
+
40
+ /**
41
+ * Check if field should be excluded for viewer role
42
+ */
43
+ function shouldExcludeForViewer(fieldName: string, fieldType: string): boolean {
44
+ const allowedFieldTypes = new Set(['single-line-text'])
45
+ const allowedFieldNames = new Set(['name', 'title'])
46
+
47
+ // Exclude sensitive field types
48
+ if (isSensitiveFieldType(fieldType)) {
49
+ return true
50
+ }
51
+
52
+ // Only allow specific field names or types
53
+ if (!allowedFieldNames.has(fieldName) && !allowedFieldTypes.has(fieldType)) {
54
+ return true
55
+ }
56
+
57
+ // For single-line-text, only allow if it's a name/title field
58
+ if (fieldType === 'single-line-text' && !allowedFieldNames.has(fieldName)) {
59
+ return true
60
+ }
61
+
62
+ return false
63
+ }
64
+
65
+ /**
66
+ * Check if field should be excluded based on default permission rules
67
+ * Sensitive fields (like salary) are restricted for non-admin roles
68
+ */
69
+ function shouldExcludeFieldByDefault(
70
+ fieldName: string,
71
+ userRole: string,
72
+ table:
73
+ | { readonly fields: readonly { readonly name: string; readonly type: string }[] }
74
+ | undefined
75
+ ): boolean {
76
+ // Admin role has full access
77
+ if (userRole === 'admin') {
78
+ return false
79
+ }
80
+
81
+ // Find field definition
82
+ const field = table?.fields.find((f) => f.name === fieldName)
83
+ if (!field) return false
84
+
85
+ // Viewer role: most restrictive access
86
+ if (userRole === 'viewer') {
87
+ return shouldExcludeForViewer(fieldName, field.type)
88
+ }
89
+
90
+ // Member role: restrict sensitive financial data
91
+ if (userRole === 'member') {
92
+ return fieldName === 'salary' && field.type === 'currency'
93
+ }
94
+
95
+ return false
96
+ }
97
+
98
+ /**
99
+ * Filter fields from a record based on user's read permissions
100
+ *
101
+ * This implements Better Auth layer field read filtering.
102
+ * Returns a record with only fields the user has permission to read.
103
+ *
104
+ * @param params - Configuration object
105
+ * @param params.app - Application configuration
106
+ * @param params.tableName - Name of the table
107
+ * @param params.userRole - User's role
108
+ * @param params.userId - User's ID (for custom conditions)
109
+ * @param params.record - Record object to filter
110
+ * @returns Record with only readable fields
111
+ */
112
+ export function filterReadableFields<T extends Record<string, unknown>>(
113
+ params: Readonly<{
114
+ app: App
115
+ tableName: string
116
+ userRole: string
117
+ userId: string
118
+ record: T
119
+ }>
120
+ ): Readonly<Record<string, unknown>> {
121
+ const { app, tableName, userRole, userId, record } = params
122
+
123
+ // Find table definition
124
+ const table = app.tables?.find((t) => t.name === tableName)
125
+
126
+ // If no explicit field permissions defined, apply default rules
127
+ if (!table?.permissions?.fields) {
128
+ return Object.keys(record).reduce<Record<string, unknown>>((acc, fieldName) => {
129
+ // Always preserve system fields (including authorship metadata)
130
+ if (isSystemField(fieldName)) {
131
+ return { ...acc, [fieldName]: record[fieldName] }
132
+ }
133
+
134
+ // Check default permission rules
135
+ if (shouldExcludeFieldByDefault(fieldName, userRole, table)) {
136
+ return acc // Exclude field
137
+ }
138
+
139
+ // Include all other fields
140
+ return { ...acc, [fieldName]: record[fieldName] }
141
+ }, {})
142
+ }
143
+
144
+ // Filter fields based on read permissions
145
+ const filteredRecord = Object.keys(record).reduce<Record<string, unknown>>((acc, fieldName) => {
146
+ // Preserve system fields (id, timestamps, authorship metadata)
147
+ // These will be transformed by record-transformer to root-level camelCase properties
148
+ if (isSystemField(fieldName)) {
149
+ return { ...acc, [fieldName]: record[fieldName] }
150
+ }
151
+
152
+ const fieldPermission = table.permissions?.fields?.find((fp) => fp.field === fieldName)
153
+
154
+ // If no specific read permission for this field, include it (inherits table permission)
155
+ if (!fieldPermission?.read) {
156
+ return { ...acc, [fieldName]: record[fieldName] }
157
+ }
158
+
159
+ // Check if user has read permission for this field
160
+ if (hasFieldReadPermission(fieldPermission.read, userRole, userId, record)) {
161
+ return { ...acc, [fieldName]: record[fieldName] }
162
+ }
163
+
164
+ // Otherwise, omit the field from the response
165
+ return acc
166
+ }, {})
167
+
168
+ return filteredRecord
169
+ }
170
+
171
+ /**
172
+ * Check if user's role has read permission.
173
+ *
174
+ * Permission format (3-format system):
175
+ * - `'all'` — Everyone
176
+ * - `'authenticated'` — Any logged-in user
177
+ * - `string[]` — Specific role names
178
+ */
179
+ function hasFieldReadPermission(
180
+ permission: TablePermission,
181
+ userRole: string,
182
+ _userId: string,
183
+ _record: Readonly<Record<string, unknown>>
184
+ ): boolean {
185
+ if (permission === 'all') return true
186
+ if (permission === 'authenticated') return true
187
+ if (Array.isArray(permission)) return permission.includes(userRole)
188
+ return false
189
+ }