@openmrs/esm-report-builder 0.1.1-pre.101

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 (670) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.js +19 -0
  3. package/.prettierrc +8 -0
  4. package/.tx/config +12 -0
  5. package/LICENSE +401 -0
  6. package/README.md +336 -0
  7. package/__mocks__/react-i18next.js +12 -0
  8. package/dist/128.css +1 -0
  9. package/dist/128.css.map +1 -0
  10. package/dist/128.js +338 -0
  11. package/dist/128.js.map +1 -0
  12. package/dist/1480.js +1 -0
  13. package/dist/1480.js.map +1 -0
  14. package/dist/1646.js +1 -0
  15. package/dist/1646.js.map +1 -0
  16. package/dist/1869.js +1 -0
  17. package/dist/1869.js.map +1 -0
  18. package/dist/1877.js +1 -0
  19. package/dist/1877.js.map +1 -0
  20. package/dist/2131.js +1 -0
  21. package/dist/2131.js.map +1 -0
  22. package/dist/2317.js +1 -0
  23. package/dist/2317.js.map +1 -0
  24. package/dist/2408.js +1 -0
  25. package/dist/2408.js.map +1 -0
  26. package/dist/2416.js +1 -0
  27. package/dist/2416.js.map +1 -0
  28. package/dist/282.js +1 -0
  29. package/dist/282.js.map +1 -0
  30. package/dist/2881.js +1 -0
  31. package/dist/2881.js.map +1 -0
  32. package/dist/2997.js +1 -0
  33. package/dist/2997.js.map +1 -0
  34. package/dist/3378.js +1 -0
  35. package/dist/3378.js.map +1 -0
  36. package/dist/3963.js +1 -0
  37. package/dist/3963.js.map +1 -0
  38. package/dist/4106.js +1 -0
  39. package/dist/4106.js.map +1 -0
  40. package/dist/4111.js +1 -0
  41. package/dist/4111.js.map +1 -0
  42. package/dist/4171.css +1 -0
  43. package/dist/4171.css.map +1 -0
  44. package/dist/4171.js +43 -0
  45. package/dist/4171.js.map +1 -0
  46. package/dist/434.js +1 -0
  47. package/dist/434.js.map +1 -0
  48. package/dist/4348.js +1 -0
  49. package/dist/4348.js.map +1 -0
  50. package/dist/4383.js +1 -0
  51. package/dist/4383.js.map +1 -0
  52. package/dist/4658.js +1 -0
  53. package/dist/4658.js.map +1 -0
  54. package/dist/4668.js +1 -0
  55. package/dist/4668.js.map +1 -0
  56. package/dist/4928.js +1 -0
  57. package/dist/4928.js.map +1 -0
  58. package/dist/5098.js +1 -0
  59. package/dist/5098.js.map +1 -0
  60. package/dist/5117.js +1 -0
  61. package/dist/5117.js.map +1 -0
  62. package/dist/5132.js +1 -0
  63. package/dist/5132.js.map +1 -0
  64. package/dist/5145.js +1 -0
  65. package/dist/5145.js.map +1 -0
  66. package/dist/550.js +1 -0
  67. package/dist/550.js.map +1 -0
  68. package/dist/5503.js +1 -0
  69. package/dist/5503.js.map +1 -0
  70. package/dist/556.js +1 -0
  71. package/dist/556.js.map +1 -0
  72. package/dist/5644.js +1 -0
  73. package/dist/5644.js.map +1 -0
  74. package/dist/5940.js +1 -0
  75. package/dist/5940.js.map +1 -0
  76. package/dist/6047.js +1 -0
  77. package/dist/6047.js.map +1 -0
  78. package/dist/6237.js +1 -0
  79. package/dist/6237.js.map +1 -0
  80. package/dist/6371.js +1 -0
  81. package/dist/6371.js.map +1 -0
  82. package/dist/6377.js +1 -0
  83. package/dist/6377.js.map +1 -0
  84. package/dist/6444.js +1 -0
  85. package/dist/6444.js.map +1 -0
  86. package/dist/6508.js +1 -0
  87. package/dist/6508.js.map +1 -0
  88. package/dist/6652.js +1 -0
  89. package/dist/6652.js.map +1 -0
  90. package/dist/6686.js +1 -0
  91. package/dist/6686.js.map +1 -0
  92. package/dist/6724.js +1 -0
  93. package/dist/6724.js.map +1 -0
  94. package/dist/6867.js +19 -0
  95. package/dist/6867.js.map +1 -0
  96. package/dist/6904.js +1 -0
  97. package/dist/6904.js.map +1 -0
  98. package/dist/7045.js +1 -0
  99. package/dist/7045.js.map +1 -0
  100. package/dist/7175.js +1 -0
  101. package/dist/7175.js.map +1 -0
  102. package/dist/7182.js +1 -0
  103. package/dist/7182.js.map +1 -0
  104. package/dist/7475.js +1 -0
  105. package/dist/7475.js.map +1 -0
  106. package/dist/7742.js +1 -0
  107. package/dist/7742.js.map +1 -0
  108. package/dist/7912.js +1 -0
  109. package/dist/7912.js.map +1 -0
  110. package/dist/8251.js +1 -0
  111. package/dist/8251.js.map +1 -0
  112. package/dist/8358.js +1 -0
  113. package/dist/8358.js.map +1 -0
  114. package/dist/8359.js +1 -0
  115. package/dist/8359.js.map +1 -0
  116. package/dist/8695.js +1 -0
  117. package/dist/8695.js.map +1 -0
  118. package/dist/903.js +1 -0
  119. package/dist/903.js.map +1 -0
  120. package/dist/9072.js +1 -0
  121. package/dist/9072.js.map +1 -0
  122. package/dist/9806.js +1 -0
  123. package/dist/9806.js.map +1 -0
  124. package/dist/997.js +1 -0
  125. package/dist/997.js.map +1 -0
  126. package/dist/openmrs-esm-report-builder.js +3 -0
  127. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +1433 -0
  128. package/dist/openmrs-esm-report-builder.js.map +1 -0
  129. package/dist/routes.json +1 -0
  130. package/e2e/core/env.ts +5 -0
  131. package/e2e/core/global-setup.ts +34 -0
  132. package/e2e/core/test.ts +21 -0
  133. package/e2e/pages/app-shell.page.ts +35 -0
  134. package/e2e/pages/etl-sources.page.ts +36 -0
  135. package/e2e/pages/report-sections.page.ts +21 -0
  136. package/e2e/pages/section-preview.modal.ts +29 -0
  137. package/e2e/specs/etl-sources.spec.ts +51 -0
  138. package/e2e/specs/navigation.spec.ts +13 -0
  139. package/e2e/specs/section-preview-validation.spec.ts +38 -0
  140. package/e2e/specs/section-preview.spec.ts +94 -0
  141. package/e2e/support/github/Dockerfile +32 -0
  142. package/e2e/support/github/Dockerfile.dockerignore +13 -0
  143. package/e2e/support/github/docker-compose.yml +31 -0
  144. package/e2e/support/github/run-e2e-docker-env.sh +131 -0
  145. package/e2e/support/github/spa-assemble-config.json +6 -0
  146. package/e2e/tsconfig.json +11 -0
  147. package/example.env +5 -0
  148. package/linelist_report_builder_design_spec.md +816 -0
  149. package/package.json +98 -0
  150. package/playwright.config.ts +28 -0
  151. package/rspack.config.js +2 -0
  152. package/src/__debug__/all-designs.test.tsx +33 -0
  153. package/src/admin-card-link.extension.module.scss +55 -0
  154. package/src/admin-card-link.extension.tsx +24 -0
  155. package/src/components/admin/admin-page.component.tsx +75 -0
  156. package/src/components/admin/age-categories-page.component.tsx +109 -0
  157. package/src/components/admin/age-groups-page.component.tsx +146 -0
  158. package/src/components/admin/etl-browser/columns-tab.component.tsx +80 -0
  159. package/src/components/admin/etl-browser/etl-browser-page.component.tsx +309 -0
  160. package/src/components/admin/etl-browser/etl-browser.component.scss +424 -0
  161. package/src/components/admin/etl-browser/metadata-tab.component.tsx +65 -0
  162. package/src/components/admin/etl-browser/preview-grid.component.tsx +189 -0
  163. package/src/components/admin/etl-browser/relationships-tab.component.tsx +21 -0
  164. package/src/components/admin/etl-browser/sources-tree.component.tsx +118 -0
  165. package/src/components/admin/etl-browser/table-detail.component.tsx +124 -0
  166. package/src/components/admin/etl-browser/table-info-strip.component.tsx +51 -0
  167. package/src/components/admin/etl-browser/table-list.component.tsx +136 -0
  168. package/src/components/admin/etl-browser/use-table-preview.ts +47 -0
  169. package/src/components/admin/etl-sources-page.component.tsx +355 -0
  170. package/src/components/admin/etl-tasks-page.component.scss +370 -0
  171. package/src/components/admin/etl-tasks-page.component.tsx +768 -0
  172. package/src/components/admin/report-categories-page.component.tsx +267 -0
  173. package/src/components/admin/report-library-page.component.tsx +551 -0
  174. package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
  175. package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
  176. package/src/components/ai-support/ai.service.ts +21 -0
  177. package/src/components/ai-support/ai.types.ts +12 -0
  178. package/src/components/app-shell/report-builder-shell.component.tsx +242 -0
  179. package/src/components/app-shell/report-builder-shell.scss +18 -0
  180. package/src/components/dashboards/dashboard-config.schema.ts +97 -0
  181. package/src/components/dashboards/dashboard-form-modal.component.tsx +150 -0
  182. package/src/components/dashboards/dashboards-admin.scss +62 -0
  183. package/src/components/dashboards/dashboards-page.component.tsx +153 -0
  184. package/src/components/dashboards/dashboards-table.component.tsx +130 -0
  185. package/src/components/dashboards/sections/dashboard-autoinclude.section.tsx +75 -0
  186. package/src/components/dashboards/sections/dashboard-basics.section.tsx +80 -0
  187. package/src/components/dashboards/sections/dashboard-json.section.tsx +79 -0
  188. package/src/components/dashboards/sections/dashboard-sections-editor.section.tsx +130 -0
  189. package/src/components/dashboards/sections/dashboard-widgets-editor.section.tsx +184 -0
  190. package/src/components/data-themes/data-theme-form.component.tsx +59 -0
  191. package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
  192. package/src/components/data-themes/data-themes-page.component.tsx +139 -0
  193. package/src/components/data-themes/data-themes-table.component.tsx +98 -0
  194. package/src/components/data-themes/icon-registry.ts +35 -0
  195. package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
  196. package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
  197. package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
  198. package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
  199. package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
  200. package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
  201. package/src/components/data-visualizer/components/cqi-components/cqi-data-table.component.tsx +214 -0
  202. package/src/components/data-visualizer/components/cqi-components/cqi-elements.ts +203 -0
  203. package/src/components/data-visualizer/components/cqi-components/detail.component.tsx +110 -0
  204. package/src/components/data-visualizer/components/cqi-components/detail.scss +22 -0
  205. package/src/components/data-visualizer/components/data-table/data-table.component.tsx +208 -0
  206. package/src/components/data-visualizer/components/data-table/data-tables.scss +206 -0
  207. package/src/components/data-visualizer/components/empty-state/empty-state-illustration.component.tsx +53 -0
  208. package/src/components/data-visualizer/components/header/header.component.tsx +43 -0
  209. package/src/components/data-visualizer/components/header/header.scss +71 -0
  210. package/src/components/data-visualizer/components/model/confirm.component.tsx +40 -0
  211. package/src/components/data-visualizer/components/panel/panel.component.tsx +20 -0
  212. package/src/components/data-visualizer/components/panel/panel.scss +25 -0
  213. package/src/components/data-visualizer/components/popover/modifier-panel.tsx +53 -0
  214. package/src/components/data-visualizer/constants.ts +445 -0
  215. package/src/components/data-visualizer/data-visualizer-illustration.component.tsx +31 -0
  216. package/src/components/data-visualizer/data-visualizer.component.tsx +1935 -0
  217. package/src/components/data-visualizer/data-visualizer.resource.ts +462 -0
  218. package/src/components/data-visualizer/data-visualizer.scss +448 -0
  219. package/src/components/data-visualizer/declarations.d.ts +131 -0
  220. package/src/components/data-visualizer/parameter-inputs/boolean-parameter-input.component.tsx +34 -0
  221. package/src/components/data-visualizer/parameter-inputs/concept-parameter-input.component.tsx +81 -0
  222. package/src/components/data-visualizer/parameter-inputs/date-parameter-input.component.tsx +110 -0
  223. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.scss +47 -0
  224. package/src/components/data-visualizer/parameter-inputs/date-range-input.component.tsx +228 -0
  225. package/src/components/data-visualizer/parameter-inputs/identifier-type-parameter-input.component.tsx +69 -0
  226. package/src/components/data-visualizer/parameter-inputs/list-parameter-input.component.tsx +73 -0
  227. package/src/components/data-visualizer/parameter-inputs/location-parameter-input.component.tsx +81 -0
  228. package/src/components/data-visualizer/parameter-inputs/number-parameter-input.component.tsx +40 -0
  229. package/src/components/data-visualizer/parameter-inputs/person-attribute-parameter-input.component.tsx +71 -0
  230. package/src/components/data-visualizer/parameter-inputs/text-parameter-input.component.tsx +52 -0
  231. package/src/components/data-visualizer/report-parameter-modal.component.tsx +383 -0
  232. package/src/components/data-visualizer/types.ts +206 -0
  233. package/src/components/etl-monitor/builder/builder-state-machine.ts +381 -0
  234. package/src/components/etl-monitor/builder/design-registry.tsx +139 -0
  235. package/src/components/etl-monitor/builder/etl-monitor-builder-context.tsx +702 -0
  236. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.component.tsx +461 -0
  237. package/src/components/etl-monitor/builder/etl-monitor-builder-modal.scss +256 -0
  238. package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +387 -0
  239. package/src/components/etl-monitor/builder/etl-monitor-builder-page.scss +976 -0
  240. package/src/components/etl-monitor/builder/generated-config-panel.component.tsx +168 -0
  241. package/src/components/etl-monitor/builder/generated-config-panel.scss +198 -0
  242. package/src/components/etl-monitor/builder/index.ts +5 -0
  243. package/src/components/etl-monitor/builder/monitor-preview.tsx +124 -0
  244. package/src/components/etl-monitor/builder/steps/data-source-step.component.tsx +667 -0
  245. package/src/components/etl-monitor/builder/steps/data-source-step.scss +284 -0
  246. package/src/components/etl-monitor/builder/steps/design-samples.ts +278 -0
  247. package/src/components/etl-monitor/builder/steps/design-step.component.tsx +299 -0
  248. package/src/components/etl-monitor/builder/steps/design-step.scss +479 -0
  249. package/src/components/etl-monitor/builder/steps/fields-step.component.tsx +859 -0
  250. package/src/components/etl-monitor/builder/steps/fields-step.scss +459 -0
  251. package/src/components/etl-monitor/builder/steps/general-step.component.tsx +288 -0
  252. package/src/components/etl-monitor/builder/steps/general-step.scss +103 -0
  253. package/src/components/etl-monitor/builder/steps/index.ts +5 -0
  254. package/src/components/etl-monitor/builder/steps/preview-step.component.tsx +245 -0
  255. package/src/components/etl-monitor/builder/steps/preview-step.scss +236 -0
  256. package/src/components/etl-monitor/etl-monitor-card-shell.component.tsx +154 -0
  257. package/src/components/etl-monitor/etl-monitor-card-shell.scss +103 -0
  258. package/src/components/etl-monitor/etl-monitor-dashboard.component.tsx +227 -0
  259. package/src/components/etl-monitor/etl-monitor-dashboard.scss +148 -0
  260. package/src/components/etl-monitor/etl-monitor-status-card.component.tsx +192 -0
  261. package/src/components/etl-monitor/etl-monitors-page.component.tsx +621 -0
  262. package/src/components/etl-monitor/renderers/DetailsRenderer.tsx +2 -0
  263. package/src/components/etl-monitor/renderers/ErrorLogRenderer.tsx +2 -0
  264. package/src/components/etl-monitor/renderers/EtlMonitorWidget.tsx +97 -0
  265. package/src/components/etl-monitor/renderers/MetricsGridRenderer.tsx +2 -0
  266. package/src/components/etl-monitor/renderers/MonitorRenderer.tsx +175 -0
  267. package/src/components/etl-monitor/renderers/ProgressRenderer.tsx +2 -0
  268. package/src/components/etl-monitor/renderers/StatusCardRenderer.tsx +2 -0
  269. package/src/components/etl-monitor/renderers/SummaryCardRenderer.tsx +2 -0
  270. package/src/components/etl-monitor/renderers/TableRenderer.tsx +2 -0
  271. package/src/components/etl-monitor/renderers/component-icons.tsx +33 -0
  272. package/src/components/etl-monitor/renderers/components/details-renderer.tsx +195 -0
  273. package/src/components/etl-monitor/renderers/components/error-log-renderer.tsx +104 -0
  274. package/src/components/etl-monitor/renderers/components/index.ts +6 -0
  275. package/src/components/etl-monitor/renderers/components/log-renderer.tsx +86 -0
  276. package/src/components/etl-monitor/renderers/components/metrics-grid-renderer.tsx +133 -0
  277. package/src/components/etl-monitor/renderers/components/progress-renderer.tsx +188 -0
  278. package/src/components/etl-monitor/renderers/components/status-card-renderer.tsx +247 -0
  279. package/src/components/etl-monitor/renderers/components/summary-card-renderer.tsx +160 -0
  280. package/src/components/etl-monitor/renderers/components/table-renderer.tsx +175 -0
  281. package/src/components/etl-monitor/renderers/components/time-series-renderer.tsx +310 -0
  282. package/src/components/etl-monitor/renderers/data-transformer.ts +361 -0
  283. package/src/components/etl-monitor/renderers/index.ts +11 -0
  284. package/src/components/etl-monitor/renderers/legacy-monitor-renderer.tsx +69 -0
  285. package/src/components/etl-monitor/renderers/monitor-renderer.tsx +6 -0
  286. package/src/components/etl-monitor/renderers/monitor-renderers.scss +1621 -0
  287. package/src/components/etl-monitor/renderers/monitor-widget-card.component.tsx +54 -0
  288. package/src/components/etl-monitor/renderers/monitor-widget-card.scss +16 -0
  289. package/src/components/etl-monitor/renderers/v2-monitor-renderer.tsx +227 -0
  290. package/src/components/import-export/artifact-groups.ts +153 -0
  291. package/src/components/import-export/export-report-modal.component.tsx +265 -0
  292. package/src/components/import-export/import-export-page.component.tsx +610 -0
  293. package/src/components/import-export/import-export.styles.scss +825 -0
  294. package/src/components/import-export/import-package-modal.component.tsx +401 -0
  295. package/src/components/import-export/index.ts +3 -0
  296. package/src/components/indicators/README.md +24 -0
  297. package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
  298. package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
  299. package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
  300. package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
  301. package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
  302. package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
  303. package/src/components/indicators/create-base-indicator-modal.component.tsx +623 -0
  304. package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +295 -0
  305. package/src/components/indicators/create-custom-indicator-modal.component.tsx +548 -0
  306. package/src/components/indicators/create-final-indicator-modal.component.tsx +285 -0
  307. package/src/components/indicators/custom-condition-input.component.tsx +310 -0
  308. package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
  309. package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
  310. package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
  311. package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
  312. package/src/components/indicators/indicator-run-preview-modal.component.tsx +391 -0
  313. package/src/components/indicators/indicator-search-select.component.tsx +297 -0
  314. package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
  315. package/src/components/indicators/indicators-page.component.tsx +673 -0
  316. package/src/components/indicators/indicators-page.scss +121 -0
  317. package/src/components/indicators/indicators-table.component.tsx +188 -0
  318. package/src/components/indicators/preview-table.component.tsx +31 -0
  319. package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
  320. package/src/components/indicators/sections/composite-indicator-picker.section.tsx +116 -0
  321. package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
  322. package/src/components/indicators/sections/custom-indicator-basics.section.tsx +65 -0
  323. package/src/components/indicators/sections/custom-indicator-disaggregation.section.tsx +93 -0
  324. package/src/components/indicators/sections/custom-indicator-filters.section.tsx +203 -0
  325. package/src/components/indicators/sections/custom-indicator-population.section.tsx +107 -0
  326. package/src/components/indicators/sections/custom-indicator-sql-preview.section.tsx +108 -0
  327. package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
  328. package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
  329. package/src/components/indicators/sections/final-indicator-picker.section.tsx +60 -0
  330. package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
  331. package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
  332. package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
  333. package/src/components/indicators/sections/indicator-conditions.section.tsx +279 -0
  334. package/src/components/indicators/sections/indicator-sql-preview.section.tsx +63 -0
  335. package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
  336. package/src/components/indicators/sql-preview.component.tsx +19 -0
  337. package/src/components/indicators/tag-input.component.tsx +177 -0
  338. package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
  339. package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
  340. package/src/components/indicators/types/composite-indicator.types.ts +27 -0
  341. package/src/components/indicators/types/condition-ui.types.ts +11 -0
  342. package/src/components/indicators/types/custom-indicator.types.ts +161 -0
  343. package/src/components/indicators/types/data-theme-config.types.ts +88 -0
  344. package/src/components/indicators/types/indicator-types.ts +57 -0
  345. package/src/components/indicators/types/sql-builders.ts +43 -0
  346. package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +143 -0
  347. package/src/components/indicators/utils/composite-indicator-sql.utils.ts +426 -0
  348. package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
  349. package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +493 -0
  350. package/src/components/indicators/utils/custom-indicator-interpreter.ts +702 -0
  351. package/src/components/indicators/utils/final-indicator-sql.utils.ts +190 -0
  352. package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
  353. package/src/components/indicators/utils/indicator-sql-resolution.utils.ts +204 -0
  354. package/src/components/indicators/utils/indicator-sql.utils.ts +721 -0
  355. package/src/components/indicators/utils/population-sql.compiler.test.ts +985 -0
  356. package/src/components/indicators/utils/population-sql.compiler.ts +858 -0
  357. package/src/components/landing/landing-action-item.component.tsx +50 -0
  358. package/src/components/landing/landing-action-list.component.tsx +12 -0
  359. package/src/components/landing/landing-activity-list.component.tsx +47 -0
  360. package/src/components/landing/landing-footer-grid.component.tsx +45 -0
  361. package/src/components/landing/landing-hero.component.tsx +18 -0
  362. package/src/components/landing/landing-section-card.component.tsx +33 -0
  363. package/src/components/landing/report-builder-landing-page.component.tsx +186 -0
  364. package/src/components/landing/report-builder-landing-page.scss +131 -0
  365. package/src/components/legacy-reports/legacy-report-detail-page.component.tsx +300 -0
  366. package/src/components/legacy-reports/legacy-report-editor-page.component.tsx +324 -0
  367. package/src/components/legacy-reports/legacy-report-upload-modal.component.tsx +307 -0
  368. package/src/components/legacy-reports/legacy-reports-page.component.tsx +206 -0
  369. package/src/components/legacy-reports/legacy-reports-table.component.tsx +154 -0
  370. package/src/components/legacy-reports/tabs/legacy-report-datasets-tab.component.tsx +344 -0
  371. package/src/components/legacy-reports/tabs/legacy-report-dimensions-tab.component.tsx +590 -0
  372. package/src/components/legacy-reports/tabs/legacy-report-indicators-tab.component.tsx +490 -0
  373. package/src/components/legacy-reports/tabs/legacy-report-json-preview-tab.component.tsx +272 -0
  374. package/src/components/legacy-reports/tabs/legacy-report-overview-tab.component.tsx +218 -0
  375. package/src/components/legacy-reports/tabs/legacy-report-parameters-tab.component.tsx +279 -0
  376. package/src/components/linelist/definition/indicator-to-sql.utils.ts +281 -0
  377. package/src/components/linelist/definition/linelist-definition-editor.component.tsx +1676 -0
  378. package/src/components/linelist/definition/linelist-definition-editor.scss +461 -0
  379. package/src/components/linelist/definition/linelist-definition.types.ts +108 -0
  380. package/src/components/linelist/definition/panels/column-properties-panel.component.tsx +328 -0
  381. package/src/components/linelist/definition/panels/column-selector-panel.component.tsx +1082 -0
  382. package/src/components/linelist/definition/panels/column-selector-panel.scss +100 -0
  383. package/src/components/linelist/definition/panels/custom-sql-column-modal.component.tsx +248 -0
  384. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.component.tsx +300 -0
  385. package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.scss +21 -0
  386. package/src/components/linelist/definition/panels/multi-datasource-selector.component.tsx +332 -0
  387. package/src/components/linelist/definition/panels/multi-datasource-selector.scss +161 -0
  388. package/src/components/linelist/definition/panels/observation-column-modal.component.tsx +450 -0
  389. package/src/components/linelist/definition/panels/observation-column-modal.scss +29 -0
  390. package/src/components/linelist/definition/panels/parameter-panel.component.tsx +659 -0
  391. package/src/components/linelist/definition/panels/parameter-panel.scss +64 -0
  392. package/src/components/linelist/definition/panels/population-source-selector.component.tsx +407 -0
  393. package/src/components/linelist/definition/panels/population-source-selector.scss +187 -0
  394. package/src/components/linelist/design/panels/column-category-selector.component.tsx +433 -0
  395. package/src/components/linelist/design/panels/column-category-selector.scss +324 -0
  396. package/src/components/linelist/design/panels/column-debug-panel.component.tsx +197 -0
  397. package/src/components/linelist/design/panels/column-debug-panel.scss +141 -0
  398. package/src/components/linelist/design/panels/data-catalogue.component.tsx +1062 -0
  399. package/src/components/linelist/design/panels/data-catalogue.scss +263 -0
  400. package/src/components/linelist/design/panels/display-settings-panel.component.tsx +405 -0
  401. package/src/components/linelist/design/panels/display-settings-panel.scss +78 -0
  402. package/src/components/linelist/design/panels/filtermap-editor.component.tsx +434 -0
  403. package/src/components/linelist/design/panels/filtermap-editor.scss +176 -0
  404. package/src/components/linelist/design/panels/json-preview.component.tsx +114 -0
  405. package/src/components/linelist/design/panels/json-preview.scss +85 -0
  406. package/src/components/linelist/design/panels/query-config-panel.component.tsx +1137 -0
  407. package/src/components/linelist/design/panels/query-config-panel.scss +341 -0
  408. package/src/components/linelist/design/panels/sort-config-panel.component.tsx +330 -0
  409. package/src/components/linelist/design/panels/sort-config-panel.scss +77 -0
  410. package/src/components/linelist/design/panels/sql-builder-panel.component.tsx +475 -0
  411. package/src/components/linelist/design/panels/sql-builder-panel.scss +226 -0
  412. package/src/components/linelist/design/panels/transformation-panel.component.tsx +687 -0
  413. package/src/components/linelist/design/panels/transformation-panel.scss +81 -0
  414. package/src/components/linelist/design/panels/visual-filter-panel.component.tsx +595 -0
  415. package/src/components/linelist/design/panels/visual-filter-panel.scss +76 -0
  416. package/src/components/linelist/linelist-dashboard.page.component.tsx +756 -0
  417. package/src/components/linelist/linelist-dashboard.page.scss +240 -0
  418. package/src/components/linelist/linelist-editor.page.component.tsx +134 -0
  419. package/src/components/linelist/linelist-editor.page.scss +28 -0
  420. package/src/components/linelist/linelist-run-report.page.component.tsx +715 -0
  421. package/src/components/linelist/linelist-run-report.page.scss +136 -0
  422. package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +1454 -0
  423. package/src/components/linelist/modals/linelist-report-builder-modal.scss +164 -0
  424. package/src/components/linelist/modals/preview-parameter-modal.component.tsx +361 -0
  425. package/src/components/report/dashboard/reports-table.component.tsx +186 -0
  426. package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
  427. package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
  428. package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
  429. package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
  430. package/src/components/report/definition/report-definition.types.ts +66 -0
  431. package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
  432. package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
  433. package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
  434. package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
  435. package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
  436. package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
  437. package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
  438. package/src/components/report/design/panels/template-structure-panel.scss +81 -0
  439. package/src/components/report/design/report-design-editor.component.tsx +1170 -0
  440. package/src/components/report/design/report-design.types.ts +48 -0
  441. package/src/components/report/design/report-design.utils.ts +86 -0
  442. package/src/components/report/panels/report-detail.component.tsx +71 -0
  443. package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
  444. package/src/components/report/report-dashboard.page.component.tsx +554 -0
  445. package/src/components/report/report-editor.page.component.tsx +590 -0
  446. package/src/components/report/report.routes.tsx +32 -0
  447. package/src/components/report-sections/create-report-section-modal.component.tsx +388 -0
  448. package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
  449. package/src/components/report-sections/hooks/useSectionEditorState.ts +449 -0
  450. package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
  451. package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
  452. package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +201 -0
  453. package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
  454. package/src/components/report-sections/report-section-preview-modal.component.tsx +548 -0
  455. package/src/components/report-sections/report-sections-page.component.tsx +325 -0
  456. package/src/components/report-sections/section-disaggregation.utils.ts +637 -0
  457. package/src/components/report-sections/section-types.ts +55 -0
  458. package/src/components/report-sections/section-utils.ts +50 -0
  459. package/src/components/report-sections/sections-page.scss +79 -0
  460. package/src/components/run-reports/run-reports-page.component.tsx +8 -0
  461. package/src/components/security/access-denied-page.component.tsx +64 -0
  462. package/src/components/security/access-denied-page.scss +39 -0
  463. package/src/components/security/privilege-route.component.test.tsx +76 -0
  464. package/src/components/security/privilege-route.component.tsx +39 -0
  465. package/src/components/shared/compile-setup-modal.component.tsx +182 -0
  466. package/src/components/shared/concept-selector.component.tsx +245 -0
  467. package/src/components/shared/concept-selector.scss +121 -0
  468. package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
  469. package/src/components/shared/header/header.component.tsx +85 -0
  470. package/src/components/shared/header/header.scss +151 -0
  471. package/src/components/shared/header/illustration.component.tsx +34 -0
  472. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
  473. package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
  474. package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
  475. package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
  476. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
  477. package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
  478. package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
  479. package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
  480. package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
  481. package/src/config-schema.ts +43 -0
  482. package/src/constants/privileges.test.ts +54 -0
  483. package/src/constants/privileges.ts +118 -0
  484. package/src/features/dashboards/DashboardRendererPage.tsx +174 -0
  485. package/src/features/dashboards/DashboardsPage.tsx +164 -0
  486. package/src/features/dashboards/EtlDashboardPage.tsx +32 -0
  487. package/src/features/dashboards/components/DashboardEmptyState.tsx +41 -0
  488. package/src/features/dashboards/components/DashboardGrid.tsx +48 -0
  489. package/src/features/dashboards/components/DashboardHeader.tsx +58 -0
  490. package/src/features/dashboards/components/DashboardSection.tsx +46 -0
  491. package/src/features/dashboards/components/ReportLaunchCard.tsx +51 -0
  492. package/src/features/dashboards/components/WidgetSlotDispatcher.tsx +61 -0
  493. package/src/features/dashboards/components/WidgetUnavailableCard.tsx +36 -0
  494. package/src/features/dashboards/dashboards.scss +196 -0
  495. package/src/features/dashboards/hooks/useDashboard.ts +61 -0
  496. package/src/features/dashboards/hooks/useDashboardWidgets.ts +74 -0
  497. package/src/features/dashboards/hooks/useDashboards.ts +56 -0
  498. package/src/features/dashboards/hooks/useMonitorDataMap.ts +84 -0
  499. package/src/features/dashboards/index.ts +11 -0
  500. package/src/features/dashboards/utils/dashboard-config.util.test.ts +286 -0
  501. package/src/features/dashboards/utils/dashboard-config.util.ts +382 -0
  502. package/src/features/report-visualizer/ReportVisualizerPage.tsx +448 -0
  503. package/src/features/report-visualizer/components/Export/ExportMenu.tsx +141 -0
  504. package/src/features/report-visualizer/components/Export/index.ts +2 -0
  505. package/src/features/report-visualizer/components/ReportExplorer/QuickFilters.tsx +152 -0
  506. package/src/features/report-visualizer/components/ReportExplorer/ReportCategoryFilter.tsx +54 -0
  507. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerPanel.tsx +201 -0
  508. package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerRail.tsx +202 -0
  509. package/src/features/report-visualizer/components/ReportExplorer/ReportList.tsx +287 -0
  510. package/src/features/report-visualizer/components/ReportExplorer/ReportSearch.tsx +151 -0
  511. package/src/features/report-visualizer/components/ReportExplorer/ReportTagFilter.tsx +100 -0
  512. package/src/features/report-visualizer/components/ReportExplorer/RunReportButton.tsx +42 -0
  513. package/src/features/report-visualizer/components/ReportExplorer/RuntimeParameterForm.tsx +259 -0
  514. package/src/features/report-visualizer/components/ReportExplorer/index.ts +8 -0
  515. package/src/features/report-visualizer/components/ReportWorkspace/ReportLayer.tsx +33 -0
  516. package/src/features/report-visualizer/components/ReportWorkspace/ReportSummary.tsx +88 -0
  517. package/src/features/report-visualizer/components/ReportWorkspace/ReportViewTabs.tsx +72 -0
  518. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspaceHeader.tsx +92 -0
  519. package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspacePanel.tsx +242 -0
  520. package/src/features/report-visualizer/components/ReportWorkspace/index.ts +5 -0
  521. package/src/features/report-visualizer/components/TableView/ColumnVisibilityMenu.tsx +206 -0
  522. package/src/features/report-visualizer/components/TableView/ReportTable.tsx +253 -0
  523. package/src/features/report-visualizer/components/TableView/index.ts +2 -0
  524. package/src/features/report-visualizer/hooks/index.ts +4 -0
  525. package/src/features/report-visualizer/hooks/useExplorerPreferences.ts +103 -0
  526. package/src/features/report-visualizer/hooks/useReportCapabilities.ts +112 -0
  527. package/src/features/report-visualizer/hooks/useReportParameters.ts +152 -0
  528. package/src/features/report-visualizer/hooks/useReports.ts +135 -0
  529. package/src/features/report-visualizer/index.ts +6 -0
  530. package/src/features/report-visualizer/report-visualizer.scss +593 -0
  531. package/src/features/report-visualizer/types/index.ts +1 -0
  532. package/src/features/report-visualizer/types/report-visualizer.types.ts +104 -0
  533. package/src/features/report-visualizer/utils/status-colors.ts +117 -0
  534. package/src/globals.d.ts +9 -0
  535. package/src/hooks/address-template.ts +86 -0
  536. package/src/hooks/concept.ts +112 -0
  537. package/src/hooks/indicator/index.ts +1 -0
  538. package/src/hooks/indicator/useIndicatorsByTheme.ts +41 -0
  539. package/src/hooks/openmrs-reference-data.ts +432 -0
  540. package/src/hooks/theme/index.ts +4 -0
  541. package/src/hooks/theme/useDataTheme.ts +61 -0
  542. package/src/hooks/theme/useDataThemes.ts +28 -0
  543. package/src/hooks/theme/useMambaTableMeta.ts +34 -0
  544. package/src/hooks/theme/useMambaTables.ts +95 -0
  545. package/src/hooks/use-report-builder-privileges.test.tsx +92 -0
  546. package/src/hooks/use-report-builder-privileges.ts +58 -0
  547. package/src/index.ts +29 -0
  548. package/src/resources/address-template/address-template.api.ts +180 -0
  549. package/src/resources/agegroup/agegroups.api.ts +58 -0
  550. package/src/resources/concept/concept.api.ts +116 -0
  551. package/src/resources/concepts/concept-mapper.ts +21 -0
  552. package/src/resources/concepts/concept-types.ts +31 -0
  553. package/src/resources/concepts/concepts.resource.ts +44 -0
  554. package/src/resources/concepts/useConceptSearch.ts +41 -0
  555. package/src/resources/dashboard/dashboard.api.ts +107 -0
  556. package/src/resources/etl-monitor/etl-monitor-test.api.ts +409 -0
  557. package/src/resources/etl-monitor/etl-monitor.api.ts +381 -0
  558. package/src/resources/etl-source/etl-source.api.ts +65 -0
  559. package/src/resources/etl-task/etl-task.api.ts +96 -0
  560. package/src/resources/etl-task/etl-task.types.ts +31 -0
  561. package/src/resources/indicator/indicators.api.ts +125 -0
  562. package/src/resources/legacy-reports/legacy-reports.api.ts +144 -0
  563. package/src/resources/linelist/linelist-reports.api.ts +616 -0
  564. package/src/resources/location/location.api.ts +135 -0
  565. package/src/resources/openmrs-api.ts +40 -0
  566. package/src/resources/patient-identifier-type/patient-identifier-type.api.ts +65 -0
  567. package/src/resources/person-attribute-type/person-attribute-type.api.ts +71 -0
  568. package/src/resources/preview/sql-preview.api.ts +54 -0
  569. package/src/resources/report/reports.api.ts +140 -0
  570. package/src/resources/report-category/report-category.api.ts +41 -0
  571. package/src/resources/report-import-export/import-export-api.ts +164 -0
  572. package/src/resources/report-library/report-library.api.ts +63 -0
  573. package/src/resources/report-section/report-sections.api.ts +73 -0
  574. package/src/resources/report-section/section-preview.api.ts +25 -0
  575. package/src/resources/theme/data-theme.api.ts +88 -0
  576. package/src/resources/theme/etl-schema.api.ts +101 -0
  577. package/src/resources/theme/etl-table-meta.api.ts +45 -0
  578. package/src/root.component.tsx +20 -0
  579. package/src/routes/report-builder.routes.tsx +160 -0
  580. package/src/routes/report-builder.scss +56 -0
  581. package/src/routes.json +31 -0
  582. package/src/sample-template.ts +121 -0
  583. package/src/template-utils.ts +110 -0
  584. package/src/types/carbon-react-inert.d.ts +13 -0
  585. package/src/types/condition-operators.ts +63 -0
  586. package/src/types/dashboard/dashboard.types.ts +137 -0
  587. package/src/types/dashboard/index.ts +4 -0
  588. package/src/types/etl/etl-types.ts +238 -0
  589. package/src/types/etl-monitor/etl-monitor-builder.types.ts +555 -0
  590. package/src/types/etl-monitor/etl-monitor-v2.types.ts +397 -0
  591. package/src/types/etl-monitor/etl-monitor.types.ts +188 -0
  592. package/src/types/etl-monitor/index.ts +6 -0
  593. package/src/types/linelist/compile-config.ts +455 -0
  594. package/src/types/linelist-types.ts +2059 -0
  595. package/src/types/theme/data-theme.types.ts +88 -0
  596. package/src/utils/api-error.utils.test.ts +147 -0
  597. package/src/utils/api-error.utils.ts +131 -0
  598. package/src/utils/config-enhancer.ts +62 -0
  599. package/src/utils/etl-browser/etl-browser.util.test.ts +151 -0
  600. package/src/utils/etl-browser/export-csv.util.ts +43 -0
  601. package/src/utils/etl-browser/group-tables.util.ts +84 -0
  602. package/src/utils/etl-browser/index.ts +4 -0
  603. package/src/utils/etl-browser/summarize-columns.util.ts +27 -0
  604. package/src/utils/etl-browser/table-sql.util.ts +20 -0
  605. package/src/utils/etl-monitor/config-v2-adapter.util.ts +460 -0
  606. package/src/utils/etl-monitor/endpoint-config-normalizer.util.ts +383 -0
  607. package/src/utils/etl-monitor/index.ts +10 -0
  608. package/src/utils/etl-monitor/jsonpath-resolver.util.ts +118 -0
  609. package/src/utils/etl-monitor/normalization-examples.ts +0 -0
  610. package/src/utils/etl-monitor/schema-inspector.util.ts +76 -0
  611. package/src/utils/etl-monitor/semantic-type-detector.util.ts +76 -0
  612. package/src/utils/etl-monitor/status-mapper.util.ts +105 -0
  613. package/src/utils/etl-monitor/value-formatters.util.ts +167 -0
  614. package/src/utils/etl-task/etl-task-card.util.test.ts +240 -0
  615. package/src/utils/etl-task/etl-task-card.util.ts +276 -0
  616. package/src/utils/etl-task/index.ts +1 -0
  617. package/src/utils/html-entities.utils.ts +34 -0
  618. package/src/utils/parameter-resolution.ts +218 -0
  619. package/src/views/etl-monitor-builder/add-monitor-modal.component.scss +0 -0
  620. package/src/views/etl-monitor-builder/add-monitor-modal.component.tsx +210 -0
  621. package/src/views/etl-monitor-builder/etl-monitor-builder.component.scss +157 -0
  622. package/src/views/etl-monitor-builder/etl-monitor-builder.component.tsx +252 -0
  623. package/src/views/etl-monitor-builder/etl-monitor-builder.store.ts +126 -0
  624. package/tools/i18next-parser.config.js +0 -0
  625. package/tools/setup-tests.ts +1 -0
  626. package/tools/update-openmrs-deps.mjs +42 -0
  627. package/translations/am.json +65 -0
  628. package/translations/ar.json +65 -0
  629. package/translations/ar_SY.json +65 -0
  630. package/translations/bn.json +65 -0
  631. package/translations/cs.json +65 -0
  632. package/translations/de.json +65 -0
  633. package/translations/en.json +65 -0
  634. package/translations/en_US.json +65 -0
  635. package/translations/es.json +65 -0
  636. package/translations/es_MX.json +65 -0
  637. package/translations/fr.json +65 -0
  638. package/translations/he.json +65 -0
  639. package/translations/hi.json +65 -0
  640. package/translations/hi_IN.json +65 -0
  641. package/translations/id.json +65 -0
  642. package/translations/it.json +65 -0
  643. package/translations/ka.json +65 -0
  644. package/translations/km.json +65 -0
  645. package/translations/ku.json +65 -0
  646. package/translations/ky.json +65 -0
  647. package/translations/lg.json +65 -0
  648. package/translations/ne.json +65 -0
  649. package/translations/pl.json +65 -0
  650. package/translations/pt.json +65 -0
  651. package/translations/pt_BR.json +65 -0
  652. package/translations/qu.json +65 -0
  653. package/translations/ro_RO.json +65 -0
  654. package/translations/ru_RU.json +65 -0
  655. package/translations/si.json +65 -0
  656. package/translations/sq.json +65 -0
  657. package/translations/sw.json +65 -0
  658. package/translations/sw_KE.json +65 -0
  659. package/translations/tr.json +65 -0
  660. package/translations/tr_TR.json +65 -0
  661. package/translations/uk.json +65 -0
  662. package/translations/uz.json +65 -0
  663. package/translations/uz@Latn.json +65 -0
  664. package/translations/uz_UZ.json +65 -0
  665. package/translations/vi.json +65 -0
  666. package/translations/zh.json +65 -0
  667. package/translations/zh_CN.json +65 -0
  668. package/translations/zh_TW.json +65 -0
  669. package/tsconfig.json +24 -0
  670. package/vitest.config.mts +42 -0
