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,440 @@
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
+ /**
9
+ * Reserved SQL keywords that require escaping when used in identifiers
10
+ * Based on PostgreSQL reserved keywords list
11
+ * @see https://www.postgresql.org/docs/current/sql-keywords-appendix.html
12
+ */
13
+ const SQL_RESERVED_KEYWORDS = new Set([
14
+ 'select',
15
+ 'insert',
16
+ 'update',
17
+ 'delete',
18
+ 'from',
19
+ 'where',
20
+ 'join',
21
+ 'inner',
22
+ 'outer',
23
+ 'left',
24
+ 'right',
25
+ 'full',
26
+ 'cross',
27
+ 'on',
28
+ 'as',
29
+ 'table',
30
+ 'create',
31
+ 'alter',
32
+ 'drop',
33
+ 'truncate',
34
+ 'add',
35
+ 'column',
36
+ 'constraint',
37
+ 'primary',
38
+ 'foreign',
39
+ 'key',
40
+ 'references',
41
+ 'unique',
42
+ 'index',
43
+ 'view',
44
+ 'database',
45
+ 'schema',
46
+ 'grant',
47
+ 'revoke',
48
+ 'transaction',
49
+ 'commit',
50
+ 'rollback',
51
+ 'union',
52
+ 'intersect',
53
+ 'except',
54
+ 'group',
55
+ 'having',
56
+ 'order',
57
+ 'limit',
58
+ 'offset',
59
+ 'distinct',
60
+ 'all',
61
+ 'any',
62
+ 'some',
63
+ 'exists',
64
+ 'in',
65
+ 'between',
66
+ 'like',
67
+ 'ilike',
68
+ 'and',
69
+ 'or',
70
+ 'not',
71
+ 'null',
72
+ 'is',
73
+ 'true',
74
+ 'false',
75
+ 'case',
76
+ 'when',
77
+ 'then',
78
+ 'else',
79
+ 'end',
80
+ 'cast',
81
+ 'default',
82
+ 'check',
83
+ 'user',
84
+ 'current_user',
85
+ 'session_user',
86
+ 'current_date',
87
+ 'current_time',
88
+ 'current_timestamp',
89
+ ])
90
+
91
+ /**
92
+ * Check if an identifier needs escaping due to reserved words
93
+ * Split identifier by underscores and check if any token is a reserved word
94
+ * Examples:
95
+ * - "order" → needs escaping (is reserved word)
96
+ * - "order_num" → needs escaping (token "order" is reserved)
97
+ * - "created_at" → no escaping ("created" and "at" on their own are not problematic)
98
+ * - "user_id" → needs escaping (token "user" is reserved)
99
+ * - "select" → needs escaping (is reserved word)
100
+ */
101
+ const containsReservedWord = (identifier: string): boolean => {
102
+ const lowerIdentifier = identifier.toLowerCase()
103
+ // Check if the identifier itself is a reserved word
104
+ if (SQL_RESERVED_KEYWORDS.has(lowerIdentifier)) {
105
+ return true
106
+ }
107
+ // Split by underscores and check each token
108
+ const tokens = lowerIdentifier.split('_')
109
+ return tokens.some((token) => SQL_RESERVED_KEYWORDS.has(token))
110
+ }
111
+
112
+ /**
113
+ * Escape a field name for use in SQL if it contains reserved words
114
+ * PostgreSQL uses double quotes for identifier escaping
115
+ */
116
+ const escapeFieldName = (fieldName: string): string =>
117
+ containsReservedWord(fieldName) ? `"${fieldName}"` : fieldName
118
+
119
+ /**
120
+ * Volatile SQL functions that cannot be used in GENERATED ALWAYS AS columns
121
+ * These functions return different values on each call or depend on external state
122
+ *
123
+ * NOTE: TO_CHAR is marked as STABLE (not IMMUTABLE) in PostgreSQL because its output
124
+ * can vary based on LC_TIME locale settings, making it unsuitable for GENERATED columns.
125
+ * Formulas using TO_CHAR will use trigger-based computation instead.
126
+ */
127
+ const volatileSQLFunctions = [
128
+ 'CURRENT_DATE',
129
+ 'CURRENT_TIME',
130
+ 'CURRENT_TIMESTAMP',
131
+ 'NOW()',
132
+ 'TIMEOFDAY()',
133
+ 'TRANSACTION_TIMESTAMP()',
134
+ 'STATEMENT_TIMESTAMP()',
135
+ 'CLOCK_TIMESTAMP()',
136
+ 'RANDOM()',
137
+ 'SETSEED(',
138
+ 'DECODE(',
139
+ 'CONVERT_FROM(',
140
+ 'TO_CHAR(',
141
+ 'TO_DATE(',
142
+ 'DATE_TRUNC(',
143
+ 'ARRAY_TO_STRING(',
144
+ ]
145
+
146
+ /**
147
+ * Type casts that make expressions non-immutable in PostgreSQL
148
+ * Casts to TIMESTAMP types depend on locale settings (DateStyle, TimeZone)
149
+ * making them volatile even when used with immutable functions like EXTRACT
150
+ */
151
+ const volatileTypeCasts = ['::TIMESTAMP', '::TIMESTAMPTZ', '::DATE', '::TIME']
152
+
153
+ /**
154
+ * Check if formula contains volatile functions that make it non-immutable
155
+ * PostgreSQL GENERATED ALWAYS AS columns must be immutable (deterministic)
156
+ *
157
+ * Special timestamp fields (created_at, updated_at) are set by triggers, so formulas
158
+ * referencing them must use trigger-based computation, not GENERATED columns
159
+ */
160
+ export const isFormulaVolatile = (formula: string): boolean => {
161
+ const upperFormula = formula.toUpperCase()
162
+ return (
163
+ volatileSQLFunctions.some((fn) => upperFormula.includes(fn)) ||
164
+ volatileTypeCasts.some((cast) => upperFormula.includes(cast)) ||
165
+ upperFormula.includes('CREATED_AT') ||
166
+ upperFormula.includes('UPDATED_AT')
167
+ )
168
+ }
169
+
170
+ /**
171
+ * SQL functions that return array types
172
+ * Used to automatically adjust column type when formula returns an array
173
+ *
174
+ * @example
175
+ * STRING_TO_ARRAY('a,b,c', ',') → ['a', 'b', 'c'] (TEXT[])
176
+ */
177
+ const arrayReturningFunctions = ['STRING_TO_ARRAY']
178
+
179
+ /**
180
+ * Check if formula returns an array type
181
+ * Some PostgreSQL functions return arrays regardless of input type
182
+ * NOTE: ARRAY_TO_STRING wraps an array and returns text, so check for it first
183
+ * NOTE: CARDINALITY wraps an array and returns integer, so check for it too
184
+ */
185
+ export const isFormulaReturningArray = (formula: string): boolean => {
186
+ const upperFormula = formula.toUpperCase().trim()
187
+
188
+ // If formula starts with ARRAY_TO_STRING, the result is text, not array
189
+ if (upperFormula.startsWith('ARRAY_TO_STRING(')) {
190
+ return false
191
+ }
192
+
193
+ // If formula starts with CARDINALITY, the result is integer, not array
194
+ if (upperFormula.startsWith('CARDINALITY(')) {
195
+ return false
196
+ }
197
+
198
+ return arrayReturningFunctions.some((fn) => upperFormula.includes(fn))
199
+ }
200
+
201
+ /**
202
+ * Parse ROUND function arguments, handling nested parentheses
203
+ * Returns {firstArg, secondArg, start, end} or undefined if not a valid ROUND call
204
+ */
205
+ const parseRoundArgs = (
206
+ formula: string,
207
+ matchIndex: number,
208
+ matchLength: number
209
+ ):
210
+ | {
211
+ readonly firstArg: string
212
+ readonly secondArg: string
213
+ readonly start: number
214
+ readonly end: number
215
+ }
216
+ | undefined => {
217
+ const argsStart = matchIndex + matchLength
218
+ const chars = [...formula.slice(argsStart)]
219
+
220
+ // Track state through the argument parsing
221
+ const state = chars.reduce<{
222
+ depth: number
223
+ firstArgEnd: number
224
+ currentIndex: number
225
+ done: boolean
226
+ }>(
227
+ (acc, char, idx) => {
228
+ if (acc.done) return acc
229
+
230
+ const absoluteIndex = argsStart + idx
231
+
232
+ if (char === '(') {
233
+ return { ...acc, depth: acc.depth + 1, currentIndex: absoluteIndex }
234
+ }
235
+
236
+ if (char === ')') {
237
+ const newDepth = acc.depth - 1
238
+ if (newDepth === 0) {
239
+ return { ...acc, depth: newDepth, currentIndex: absoluteIndex, done: true }
240
+ }
241
+ return { ...acc, depth: newDepth, currentIndex: absoluteIndex }
242
+ }
243
+
244
+ if (char === ',' && acc.depth === 1 && acc.firstArgEnd === -1) {
245
+ return { ...acc, firstArgEnd: absoluteIndex, currentIndex: absoluteIndex }
246
+ }
247
+
248
+ return { ...acc, currentIndex: absoluteIndex }
249
+ },
250
+ { depth: 1, firstArgEnd: -1, currentIndex: argsStart, done: false }
251
+ )
252
+
253
+ if (state.firstArgEnd === -1) return undefined
254
+
255
+ return {
256
+ firstArg: formula.slice(argsStart, state.firstArgEnd).trim(),
257
+ secondArg: formula.slice(state.firstArgEnd + 1, state.currentIndex).trim(),
258
+ start: matchIndex,
259
+ end: state.currentIndex + 1,
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Translate date/datetime/time field casts to TEXT using TO_CHAR
265
+ * DATE::TEXT depends on DateStyle (volatile), but TO_CHAR with format is immutable
266
+ */
267
+ const translateDateCastsToText = (
268
+ formula: string,
269
+ allFields?: readonly { name: string; type: string }[]
270
+ ): string => {
271
+ if (!allFields) return formula
272
+
273
+ return formula.replace(/(\w+)::TEXT/gi, (match, fieldName) => {
274
+ const field = allFields.find((f) => f.name.toLowerCase() === fieldName.toLowerCase())
275
+ if (field && (field.type === 'date' || field.type === 'datetime' || field.type === 'time')) {
276
+ // Use appropriate format based on field type
277
+ if (field.type === 'date') {
278
+ return `TO_CHAR(${escapeFieldName(fieldName)}, 'YYYY-MM-DD')`
279
+ }
280
+ if (field.type === 'datetime') {
281
+ return `TO_CHAR(${escapeFieldName(fieldName)}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`
282
+ }
283
+ if (field.type === 'time') {
284
+ return `TO_CHAR(${escapeFieldName(fieldName)}, 'HH24:MI:SS')`
285
+ }
286
+ }
287
+ return match // Keep original for non-date fields (e.g., num::TEXT)
288
+ })
289
+ }
290
+
291
+ /**
292
+ * Translate SUBSTR to PostgreSQL SUBSTRING syntax
293
+ * SUBSTR(text, start, length) → SUBSTRING(text FROM start FOR length)
294
+ */
295
+ const translateSubstrToSubstring = (formula: string): string =>
296
+ formula.replace(
297
+ /SUBSTR\s*\(\s*([^,]+?)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/gi,
298
+ (_, text, start, length) => {
299
+ const trimmedText = text.trim()
300
+ // Only escape if it's a field name (not already a function call or quoted)
301
+ const escapedText = trimmedText.match(/^\w+$/) ? escapeFieldName(trimmedText) : trimmedText
302
+ return `SUBSTRING(${escapedText} FROM ${start} FOR ${length})`
303
+ }
304
+ )
305
+
306
+ /**
307
+ * Add NUMERIC casts to ROUND functions that use double precision functions
308
+ * PostgreSQL's ROUND(numeric, integer) exists but ROUND(double precision, integer) does not
309
+ */
310
+ const addNumericCastsToRound = (formula: string): string => {
311
+ // Find all ROUND( occurrences and build replacement list
312
+ const roundMatches = [...formula.matchAll(/ROUND\s*\(/gi)]
313
+ const replacements = roundMatches
314
+ .map((match) => parseRoundArgs(formula, match.index ?? 0, match[0].length))
315
+ .filter((parsed): parsed is NonNullable<typeof parsed> => parsed !== undefined)
316
+ .filter((parsed) => /SQRT|POWER|EXP|LN|LOG/i.test(parsed.firstArg))
317
+ .map((parsed) => ({
318
+ start: parsed.start,
319
+ end: parsed.end,
320
+ replacement: `ROUND((${parsed.firstArg})::NUMERIC, ${parsed.secondArg})`,
321
+ }))
322
+
323
+ // Apply replacements in reverse order to maintain indices
324
+ const sortedReplacements = replacements.toSorted((a, b) => b.start - a.start)
325
+
326
+ return sortedReplacements.reduce(
327
+ (result, { start, end, replacement }) =>
328
+ result.slice(0, start) + replacement + result.slice(end),
329
+ formula
330
+ )
331
+ }
332
+
333
+ /**
334
+ * Escape field names that contain reserved words
335
+ * This handles references like "order_num * 2" → "\"order_num\" * 2"
336
+ */
337
+ const escapeReservedFieldNames = (
338
+ formula: string,
339
+ allFields?: readonly { name: string; type: string }[]
340
+ ): string => {
341
+ if (!allFields) return formula
342
+
343
+ return allFields.reduce((acc, field) => {
344
+ // Only escape this field if it needs escaping
345
+ if (!containsReservedWord(field.name)) {
346
+ return acc
347
+ }
348
+
349
+ // Create a regex that matches the field name as a whole word
350
+ // Use word boundaries (\b) to ensure we only match complete field names
351
+ // Use negative lookbehind to avoid matching if already quoted
352
+ const fieldRegex = new RegExp(`(?<!["'])\\b${field.name}\\b(?!["'])`, 'gi')
353
+ return acc.replace(fieldRegex, (match) => escapeFieldName(match))
354
+ }, formula)
355
+ }
356
+
357
+ /**
358
+ * Translate formula from user-friendly syntax to PostgreSQL syntax
359
+ * Converts SUBSTR(text, start, length) to SUBSTRING(text FROM start FOR length)
360
+ * Converts date_field::TEXT to TO_CHAR(date_field, 'YYYY-MM-DD') for immutability
361
+ * Casts ROUND arguments to NUMERIC when input may be double precision
362
+ * Escapes field names that contain reserved words (e.g., order_num → "order_num")
363
+ *
364
+ * NOTE: PostgreSQL natively supports nested function calls like ROUND(SQRT(ABS(value)), 2)
365
+ * and all standard mathematical functions (ABS, SQRT, ROUND, POWER, etc.), but ROUND only
366
+ * accepts NUMERIC as first argument, not double precision. Functions like SQRT, POWER, EXP, LN
367
+ * return double precision, so we cast them to NUMERIC before passing to ROUND.
368
+ */
369
+ export const translateFormulaToPostgres = (
370
+ formula: string,
371
+ allFields?: readonly { name: string; type: string }[]
372
+ ): string => {
373
+ const withDateToText = translateDateCastsToText(formula, allFields)
374
+ const withSubstring = translateSubstrToSubstring(withDateToText)
375
+ const withRoundCast = addNumericCastsToRound(withSubstring)
376
+ return escapeReservedFieldNames(withRoundCast, allFields)
377
+ }
378
+
379
+ /**
380
+ * EXTRACT date/time field keywords that should not be qualified
381
+ * These are valid arguments to EXTRACT() function and not column references
382
+ */
383
+ const EXTRACT_KEYWORDS = new Set([
384
+ 'year',
385
+ 'month',
386
+ 'day',
387
+ 'hour',
388
+ 'minute',
389
+ 'second',
390
+ 'dow',
391
+ 'doy',
392
+ 'week',
393
+ 'quarter',
394
+ 'decade',
395
+ 'century',
396
+ 'millennium',
397
+ 'epoch',
398
+ 'timezone',
399
+ 'timezone_hour',
400
+ 'timezone_minute',
401
+ ])
402
+
403
+ /**
404
+ * Qualify column references in a formula with a prefix
405
+ * Used by trigger functions to reference columns from subqueries or NEW/OLD records
406
+ *
407
+ * @example
408
+ * qualifyColumnReferences('NOT paid AND due_date < CURRENT_DATE', fields, 't')
409
+ * // Returns: 'NOT t.paid AND t.due_date < CURRENT_DATE'
410
+ *
411
+ * @example
412
+ * qualifyColumnReferences('EXTRACT(HOUR FROM timestamp_value::TIMESTAMP)', fields, 't')
413
+ * // Returns: 'EXTRACT(HOUR FROM t.timestamp_value::TIMESTAMP)' (HOUR not qualified)
414
+ */
415
+ export const qualifyColumnReferences = (
416
+ formula: string,
417
+ allFields: readonly { name: string; type: string }[],
418
+ prefix: string
419
+ ): string =>
420
+ allFields.reduce((acc, field) => {
421
+ // Don't qualify EXTRACT keywords (e.g., HOUR, MINUTE, DAY)
422
+ if (EXTRACT_KEYWORDS.has(field.name.toLowerCase())) {
423
+ // Check if this field name appears as an EXTRACT keyword
424
+ const extractPattern = new RegExp(`\\bEXTRACT\\s*\\(\\s*${field.name}\\s+FROM\\b`, 'gi')
425
+ if (extractPattern.test(acc)) {
426
+ // This is an EXTRACT keyword, only qualify non-keyword occurrences
427
+ // Match the field name but exclude it when preceded by EXTRACT(
428
+ const fieldRegex = new RegExp(
429
+ `(?<!EXTRACT\\s{0,10}\\(\\s{0,10})(?<![."])\\b${field.name}\\b(?!["']|\\s+FROM)`,
430
+ 'gi'
431
+ )
432
+ return acc.replace(fieldRegex, `${prefix}.${field.name}`)
433
+ }
434
+ }
435
+
436
+ // Create regex that matches field name as a whole word
437
+ // Use word boundaries (\b) and negative lookbehind for dots (avoid double-qualifying)
438
+ const fieldRegex = new RegExp(`(?<![."])\\b${field.name}\\b(?!["'])`, 'gi')
439
+ return acc.replace(fieldRegex, `${prefix}.${field.name}`)
440
+ }, formula)
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Copyright (c) 2025 ESSENTIAL SERVICES
3
+ *
4
+ * This source code is licensed under the Business Source License 1.1
5
+ * found in the LICENSE.md file in the root directory of this source tree.
6
+ */
7
+
8
+ import { sanitizeTableName } from '../field-utils'
9
+ import { isRelationshipField, isUserField } from '../sql/sql-generators'
10
+ import type { Table } from '@/domain/models/app/table'
11
+ import type { Fields } from '@/domain/models/app/table/fields'
12
+
13
+ /**
14
+ * Generate standard indexes for indexed fields
15
+ */
16
+ const generateStandardIndexes = (table: Table): readonly string[] => {
17
+ const sanitized = sanitizeTableName(table.name)
18
+ return table.fields
19
+ .filter(
20
+ (field): field is Fields[number] & { indexed: true } => 'indexed' in field && !!field.indexed
21
+ )
22
+ .map((field) => {
23
+ // Status fields use special naming: idx_{table}_status instead of idx_{table}_{field_name}
24
+ const indexSuffix = field.type === 'status' ? 'status' : field.name
25
+ const indexName = `idx_${sanitized}_${indexSuffix}`
26
+ const indexType =
27
+ field.type === 'array' || field.type === 'json'
28
+ ? 'USING gin'
29
+ : field.type === 'geolocation'
30
+ ? 'USING gist'
31
+ : 'USING btree'
32
+ return `CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} ${indexType} (${field.name})`
33
+ })
34
+ }
35
+
36
+ /**
37
+ * Generate unique indexes for autonumber fields
38
+ */
39
+ const generateAutonumberIndexes = (table: Table): readonly string[] => {
40
+ const sanitized = sanitizeTableName(table.name)
41
+ return table.fields
42
+ .filter((field) => field.type === 'autonumber')
43
+ .map((field) => {
44
+ const indexName = `idx_${sanitized}_${field.name}_unique`
45
+ return `CREATE UNIQUE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} (${field.name})`
46
+ })
47
+ }
48
+
49
+ /**
50
+ * Generate exclusion constraints for geolocation fields with unique constraint
51
+ * NOTE: POINT type doesn't support btree UNIQUE constraints or GiST UNIQUE indexes
52
+ * PostgreSQL requires EXCLUDE USING gist for uniqueness on geometric types using ~= operator
53
+ */
54
+ const generateGeolocationConstraints = (table: Table): readonly string[] => {
55
+ const sanitized = sanitizeTableName(table.name)
56
+ return table.fields
57
+ .filter(
58
+ (field): field is Fields[number] & { type: 'geolocation'; unique: true } =>
59
+ field.type === 'geolocation' && 'unique' in field && !!field.unique
60
+ )
61
+ .map((field) => {
62
+ // Use PostgreSQL naming convention: {table}_{column}_key (matches constraint naming)
63
+ const constraintName = `${sanitized}_${field.name}_key`
64
+ return `ALTER TABLE public.${sanitized} ADD CONSTRAINT ${constraintName} EXCLUDE USING gist (${field.name} WITH ~=)`
65
+ })
66
+ }
67
+
68
+ /**
69
+ * Generate full-text search GIN indexes for rich-text fields
70
+ */
71
+ const generateFullTextSearchIndexes = (table: Table): readonly string[] => {
72
+ const sanitized = sanitizeTableName(table.name)
73
+ return table.fields
74
+ .filter(
75
+ (field): field is Fields[number] & { type: 'rich-text'; fullTextSearch: true } =>
76
+ field.type === 'rich-text' && 'fullTextSearch' in field && !!field.fullTextSearch
77
+ )
78
+ .map((field) => {
79
+ const indexName = `idx_${sanitized}_${field.name}_fulltext`
80
+ return `CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} USING gin (to_tsvector('english'::regconfig, ${field.name}))`
81
+ })
82
+ }
83
+
84
+ /**
85
+ * Generate custom indexes from table.indexes configuration
86
+ */
87
+ const generateCustomIndexes = (table: Table): readonly string[] => {
88
+ const sanitized = sanitizeTableName(table.name)
89
+ return (
90
+ table.indexes?.map((index) => {
91
+ const uniqueClause = index.unique ? 'UNIQUE ' : ''
92
+ const fields = index.fields.join(', ')
93
+ const whereClause = 'where' in index && index.where ? ` WHERE ${index.where}` : ''
94
+ return `CREATE ${uniqueClause}INDEX IF NOT EXISTS ${index.name} ON public.${sanitized} (${fields})${whereClause}`
95
+ }) ?? []
96
+ )
97
+ }
98
+
99
+ /**
100
+ * Generate index for intrinsic deleted_at column (soft-delete optimization)
101
+ * This index improves performance for common soft-delete queries:
102
+ * - WHERE deleted_at IS NULL (active records)
103
+ * - WHERE deleted_at IS NOT NULL (deleted records)
104
+ */
105
+ const generateDeletedAtIndex = (table: Table): readonly string[] => {
106
+ const sanitized = sanitizeTableName(table.name)
107
+ const indexName = `idx_${sanitized}_deleted_at`
108
+ return [`CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} USING btree (deleted_at)`]
109
+ }
110
+
111
+ /**
112
+ * Generate indexes for foreign key columns (relationship and user fields)
113
+ * Foreign key columns benefit from indexes for JOIN operations and referential integrity checks
114
+ * This improves query performance when filtering or joining on relationships
115
+ */
116
+ const generateForeignKeyIndexes = (table: Table): readonly string[] => {
117
+ const sanitized = sanitizeTableName(table.name)
118
+ const relationshipIndexes = table.fields
119
+ .filter(isRelationshipField)
120
+ .filter((field) => {
121
+ // Only create indexes for many-to-one relationships (where FK exists on this table)
122
+ // Exclude one-to-many (FK in related table) and many-to-many (uses junction table)
123
+ return (
124
+ !('relationType' in field) ||
125
+ (field.relationType !== 'one-to-many' && field.relationType !== 'many-to-many')
126
+ )
127
+ })
128
+ .map((field) => {
129
+ const indexName = `idx_${sanitized}_${field.name}_fk`
130
+ return `CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} USING btree (${field.name})`
131
+ })
132
+
133
+ const userFieldIndexes = table.fields.filter(isUserField).map((field) => {
134
+ const indexName = `idx_${sanitized}_${field.name}_fk`
135
+ return `CREATE INDEX IF NOT EXISTS ${indexName} ON public.${sanitized} USING btree (${field.name})`
136
+ })
137
+
138
+ return [...relationshipIndexes, ...userFieldIndexes]
139
+ }
140
+
141
+ /**
142
+ * Generate CREATE INDEX statements for indexed fields and autonumber fields
143
+ */
144
+ export const generateIndexStatements = (table: Table): readonly string[] => [
145
+ ...generateStandardIndexes(table),
146
+ ...generateAutonumberIndexes(table),
147
+ ...generateGeolocationConstraints(table),
148
+ ...generateFullTextSearchIndexes(table),
149
+ ...generateCustomIndexes(table),
150
+ ...generateForeignKeyIndexes(table),
151
+ ...generateDeletedAtIndex(table),
152
+ ]