@@ -0,0 +1,985 @@
1
+ /**
2
+ * Unit tests for the population SQL compiler.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import {
7
+ compilePopulationSql,
8
+ generateScalarCountSql,
9
+ generateAgeSexDisaggregationSql,
10
+ clearCompilationCache,
11
+ CircularDependencyError,
12
+ MissingReferenceError,
13
+ } from './population-sql.compiler';
14
+ import type { IndicatorDto } from '../../../resources/indicator/indicators.api';
15
+
16
+ // Mock indicator factory
17
+ function createIndicator(overrides: Partial<IndicatorDto> = {}): IndicatorDto {
18
+ return {
19
+ uuid: 'test-uuid',
20
+ name: 'Test Indicator',
21
+ code: 'TEST',
22
+ kind: 'BASE',
23
+ configJson: null,
24
+ sqlTemplate: null,
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ // Mock getIndicator function
30
+ function createMockGetIndicator(indicators: Map<string, IndicatorDto>) {
31
+ return async (uuid: string): Promise<IndicatorDto | null> => {
32
+ return indicators.get(uuid) || null;
33
+ };
34
+ }
35
+
36
+ describe('Population SQL Compiler', () => {
37
+ beforeEach(() => {
38
+ clearCompilationCache();
39
+ });
40
+
41
+ describe('Base Indicator Population', () => {
42
+ it('should extract population SQL from a base indicator', async () => {
43
+ const indicator = createIndicator({
44
+ kind: 'BASE',
45
+ sqlTemplate: 'SELECT DISTINCT a.patient_id FROM table_a a WHERE a.date >= :startDate',
46
+ });
47
+
48
+ const getIndicator = createMockGetIndicator(new Map());
49
+ const result = await compilePopulationSql(indicator, getIndicator);
50
+
51
+ expect(result.sql).toContain('SELECT DISTINCT');
52
+ expect(result.sql).toContain('patient_id');
53
+ expect(result.sql).toContain(':startDate');
54
+ });
55
+
56
+ it('should convert COUNT SQL to population SQL', async () => {
57
+ const indicator = createIndicator({
58
+ kind: 'BASE',
59
+ sqlTemplate: 'SELECT COUNT(*) AS total FROM table_a a WHERE a.date >= :startDate',
60
+ });
61
+
62
+ const getIndicator = createMockGetIndicator(new Map());
63
+ const result = await compilePopulationSql(indicator, getIndicator);
64
+
65
+ expect(result.sql).toContain('SELECT DISTINCT');
66
+ expect(result.sql).not.toContain('COUNT(*)');
67
+ });
68
+
69
+ it('should fix :stratDate typo to :startDate', async () => {
70
+ const indicator = createIndicator({
71
+ kind: 'BASE',
72
+ sqlTemplate: 'SELECT DISTINCT a.patient_id FROM table_a a WHERE a.date >= :stratDate',
73
+ });
74
+
75
+ const getIndicator = createMockGetIndicator(new Map());
76
+ const result = await compilePopulationSql(indicator, getIndicator);
77
+
78
+ expect(result.sql).toContain(':startDate');
79
+ expect(result.sql).not.toContain(':stratDate');
80
+ });
81
+
82
+ it('should handle population SQL with config_json source', async () => {
83
+ const config = {
84
+ version: 1,
85
+ sqlPreview: 'SELECT DISTINCT patient_id FROM patients WHERE active = 1',
86
+ };
87
+
88
+ const indicator = createIndicator({
89
+ kind: 'BASE',
90
+ configJson: JSON.stringify(config),
91
+ sqlTemplate: null,
92
+ });
93
+
94
+ const getIndicator = createMockGetIndicator(new Map());
95
+ const result = await compilePopulationSql(indicator, getIndicator);
96
+
97
+ expect(result.sql).toContain('SELECT DISTINCT patient_id');
98
+ });
99
+ });
100
+
101
+ describe('Composite Indicator - AND Operator', () => {
102
+ it('should compile AND composite indicator with INNER JOIN', async () => {
103
+ // Create two base indicators
104
+ const indicatorA = createIndicator({
105
+ uuid: 'indicator-a',
106
+ code: 'A',
107
+ kind: 'BASE',
108
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_a WHERE condition_a = 1',
109
+ });
110
+
111
+ const indicatorB = createIndicator({
112
+ uuid: 'indicator-b',
113
+ code: 'B',
114
+ kind: 'BASE',
115
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_b WHERE condition_b = 1',
116
+ });
117
+
118
+ // Create composite indicator
119
+ const composite = createIndicator({
120
+ uuid: 'composite-and',
121
+ code: 'A_AND_B',
122
+ kind: 'COMPOSITE',
123
+ configJson: JSON.stringify({
124
+ version: 1,
125
+ unit: 'Patients',
126
+ operator: 'AND',
127
+ indicatorAId: 'indicator-a',
128
+ indicatorBId: 'indicator-b',
129
+ }),
130
+ });
131
+
132
+ const indicators = new Map([
133
+ ['indicator-a', indicatorA],
134
+ ['indicator-b', indicatorB],
135
+ ]);
136
+ const getIndicator = createMockGetIndicator(indicators);
137
+
138
+ const result = await compilePopulationSql(composite, getIndicator);
139
+
140
+ expect(result.sql).toContain('WITH A AS');
141
+ expect(result.sql).toMatch(/B AS \(/); // B AS ( appears after A, without WITH
142
+ expect(result.sql).toContain('INNER JOIN B');
143
+ expect(result.sql).toContain('ON B.patient_id = A.patient_id');
144
+ expect(result.sql).toMatch(/SELECT DISTINCT\s+A\.patient_id/); // Handles multi-line SELECT
145
+ });
146
+
147
+ it('should compile nested composite indicators correctly', async () => {
148
+ // HC01 = enrollment A_AND_NOT_B transfer-in
149
+ const enrollment = createIndicator({
150
+ uuid: 'enrollment',
151
+ code: 'ENROLLMENT',
152
+ kind: 'BASE',
153
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM enrollment WHERE date >= :startDate',
154
+ });
155
+
156
+ const transferIn = createIndicator({
157
+ uuid: 'transfer-in',
158
+ code: 'TRANSFER_IN',
159
+ kind: 'BASE',
160
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM transfer_in WHERE date >= :startDate',
161
+ });
162
+
163
+ const hc01 = createIndicator({
164
+ uuid: 'hc01',
165
+ code: 'HC01',
166
+ kind: 'COMPOSITE',
167
+ configJson: JSON.stringify({
168
+ version: 1,
169
+ unit: 'Patients',
170
+ operator: 'A_AND_NOT_B',
171
+ indicatorAId: 'enrollment',
172
+ indicatorBId: 'transfer-in',
173
+ }),
174
+ });
175
+
176
+ const tbAssessed = createIndicator({
177
+ uuid: 'tb-assessed',
178
+ code: 'TB_ASSESSED',
179
+ kind: 'BASE',
180
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM tb_assessment WHERE assessed = 1',
181
+ });
182
+
183
+ // HC02A = HC01 AND TB assessed
184
+ const hc02a = createIndicator({
185
+ uuid: 'hc02a',
186
+ code: 'HC02A',
187
+ kind: 'COMPOSITE',
188
+ configJson: JSON.stringify({
189
+ version: 1,
190
+ unit: 'Patients',
191
+ operator: 'AND',
192
+ indicatorAId: 'hc01',
193
+ indicatorBId: 'tb-assessed',
194
+ }),
195
+ });
196
+
197
+ const indicators = new Map([
198
+ ['enrollment', enrollment],
199
+ ['transfer-in', transferIn],
200
+ ['hc01', hc01],
201
+ ['tb-assessed', tbAssessed],
202
+ ]);
203
+ const getIndicator = createMockGetIndicator(indicators);
204
+
205
+ const result = await compilePopulationSql(hc02a, getIndicator);
206
+
207
+ // Should contain nested CTE structure
208
+ expect(result.sql).toContain('WITH A AS');
209
+
210
+ // Should have INNER JOIN for AND
211
+ expect(result.sql).toContain('INNER JOIN B');
212
+
213
+ // Should have LEFT JOIN for A_AND_NOT_B
214
+ expect(result.sql).toContain('LEFT JOIN B');
215
+ expect(result.sql).toContain('WHERE B.patient_id IS NULL');
216
+
217
+ // Should not contain COUNT(*)
218
+ expect(result.sql).not.toContain('COUNT(*)');
219
+
220
+ // Leaf source columns keep patient_id, but the population output contract is patient_id
221
+ expect(result.sql).toMatch(/patient_id/g);
222
+ expect(result.sql).toMatch(/AS patient_id/);
223
+ });
224
+ });
225
+
226
+ describe('Composite Indicator - A_AND_NOT_B Operator', () => {
227
+ it('should compile A_AND_NOT_B with LEFT JOIN and IS NULL', async () => {
228
+ const indicatorA = createIndicator({
229
+ uuid: 'indicator-a',
230
+ code: 'A',
231
+ kind: 'BASE',
232
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_a',
233
+ });
234
+
235
+ const indicatorB = createIndicator({
236
+ uuid: 'indicator-b',
237
+ code: 'B',
238
+ kind: 'BASE',
239
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_b',
240
+ });
241
+
242
+ const composite = createIndicator({
243
+ uuid: 'composite-not',
244
+ code: 'A_NOT_B',
245
+ kind: 'COMPOSITE',
246
+ configJson: JSON.stringify({
247
+ version: 1,
248
+ unit: 'Patients',
249
+ operator: 'A_AND_NOT_B',
250
+ indicatorAId: 'indicator-a',
251
+ indicatorBId: 'indicator-b',
252
+ }),
253
+ });
254
+
255
+ const indicators = new Map([
256
+ ['indicator-a', indicatorA],
257
+ ['indicator-b', indicatorB],
258
+ ]);
259
+ const getIndicator = createMockGetIndicator(indicators);
260
+
261
+ const result = await compilePopulationSql(composite, getIndicator);
262
+
263
+ expect(result.sql).toContain('LEFT JOIN B');
264
+ expect(result.sql).toContain('WHERE B.patient_id IS NULL');
265
+ });
266
+
267
+ it('should normalize qualified leaf columns (a.encounter_id AS encounter_id) to the patient_id contract', async () => {
268
+ // Mirrors real saved indicators, which use qualified refs with an existing alias
269
+ const indicatorA = createIndicator({
270
+ uuid: 'indicator-a',
271
+ code: 'A',
272
+ kind: 'BASE',
273
+ sqlTemplate: 'SELECT DISTINCT a.encounter_id AS encounter_id FROM fact_a a WHERE a.x = 1',
274
+ });
275
+
276
+ const indicatorB = createIndicator({
277
+ uuid: 'indicator-b',
278
+ code: 'B',
279
+ kind: 'BASE',
280
+ sqlTemplate: 'SELECT DISTINCT b.encounter_id AS encounter_id FROM fact_b b WHERE b.y = 2',
281
+ });
282
+
283
+ const composite = createIndicator({
284
+ uuid: 'composite-not-qualified',
285
+ code: 'A_NOT_B_QUALIFIED',
286
+ kind: 'COMPOSITE',
287
+ configJson: JSON.stringify({
288
+ version: 1,
289
+ unit: 'Patients',
290
+ operator: 'A_AND_NOT_B',
291
+ indicatorAId: 'indicator-a',
292
+ indicatorBId: 'indicator-b',
293
+ }),
294
+ });
295
+
296
+ const indicators = new Map([
297
+ ['indicator-a', indicatorA],
298
+ ['indicator-b', indicatorB],
299
+ ]);
300
+ const getIndicator = createMockGetIndicator(indicators);
301
+
302
+ const result = await compilePopulationSql(composite, getIndicator);
303
+
304
+ // Leaves are aliased to the contract column...
305
+ expect(result.sql).toMatch(/SELECT DISTINCT a\.encounter_id AS patient_id/);
306
+ expect(result.sql).toMatch(/SELECT DISTINCT b\.encounter_id AS patient_id/);
307
+
308
+ // ...and every CTE reference uses it — no dangling encounter_id refs on A/B
309
+ expect(result.sql).toContain('ON B.patient_id = A.patient_id');
310
+ expect(result.sql).toContain('WHERE B.patient_id IS NULL');
311
+ expect(result.sql).not.toMatch(/\b[AB]\.encounter_id\b/);
312
+ });
313
+ });
314
+
315
+ describe('Composite Indicator - OR Operator', () => {
316
+ it('should compile OR with UNION', async () => {
317
+ const indicatorA = createIndicator({
318
+ uuid: 'indicator-a',
319
+ code: 'A',
320
+ kind: 'BASE',
321
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_a',
322
+ });
323
+
324
+ const indicatorB = createIndicator({
325
+ uuid: 'indicator-b',
326
+ code: 'B',
327
+ kind: 'BASE',
328
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_b',
329
+ });
330
+
331
+ const composite = createIndicator({
332
+ uuid: 'composite-or',
333
+ code: 'A_OR_B',
334
+ kind: 'COMPOSITE',
335
+ configJson: JSON.stringify({
336
+ version: 1,
337
+ unit: 'Patients',
338
+ operator: 'OR',
339
+ indicatorAId: 'indicator-a',
340
+ indicatorBId: 'indicator-b',
341
+ }),
342
+ });
343
+
344
+ const indicators = new Map([
345
+ ['indicator-a', indicatorA],
346
+ ['indicator-b', indicatorB],
347
+ ]);
348
+ const getIndicator = createMockGetIndicator(indicators);
349
+
350
+ const result = await compilePopulationSql(composite, getIndicator);
351
+
352
+ expect(result.sql).toContain('UNION');
353
+ expect(result.sql).toContain('SELECT patient_id FROM A');
354
+ expect(result.sql).toContain('SELECT patient_id FROM B');
355
+ });
356
+ });
357
+
358
+ describe('Circular Dependency Detection', () => {
359
+ it('should detect direct circular dependency (A -> B -> A)', async () => {
360
+ const indicatorA = createIndicator({
361
+ uuid: 'indicator-a',
362
+ code: 'A',
363
+ kind: 'COMPOSITE',
364
+ configJson: JSON.stringify({
365
+ version: 1,
366
+ unit: 'Patients',
367
+ operator: 'AND',
368
+ indicatorAId: 'indicator-b',
369
+ indicatorBId: 'some-base',
370
+ }),
371
+ });
372
+
373
+ const indicatorB = createIndicator({
374
+ uuid: 'indicator-b',
375
+ code: 'B',
376
+ kind: 'COMPOSITE',
377
+ configJson: JSON.stringify({
378
+ version: 1,
379
+ unit: 'Patients',
380
+ operator: 'AND',
381
+ indicatorAId: 'indicator-a',
382
+ indicatorBId: 'some-other-base',
383
+ }),
384
+ });
385
+
386
+ const someBase = createIndicator({
387
+ uuid: 'some-base',
388
+ code: 'BASE',
389
+ kind: 'BASE',
390
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM base',
391
+ });
392
+
393
+ const someOtherBase = createIndicator({
394
+ uuid: 'some-other-base',
395
+ code: 'OTHER',
396
+ kind: 'BASE',
397
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM other',
398
+ });
399
+
400
+ const indicators = new Map([
401
+ ['indicator-a', indicatorA],
402
+ ['indicator-b', indicatorB],
403
+ ['some-base', someBase],
404
+ ['some-other-base', someOtherBase],
405
+ ]);
406
+ const getIndicator = createMockGetIndicator(indicators);
407
+
408
+ await expect(compilePopulationSql(indicatorA, getIndicator)).rejects.toThrow(CircularDependencyError);
409
+ });
410
+
411
+ it('should detect deep circular dependency (A -> B -> C -> A)', async () => {
412
+ const baseA = createIndicator({
413
+ uuid: 'base-a',
414
+ code: 'BASE_A',
415
+ kind: 'BASE',
416
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM base_a',
417
+ });
418
+
419
+ const indicatorC = createIndicator({
420
+ uuid: 'indicator-c',
421
+ code: 'C',
422
+ kind: 'COMPOSITE',
423
+ configJson: JSON.stringify({
424
+ version: 1,
425
+ unit: 'Patients',
426
+ operator: 'AND',
427
+ indicatorAId: 'indicator-a',
428
+ indicatorBId: 'base-a',
429
+ }),
430
+ });
431
+
432
+ const indicatorB = createIndicator({
433
+ uuid: 'indicator-b',
434
+ code: 'B',
435
+ kind: 'COMPOSITE',
436
+ configJson: JSON.stringify({
437
+ version: 1,
438
+ unit: 'Patients',
439
+ operator: 'AND',
440
+ indicatorAId: 'indicator-c',
441
+ indicatorBId: 'base-a',
442
+ }),
443
+ });
444
+
445
+ const indicatorA = createIndicator({
446
+ uuid: 'indicator-a',
447
+ code: 'A',
448
+ kind: 'COMPOSITE',
449
+ configJson: JSON.stringify({
450
+ version: 1,
451
+ unit: 'Patients',
452
+ operator: 'AND',
453
+ indicatorAId: 'indicator-b',
454
+ indicatorBId: 'base-a',
455
+ }),
456
+ });
457
+
458
+ const indicators = new Map([
459
+ ['indicator-a', indicatorA],
460
+ ['indicator-b', indicatorB],
461
+ ['indicator-c', indicatorC],
462
+ ['base-a', baseA],
463
+ ]);
464
+ const getIndicator = createMockGetIndicator(indicators);
465
+
466
+ await expect(compilePopulationSql(indicatorA, getIndicator)).rejects.toThrow(CircularDependencyError);
467
+ });
468
+ });
469
+
470
+ describe('Missing Reference Validation', () => {
471
+ it('should throw error for missing indicatorAId', async () => {
472
+ const composite = createIndicator({
473
+ uuid: 'composite',
474
+ code: 'COMP',
475
+ kind: 'COMPOSITE',
476
+ configJson: JSON.stringify({
477
+ version: 1,
478
+ unit: 'Patients',
479
+ operator: 'AND',
480
+ indicatorBId: 'indicator-b',
481
+ }),
482
+ });
483
+
484
+ const getIndicator = createMockGetIndicator(new Map());
485
+
486
+ await expect(compilePopulationSql(composite, getIndicator)).rejects.toThrow(MissingReferenceError);
487
+ });
488
+
489
+ it('should throw error for missing indicatorBId', async () => {
490
+ const indicatorA = createIndicator({
491
+ uuid: 'indicator-a',
492
+ code: 'A',
493
+ kind: 'BASE',
494
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_a',
495
+ });
496
+
497
+ const composite = createIndicator({
498
+ uuid: 'composite',
499
+ code: 'COMP',
500
+ kind: 'COMPOSITE',
501
+ configJson: JSON.stringify({
502
+ version: 1,
503
+ unit: 'Patients',
504
+ operator: 'AND',
505
+ indicatorAId: 'indicator-a',
506
+ }),
507
+ });
508
+
509
+ const indicators = new Map([['indicator-a', indicatorA]]);
510
+ const getIndicator = createMockGetIndicator(indicators);
511
+
512
+ await expect(compilePopulationSql(composite, getIndicator)).rejects.toThrow(MissingReferenceError);
513
+ });
514
+
515
+ it('should throw error for non-existent indicatorAId', async () => {
516
+ const composite = createIndicator({
517
+ uuid: 'composite',
518
+ code: 'COMP',
519
+ kind: 'COMPOSITE',
520
+ configJson: JSON.stringify({
521
+ version: 1,
522
+ unit: 'Patients',
523
+ operator: 'AND',
524
+ indicatorAId: 'non-existent',
525
+ indicatorBId: 'indicator-b',
526
+ }),
527
+ });
528
+
529
+ const indicatorB = createIndicator({
530
+ uuid: 'indicator-b',
531
+ code: 'B',
532
+ kind: 'BASE',
533
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_b',
534
+ });
535
+
536
+ const indicators = new Map([['indicator-b', indicatorB]]);
537
+ const getIndicator = createMockGetIndicator(indicators);
538
+
539
+ await expect(compilePopulationSql(composite, getIndicator)).rejects.toThrow(MissingReferenceError);
540
+ });
541
+ });
542
+
543
+ describe('Unsupported Operator', () => {
544
+ it('should throw error for unsupported operator', async () => {
545
+ const indicatorA = createIndicator({
546
+ uuid: 'indicator-a',
547
+ code: 'A',
548
+ kind: 'BASE',
549
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_a',
550
+ });
551
+
552
+ const indicatorB = createIndicator({
553
+ uuid: 'indicator-b',
554
+ code: 'B',
555
+ kind: 'BASE',
556
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table_b',
557
+ });
558
+
559
+ const composite = createIndicator({
560
+ uuid: 'composite',
561
+ code: 'COMP',
562
+ kind: 'COMPOSITE',
563
+ configJson: JSON.stringify({
564
+ version: 1,
565
+ unit: 'Patients',
566
+ operator: 'XOR' as any, // Invalid operator
567
+ indicatorAId: 'indicator-a',
568
+ indicatorBId: 'indicator-b',
569
+ }),
570
+ });
571
+
572
+ const indicators = new Map([
573
+ ['indicator-a', indicatorA],
574
+ ['indicator-b', indicatorB],
575
+ ]);
576
+ const getIndicator = createMockGetIndicator(indicators);
577
+
578
+ await expect(compilePopulationSql(composite, getIndicator)).rejects.toThrow(Error);
579
+ });
580
+ });
581
+
582
+ describe('Retired Indicator Handling', () => {
583
+ it('should throw error when using retired indicator by default', async () => {
584
+ const retiredIndicator = createIndicator({
585
+ uuid: 'retired',
586
+ code: 'RETIRED',
587
+ kind: 'BASE',
588
+ retired: true,
589
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM retired_table',
590
+ });
591
+
592
+ const composite = createIndicator({
593
+ uuid: 'composite',
594
+ code: 'COMP',
595
+ kind: 'COMPOSITE',
596
+ configJson: JSON.stringify({
597
+ version: 1,
598
+ unit: 'Patients',
599
+ operator: 'AND',
600
+ indicatorAId: 'retired',
601
+ indicatorBId: 'active',
602
+ }),
603
+ });
604
+
605
+ const activeIndicator = createIndicator({
606
+ uuid: 'active',
607
+ code: 'ACTIVE',
608
+ kind: 'BASE',
609
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM active_table',
610
+ });
611
+
612
+ const indicators = new Map([
613
+ ['retired', retiredIndicator],
614
+ ['active', activeIndicator],
615
+ ]);
616
+ const getIndicator = createMockGetIndicator(indicators);
617
+
618
+ await expect(compilePopulationSql(composite, getIndicator)).rejects.toThrow('Cannot use retired indicator');
619
+ });
620
+
621
+ it('should allow retired indicator when allowRetired is true', async () => {
622
+ const retiredIndicator = createIndicator({
623
+ uuid: 'retired',
624
+ code: 'RETIRED',
625
+ kind: 'BASE',
626
+ retired: true,
627
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM retired_table',
628
+ });
629
+
630
+ const activeIndicator = createIndicator({
631
+ uuid: 'active',
632
+ code: 'ACTIVE',
633
+ kind: 'BASE',
634
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM active_table',
635
+ });
636
+
637
+ const composite = createIndicator({
638
+ uuid: 'composite',
639
+ code: 'COMP',
640
+ kind: 'COMPOSITE',
641
+ configJson: JSON.stringify({
642
+ version: 1,
643
+ unit: 'Patients',
644
+ operator: 'AND',
645
+ indicatorAId: 'retired',
646
+ indicatorBId: 'active',
647
+ }),
648
+ });
649
+
650
+ const indicators = new Map([
651
+ ['retired', retiredIndicator],
652
+ ['active', activeIndicator],
653
+ ]);
654
+ const getIndicator = createMockGetIndicator(indicators);
655
+
656
+ const result = await compilePopulationSql(composite, getIndicator, new Set(), { allowRetired: true });
657
+
658
+ expect(result.sql).toContain('WITH A AS');
659
+ expect(result.warnings).toContain('Using retired indicator: RETIRED');
660
+ });
661
+ });
662
+
663
+ describe('Scalar Count SQL Generation', () => {
664
+ it('should wrap population SQL in COUNT query', () => {
665
+ const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
666
+
667
+ const scalarSql = generateScalarCountSql(populationSql);
668
+
669
+ expect(scalarSql).toContain('WITH base_population AS');
670
+ expect(scalarSql).toContain('COUNT(DISTINCT patient_id) AS total');
671
+ expect(scalarSql).toContain('FROM base_population');
672
+ });
673
+ });
674
+
675
+ describe('Age/Sex Disaggregation SQL Generation', () => {
676
+ it('should generate disaggregation SQL with correct structure', () => {
677
+ const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
678
+
679
+ const disaggSql = generateAgeSexDisaggregationSql({
680
+ populationSql,
681
+ ageCategoryCode: 'HMIS_106A',
682
+ genders: ['F', 'M'],
683
+ });
684
+
685
+ expect(disaggSql).toContain('WITH base_pop AS');
686
+ expect(disaggSql).toContain('ag AS (');
687
+ expect(disaggSql).toContain('report_builder_dim_age_group');
688
+ expect(disaggSql).toContain("ac.code = 'HMIS_106A'");
689
+ expect(disaggSql).toContain('genders AS (');
690
+ expect(disaggSql).toContain("SELECT 'F' AS gender");
691
+ expect(disaggSql).toContain('cnt AS (');
692
+ expect(disaggSql).toContain('COUNT(DISTINCT base_pop.patient_id) AS value');
693
+ expect(disaggSql).toContain('CROSS JOIN genders g');
694
+ expect(disaggSql).toContain('LEFT JOIN cnt');
695
+ expect(disaggSql).toContain('COALESCE(cnt.value, 0)');
696
+ });
697
+
698
+ it('should handle only female gender', () => {
699
+ const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
700
+
701
+ const disaggSql = generateAgeSexDisaggregationSql({
702
+ populationSql,
703
+ ageCategoryCode: 'HTS',
704
+ genders: ['F'],
705
+ });
706
+
707
+ expect(disaggSql).toContain("'F'");
708
+ });
709
+ });
710
+
711
+ describe('Edge Cases', () => {
712
+ it('should handle empty genders array by defaulting to both', () => {
713
+ const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
714
+
715
+ const disaggSql = generateAgeSexDisaggregationSql({
716
+ populationSql,
717
+ ageCategoryCode: 'HMIS_106A',
718
+ genders: [],
719
+ });
720
+
721
+ // Should default to both genders
722
+ expect(disaggSql).toContain("'F'");
723
+ expect(disaggSql).toContain("'M'");
724
+ });
725
+
726
+ it('should escape single quotes in age category code', () => {
727
+ const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
728
+
729
+ const disaggSql = generateAgeSexDisaggregationSql({
730
+ populationSql,
731
+ ageCategoryCode: "O'Malley",
732
+ genders: ['F'],
733
+ });
734
+
735
+ expect(disaggSql).toContain("ac.code = 'O''Malley'");
736
+ });
737
+ });
738
+
739
+ describe('Caching', () => {
740
+ it('should cache compilation results', async () => {
741
+ const indicator = createIndicator({
742
+ uuid: 'cached-indicator',
743
+ code: 'CACHED',
744
+ kind: 'BASE',
745
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table',
746
+ });
747
+
748
+ const getIndicator = createMockGetIndicator(new Map());
749
+
750
+ // First call
751
+ const result1 = await compilePopulationSql(indicator, getIndicator);
752
+
753
+ // Clear the mock to ensure it's not called again
754
+ const newGetIndicator = createMockGetIndicator(new Map());
755
+
756
+ // Second call with different getIndicator (should use cache)
757
+ const result2 = await compilePopulationSql(indicator, newGetIndicator);
758
+
759
+ expect(result1.sql).toBe(result2.sql);
760
+ });
761
+
762
+ it('should clear cache when requested', async () => {
763
+ const indicator = createIndicator({
764
+ uuid: 'test-uuid',
765
+ code: 'TEST',
766
+ kind: 'BASE',
767
+ sqlTemplate: 'SELECT DISTINCT patient_id FROM table',
768
+ });
769
+
770
+ const getIndicator = createMockGetIndicator(new Map());
771
+
772
+ await compilePopulationSql(indicator, getIndicator);
773
+ clearCompilationCache();
774
+
775
+ // Should re-compile after cache clear
776
+ const result = await compilePopulationSql(indicator, getIndicator);
777
+
778
+ expect(result.sql).toContain('SELECT DISTINCT');
779
+ });
780
+ });
781
+
782
+ describe('Population SQL Validation - Regression Tests', () => {
783
+ it('should validate that population SQL exposes patient ID column', async () => {
784
+ const indicator = createIndicator({
785
+ kind: 'BASE',
786
+ sqlTemplate: 'SELECT DISTINCT a.patient_id FROM table_a a WHERE a.date >= :startDate',
787
+ });
788
+
789
+ const getIndicator = createMockGetIndicator(new Map());
790
+ const result = await compilePopulationSql(indicator, getIndicator);
791
+
792
+ // Should not have an error
793
+ expect(result.error).toBeUndefined();
794
+ // Should expose patient_id
795
+ expect(result.sql).toContain('patient_id');
796
+ });
797
+
798
+ it('should handle cache invalidation when SQL changes', async () => {
799
+ const indicator = createIndicator({
800
+ uuid: 'test-cache-invalidation',
801
+ code: 'CACHE_TEST',
802
+ kind: 'BASE',
803
+ sqlTemplate: 'SELECT COUNT(*) AS total FROM table_a a WHERE a.date >= :startDate',
804
+ });
805
+
806
+ const getIndicator = createMockGetIndicator(new Map());
807
+
808
+ // First compilation
809
+ const result1 = await compilePopulationSql(indicator, getIndicator);
810
+ expect(result1.sql).toContain('SELECT DISTINCT');
811
+
812
+ // Update the indicator SQL
813
+ indicator.sqlTemplate = 'SELECT COUNT(*) AS total FROM table_b b WHERE b.date >= :startDate';
814
+
815
+ // Second compilation should use fresh SQL (not cached)
816
+ const result2 = await compilePopulationSql(indicator, getIndicator);
817
+ expect(result2.sql).toContain('table_b');
818
+ expect(result2.sql).toContain('SELECT DISTINCT');
819
+ });
820
+
821
+ it('should include error when COUNT SQL cannot be converted properly', async () => {
822
+ // Create an indicator with COUNT SQL that should be converted to population SQL
823
+ const indicator = createIndicator({
824
+ kind: 'BASE',
825
+ sqlTemplate: 'SELECT COUNT(DISTINCT a.patient_id) AS total FROM table_a a',
826
+ });
827
+
828
+ const getIndicator = createMockGetIndicator(new Map());
829
+ const result = await compilePopulationSql(indicator, getIndicator);
830
+
831
+ // The conversion should work and produce SELECT DISTINCT normalized to patient_id
832
+ expect(result.sql).toContain('SELECT DISTINCT');
833
+ expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
834
+ });
835
+
836
+ it('should handle composite indicators with validation', async () => {
837
+ const indicatorA = createIndicator({
838
+ uuid: 'indicator-a',
839
+ code: 'A',
840
+ kind: 'BASE',
841
+ sqlTemplate: 'SELECT DISTINCT a.patient_id FROM table_a a WHERE condition_a = 1',
842
+ });
843
+
844
+ const indicatorB = createIndicator({
845
+ uuid: 'indicator-b',
846
+ code: 'B',
847
+ kind: 'BASE',
848
+ sqlTemplate: 'SELECT DISTINCT b.patient_id FROM table_b b WHERE condition_b = 1',
849
+ });
850
+
851
+ const composite = createIndicator({
852
+ uuid: 'composite-test',
853
+ code: 'A_AND_B',
854
+ kind: 'COMPOSITE',
855
+ configJson: JSON.stringify({
856
+ version: 1,
857
+ unit: 'Patients',
858
+ operator: 'AND',
859
+ indicatorAId: 'indicator-a',
860
+ indicatorBId: 'indicator-b',
861
+ }),
862
+ });
863
+
864
+ const indicators = new Map([
865
+ ['indicator-a', indicatorA],
866
+ ['indicator-b', indicatorB],
867
+ ]);
868
+ const getIndicator = createMockGetIndicator(indicators);
869
+
870
+ const result = await compilePopulationSql(composite, getIndicator);
871
+
872
+ // Should not have validation errors
873
+ expect(result.error).toBeUndefined();
874
+ // Should contain population SQL
875
+ expect(result.sql).toContain('SELECT DISTINCT');
876
+ // Should not contain COUNT(*)
877
+ expect(result.sql).not.toContain('COUNT(*)');
878
+ });
879
+
880
+ it('should convert COUNT(DISTINCT a.patient_id) to population SQL normalized to patient_id', async () => {
881
+ const indicator = createIndicator({
882
+ kind: 'BASE',
883
+ sqlTemplate: 'SELECT COUNT(DISTINCT a.patient_id) AS total FROM table_a a',
884
+ });
885
+
886
+ const getIndicator = createMockGetIndicator(new Map());
887
+ const result = await compilePopulationSql(indicator, getIndicator);
888
+
889
+ // Should convert to SELECT DISTINCT and normalize to patient_id
890
+ expect(result.sql).toContain('SELECT DISTINCT');
891
+ expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
892
+ expect(result.sql).not.toContain('COUNT(DISTINCT');
893
+ expect(result.sql).not.toContain('AS total');
894
+ });
895
+
896
+ it('should convert COUNT(DISTINCT a.patient_id) with WHERE clause', async () => {
897
+ const indicator = createIndicator({
898
+ kind: 'BASE',
899
+ sqlTemplate: 'SELECT COUNT(DISTINCT a.patient_id) AS total FROM table_a a WHERE a.active = 1',
900
+ });
901
+
902
+ const getIndicator = createMockGetIndicator(new Map());
903
+ const result = await compilePopulationSql(indicator, getIndicator);
904
+
905
+ // Should convert to SELECT DISTINCT, preserve WHERE clause, and normalize to patient_id
906
+ expect(result.sql).toContain('SELECT DISTINCT');
907
+ expect(result.sql).toContain('WHERE a.active = 1');
908
+ expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
909
+ });
910
+
911
+ it('should handle COUNT(DISTINCT) with patient_id column (stays as patient_id)', async () => {
912
+ const indicator = createIndicator({
913
+ kind: 'BASE',
914
+ sqlTemplate: 'SELECT COUNT(DISTINCT p.patient_id) AS total FROM patients p',
915
+ });
916
+
917
+ const getIndicator = createMockGetIndicator(new Map());
918
+ const result = await compilePopulationSql(indicator, getIndicator);
919
+
920
+ // Should convert to SELECT DISTINCT and stay as patient_id
921
+ expect(result.sql).toContain('SELECT DISTINCT');
922
+ expect(result.sql).toContain('AS patient_id');
923
+ });
924
+
925
+ it('should handle COUNT(DISTINCT) with encounter_id column (normalized to patient_id)', async () => {
926
+ const indicator = createIndicator({
927
+ kind: 'BASE',
928
+ sqlTemplate: 'SELECT COUNT(DISTINCT e.encounter_id) AS total FROM encounters e',
929
+ });
930
+
931
+ const getIndicator = createMockGetIndicator(new Map());
932
+ const result = await compilePopulationSql(indicator, getIndicator);
933
+
934
+ // Should convert to SELECT DISTINCT and normalize to patient_id
935
+ expect(result.sql).toContain('SELECT DISTINCT');
936
+ expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
937
+ });
938
+
939
+ it('should preserve WITH CTEs when converting COUNT(DISTINCT)', async () => {
940
+ const indicator = createIndicator({
941
+ kind: 'BASE',
942
+ sqlTemplate: `
943
+ WITH base AS (
944
+ SELECT patient_id FROM some_table
945
+ )
946
+ SELECT COUNT(DISTINCT a.patient_id) AS total FROM base a
947
+ `.trim(),
948
+ });
949
+
950
+ const getIndicator = createMockGetIndicator(new Map());
951
+ const result = await compilePopulationSql(indicator, getIndicator);
952
+
953
+ // Should preserve the WITH clause and normalize to patient_id
954
+ expect(result.sql).toContain('WITH base AS');
955
+ expect(result.sql).toContain('SELECT DISTINCT');
956
+ expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
957
+ });
958
+
959
+ it('should convert COUNT(DISTINCT) with subquery (no AS clause) and normalize to patient_id', async () => {
960
+ const indicator = createIndicator({
961
+ kind: 'BASE',
962
+ sqlTemplate: `
963
+ SELECT COUNT(DISTINCT a.patient_id)
964
+ FROM (
965
+ SELECT
966
+ patient_id,
967
+ TIMESTAMPDIFF(DAY, MAX(return_visit_date), :endDate) AS ltfp_days
968
+ FROM mamba_fact_encounter_hiv_art_card
969
+ WHERE encounter_date <= :endDate
970
+ AND return_visit_date >= :startDate
971
+ GROUP BY patient_id
972
+ ) a
973
+ `.trim(),
974
+ });
975
+
976
+ const getIndicator = createMockGetIndicator(new Map());
977
+ const result = await compilePopulationSql(indicator, getIndicator);
978
+
979
+ // Should convert to SELECT DISTINCT and normalize to patient_id
980
+ expect(result.sql).toContain('SELECT DISTINCT a.patient_id AS patient_id');
981
+ expect(result.sql).toContain('FROM (');
982
+ expect(result.sql).not.toContain('COUNT(DISTINCT');
983
+ });
984
+ });
985
+ });