@openmrs/esm-report-builder 0.1.1-pre.87
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.
- package/.eslintignore +5 -0
- package/.eslintrc.js +19 -0
- package/.prettierrc +8 -0
- package/.tx/config +12 -0
- package/LICENSE +401 -0
- package/README.md +336 -0
- package/__mocks__/react-i18next.js +12 -0
- package/dist/162.js +1 -0
- package/dist/162.js.map +1 -0
- package/dist/184.js +2 -0
- package/dist/184.js.LICENSE.txt +14 -0
- package/dist/184.js.map +1 -0
- package/dist/242.js +2 -0
- package/dist/242.js.LICENSE.txt +21 -0
- package/dist/242.js.map +1 -0
- package/dist/253.js +1 -0
- package/dist/253.js.map +1 -0
- package/dist/300.js +1 -0
- package/dist/311.js +1 -0
- package/dist/311.js.map +1 -0
- package/dist/469.js +2 -0
- package/dist/469.js.LICENSE.txt +45 -0
- package/dist/469.js.map +1 -0
- package/dist/478.js +2 -0
- package/dist/478.js.LICENSE.txt +9 -0
- package/dist/478.js.map +1 -0
- package/dist/539.js +1 -0
- package/dist/539.js.map +1 -0
- package/dist/540.js +2 -0
- package/dist/540.js.LICENSE.txt +9 -0
- package/dist/540.js.map +1 -0
- package/dist/669.js +1 -0
- package/dist/669.js.map +1 -0
- package/dist/710.js +1 -0
- package/dist/710.js.map +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/838.js +2 -0
- package/dist/838.js.LICENSE.txt +9 -0
- package/dist/838.js.map +1 -0
- package/dist/917.js +1 -0
- package/dist/917.js.map +1 -0
- package/dist/961.js +2 -0
- package/dist/961.js.LICENSE.txt +19 -0
- package/dist/961.js.map +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-report-builder.js +1 -0
- package/dist/openmrs-esm-report-builder.js.buildmanifest.json +530 -0
- package/dist/openmrs-esm-report-builder.js.map +1 -0
- package/dist/routes.json +1 -0
- package/e2e/core/env.ts +5 -0
- package/e2e/core/global-setup.ts +34 -0
- package/e2e/core/test.ts +21 -0
- package/e2e/pages/app-shell.page.ts +34 -0
- package/e2e/pages/etl-sources.page.ts +35 -0
- package/e2e/pages/report-sections.page.ts +20 -0
- package/e2e/pages/section-preview.modal.ts +29 -0
- package/e2e/specs/etl-sources.spec.ts +51 -0
- package/e2e/specs/navigation.spec.ts +13 -0
- package/e2e/specs/section-preview-validation.spec.ts +38 -0
- package/e2e/specs/section-preview.spec.ts +94 -0
- package/e2e/support/github/Dockerfile +22 -0
- package/e2e/support/github/Dockerfile.dockerignore +13 -0
- package/e2e/support/github/docker-compose.yml +31 -0
- package/e2e/support/github/run-e2e-docker-env.sh +131 -0
- package/e2e/support/github/spa-assemble-config.json +9 -0
- package/e2e/tsconfig.json +11 -0
- package/example.env +5 -0
- package/jest.config.js +28 -0
- package/linelist_report_builder_design_spec.md +816 -0
- package/package.json +111 -0
- package/playwright.config.ts +28 -0
- package/routes.json +22 -0
- package/src/__debug__/all-designs.test.tsx +30 -0
- package/src/admin-card-link.extension.module.scss +55 -0
- package/src/admin-card-link.extension.tsx +24 -0
- package/src/components/admin/admin-page.component.tsx +75 -0
- package/src/components/admin/age-categories-page.component.tsx +109 -0
- package/src/components/admin/age-groups-page.component.tsx +146 -0
- package/src/components/admin/etl-browser/columns-tab.component.tsx +80 -0
- package/src/components/admin/etl-browser/etl-browser-page.component.tsx +309 -0
- package/src/components/admin/etl-browser/etl-browser.component.scss +424 -0
- package/src/components/admin/etl-browser/metadata-tab.component.tsx +65 -0
- package/src/components/admin/etl-browser/preview-grid.component.tsx +189 -0
- package/src/components/admin/etl-browser/relationships-tab.component.tsx +21 -0
- package/src/components/admin/etl-browser/sources-tree.component.tsx +118 -0
- package/src/components/admin/etl-browser/table-detail.component.tsx +124 -0
- package/src/components/admin/etl-browser/table-info-strip.component.tsx +51 -0
- package/src/components/admin/etl-browser/table-list.component.tsx +136 -0
- package/src/components/admin/etl-browser/use-table-preview.ts +47 -0
- package/src/components/admin/etl-sources-page.component.tsx +355 -0
- package/src/components/admin/etl-tasks-page.component.scss +370 -0
- package/src/components/admin/etl-tasks-page.component.tsx +768 -0
- package/src/components/admin/report-categories-page.component.tsx +267 -0
- package/src/components/admin/report-library-page.component.tsx +551 -0
- package/src/components/ai-support/ai-assist-button.component.tsx +30 -0
- package/src/components/ai-support/ai-assist-modal.component.tsx +81 -0
- package/src/components/ai-support/ai.service.ts +21 -0
- package/src/components/ai-support/ai.types.ts +12 -0
- package/src/components/app-shell/report-builder-shell.component.tsx +242 -0
- package/src/components/app-shell/report-builder-shell.scss +18 -0
- package/src/components/dashboards/dashboard-config.schema.ts +97 -0
- package/src/components/dashboards/dashboard-form-modal.component.tsx +150 -0
- package/src/components/dashboards/dashboards-admin.scss +62 -0
- package/src/components/dashboards/dashboards-page.component.tsx +153 -0
- package/src/components/dashboards/dashboards-table.component.tsx +130 -0
- package/src/components/dashboards/sections/dashboard-autoinclude.section.tsx +75 -0
- package/src/components/dashboards/sections/dashboard-basics.section.tsx +80 -0
- package/src/components/dashboards/sections/dashboard-json.section.tsx +79 -0
- package/src/components/dashboards/sections/dashboard-sections-editor.section.tsx +130 -0
- package/src/components/dashboards/sections/dashboard-widgets-editor.section.tsx +184 -0
- package/src/components/data-themes/data-theme-form.component.tsx +59 -0
- package/src/components/data-themes/data-theme-modal.component.tsx +349 -0
- package/src/components/data-themes/data-themes-page.component.tsx +139 -0
- package/src/components/data-themes/data-themes-table.component.tsx +98 -0
- package/src/components/data-themes/icon-registry.ts +35 -0
- package/src/components/data-themes/sections/data-theme-basics.section.tsx +63 -0
- package/src/components/data-themes/sections/data-theme-condition-columns.section.tsx +397 -0
- package/src/components/data-themes/sections/data-theme-fields-editor.section.tsx +125 -0
- package/src/components/data-themes/sections/data-theme-metadata.section.tsx +245 -0
- package/src/components/data-themes/sections/data-theme-preview.section.tsx +115 -0
- package/src/components/data-themes/sections/data-theme-source.section.tsx +184 -0
- package/src/components/data-visualizer/components/cqi-components/cqi-data-table.component.tsx +214 -0
- package/src/components/data-visualizer/components/cqi-components/cqi-elements.ts +203 -0
- package/src/components/data-visualizer/components/cqi-components/detail.component.tsx +110 -0
- package/src/components/data-visualizer/components/cqi-components/detail.scss +22 -0
- package/src/components/data-visualizer/components/data-table/data-table.component.tsx +208 -0
- package/src/components/data-visualizer/components/data-table/data-tables.scss +206 -0
- package/src/components/data-visualizer/components/empty-state/empty-state-illustration.component.tsx +53 -0
- package/src/components/data-visualizer/components/header/header.component.tsx +43 -0
- package/src/components/data-visualizer/components/header/header.scss +71 -0
- package/src/components/data-visualizer/components/model/confirm.component.tsx +40 -0
- package/src/components/data-visualizer/components/panel/panel.component.tsx +20 -0
- package/src/components/data-visualizer/components/panel/panel.scss +25 -0
- package/src/components/data-visualizer/components/popover/modifier-panel.tsx +53 -0
- package/src/components/data-visualizer/constants.ts +445 -0
- package/src/components/data-visualizer/data-visualizer-illustration.component.tsx +31 -0
- package/src/components/data-visualizer/data-visualizer.component.tsx +1935 -0
- package/src/components/data-visualizer/data-visualizer.resource.ts +462 -0
- package/src/components/data-visualizer/data-visualizer.scss +449 -0
- package/src/components/data-visualizer/declarations.d.ts +131 -0
- package/src/components/data-visualizer/parameter-inputs/boolean-parameter-input.component.tsx +34 -0
- package/src/components/data-visualizer/parameter-inputs/concept-parameter-input.component.tsx +81 -0
- package/src/components/data-visualizer/parameter-inputs/date-parameter-input.component.tsx +110 -0
- package/src/components/data-visualizer/parameter-inputs/date-range-input.component.scss +47 -0
- package/src/components/data-visualizer/parameter-inputs/date-range-input.component.tsx +228 -0
- package/src/components/data-visualizer/parameter-inputs/identifier-type-parameter-input.component.tsx +69 -0
- package/src/components/data-visualizer/parameter-inputs/list-parameter-input.component.tsx +73 -0
- package/src/components/data-visualizer/parameter-inputs/location-parameter-input.component.tsx +81 -0
- package/src/components/data-visualizer/parameter-inputs/number-parameter-input.component.tsx +40 -0
- package/src/components/data-visualizer/parameter-inputs/person-attribute-parameter-input.component.tsx +71 -0
- package/src/components/data-visualizer/parameter-inputs/text-parameter-input.component.tsx +52 -0
- package/src/components/data-visualizer/report-parameter-modal.component.tsx +383 -0
- package/src/components/data-visualizer/types.ts +206 -0
- package/src/components/etl-monitor/builder/builder-state-machine.ts +381 -0
- package/src/components/etl-monitor/builder/design-registry.tsx +139 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-context.tsx +702 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-modal.component.tsx +461 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-modal.scss +256 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +387 -0
- package/src/components/etl-monitor/builder/etl-monitor-builder-page.scss +976 -0
- package/src/components/etl-monitor/builder/generated-config-panel.component.tsx +168 -0
- package/src/components/etl-monitor/builder/generated-config-panel.scss +198 -0
- package/src/components/etl-monitor/builder/index.ts +5 -0
- package/src/components/etl-monitor/builder/monitor-preview.tsx +124 -0
- package/src/components/etl-monitor/builder/steps/data-source-step.component.tsx +667 -0
- package/src/components/etl-monitor/builder/steps/data-source-step.scss +284 -0
- package/src/components/etl-monitor/builder/steps/design-samples.ts +278 -0
- package/src/components/etl-monitor/builder/steps/design-step.component.tsx +299 -0
- package/src/components/etl-monitor/builder/steps/design-step.scss +479 -0
- package/src/components/etl-monitor/builder/steps/fields-step.component.tsx +859 -0
- package/src/components/etl-monitor/builder/steps/fields-step.scss +459 -0
- package/src/components/etl-monitor/builder/steps/general-step.component.tsx +288 -0
- package/src/components/etl-monitor/builder/steps/general-step.scss +103 -0
- package/src/components/etl-monitor/builder/steps/index.ts +5 -0
- package/src/components/etl-monitor/builder/steps/preview-step.component.tsx +245 -0
- package/src/components/etl-monitor/builder/steps/preview-step.scss +236 -0
- package/src/components/etl-monitor/etl-monitor-card-shell.component.tsx +154 -0
- package/src/components/etl-monitor/etl-monitor-card-shell.scss +103 -0
- package/src/components/etl-monitor/etl-monitor-dashboard.component.tsx +227 -0
- package/src/components/etl-monitor/etl-monitor-dashboard.scss +148 -0
- package/src/components/etl-monitor/etl-monitor-status-card.component.tsx +192 -0
- package/src/components/etl-monitor/etl-monitors-page.component.tsx +621 -0
- package/src/components/etl-monitor/renderers/DetailsRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/ErrorLogRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/EtlMonitorWidget.tsx +97 -0
- package/src/components/etl-monitor/renderers/MetricsGridRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/MonitorRenderer.tsx +175 -0
- package/src/components/etl-monitor/renderers/ProgressRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/StatusCardRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/SummaryCardRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/TableRenderer.tsx +2 -0
- package/src/components/etl-monitor/renderers/component-icons.tsx +33 -0
- package/src/components/etl-monitor/renderers/components/details-renderer.tsx +195 -0
- package/src/components/etl-monitor/renderers/components/error-log-renderer.tsx +104 -0
- package/src/components/etl-monitor/renderers/components/index.ts +6 -0
- package/src/components/etl-monitor/renderers/components/log-renderer.tsx +86 -0
- package/src/components/etl-monitor/renderers/components/metrics-grid-renderer.tsx +133 -0
- package/src/components/etl-monitor/renderers/components/progress-renderer.tsx +188 -0
- package/src/components/etl-monitor/renderers/components/status-card-renderer.tsx +247 -0
- package/src/components/etl-monitor/renderers/components/summary-card-renderer.tsx +160 -0
- package/src/components/etl-monitor/renderers/components/table-renderer.tsx +175 -0
- package/src/components/etl-monitor/renderers/components/time-series-renderer.tsx +310 -0
- package/src/components/etl-monitor/renderers/data-transformer.ts +361 -0
- package/src/components/etl-monitor/renderers/index.ts +11 -0
- package/src/components/etl-monitor/renderers/legacy-monitor-renderer.tsx +69 -0
- package/src/components/etl-monitor/renderers/monitor-renderer.tsx +6 -0
- package/src/components/etl-monitor/renderers/monitor-renderers.scss +1621 -0
- package/src/components/etl-monitor/renderers/monitor-widget-card.component.tsx +54 -0
- package/src/components/etl-monitor/renderers/monitor-widget-card.scss +16 -0
- package/src/components/etl-monitor/renderers/v2-monitor-renderer.tsx +227 -0
- package/src/components/import-export/artifact-groups.ts +153 -0
- package/src/components/import-export/export-report-modal.component.tsx +265 -0
- package/src/components/import-export/import-export-page.component.tsx +610 -0
- package/src/components/import-export/import-export.styles.scss +825 -0
- package/src/components/import-export/import-package-modal.component.tsx +401 -0
- package/src/components/import-export/index.ts +3 -0
- package/src/components/indicators/README.md +24 -0
- package/src/components/indicators/base-indicator-basic-fields.component.tsx +18 -0
- package/src/components/indicators/base-indicator-basics.section.tsx +54 -0
- package/src/components/indicators/base-indicator-countby.section.tsx +25 -0
- package/src/components/indicators/base-indicator-criteria.section.tsx +83 -0
- package/src/components/indicators/base-indicator-details-form.component.tsx +80 -0
- package/src/components/indicators/base-indicator-theme-unit-fields.component.tsx +39 -0
- package/src/components/indicators/create-base-indicator-modal.component.tsx +623 -0
- package/src/components/indicators/create-composite-base-indicator-modal.component.tsx +295 -0
- package/src/components/indicators/create-custom-indicator-modal.component.tsx +548 -0
- package/src/components/indicators/create-final-indicator-modal.component.tsx +285 -0
- package/src/components/indicators/custom-condition-input.component.tsx +310 -0
- package/src/components/indicators/diagnosis-filters-form.component.tsx +101 -0
- package/src/components/indicators/disaggregation-panel.component.tsx +93 -0
- package/src/components/indicators/handler/concept-search-multiselect.component.tsx +153 -0
- package/src/components/indicators/handler/question-answer-concept-search.component.tsx +336 -0
- package/src/components/indicators/indicator-run-preview-modal.component.tsx +391 -0
- package/src/components/indicators/indicator-search-select.component.tsx +297 -0
- package/src/components/indicators/indicator-status-tag.component.tsx +9 -0
- package/src/components/indicators/indicators-page.component.tsx +673 -0
- package/src/components/indicators/indicators-page.scss +121 -0
- package/src/components/indicators/indicators-table.component.tsx +188 -0
- package/src/components/indicators/preview-table.component.tsx +31 -0
- package/src/components/indicators/sections/composite-indicator-basics.section.tsx +36 -0
- package/src/components/indicators/sections/composite-indicator-picker.section.tsx +116 -0
- package/src/components/indicators/sections/composite-indicator-sql-preview.section.tsx +31 -0
- package/src/components/indicators/sections/custom-indicator-basics.section.tsx +65 -0
- package/src/components/indicators/sections/custom-indicator-disaggregation.section.tsx +93 -0
- package/src/components/indicators/sections/custom-indicator-filters.section.tsx +203 -0
- package/src/components/indicators/sections/custom-indicator-population.section.tsx +107 -0
- package/src/components/indicators/sections/custom-indicator-sql-preview.section.tsx +108 -0
- package/src/components/indicators/sections/final-indicator-basics.section.tsx +46 -0
- package/src/components/indicators/sections/final-indicator-disaggregation.section.tsx +47 -0
- package/src/components/indicators/sections/final-indicator-picker.section.tsx +60 -0
- package/src/components/indicators/sections/final-indicator-preview.section.tsx +20 -0
- package/src/components/indicators/sections/final-indicator-results-preview.section.tsx +65 -0
- package/src/components/indicators/sections/indicator-basics.section.tsx +40 -0
- package/src/components/indicators/sections/indicator-conditions.section.tsx +279 -0
- package/src/components/indicators/sections/indicator-sql-preview.section.tsx +63 -0
- package/src/components/indicators/sections/indicator-theme.section.tsx +48 -0
- package/src/components/indicators/sql-preview.component.tsx +19 -0
- package/src/components/indicators/tag-input.component.tsx +177 -0
- package/src/components/indicators/types/base-indicator-form.types.ts +17 -0
- package/src/components/indicators/types/base-indicator-theme.constants.ts +8 -0
- package/src/components/indicators/types/composite-indicator.types.ts +27 -0
- package/src/components/indicators/types/condition-ui.types.ts +11 -0
- package/src/components/indicators/types/custom-indicator.types.ts +161 -0
- package/src/components/indicators/types/data-theme-config.types.ts +88 -0
- package/src/components/indicators/types/indicator-types.ts +57 -0
- package/src/components/indicators/types/sql-builders.ts +43 -0
- package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +143 -0
- package/src/components/indicators/utils/composite-indicator-sql.utils.ts +426 -0
- package/src/components/indicators/utils/concept-preload.utils.ts +21 -0
- package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +493 -0
- package/src/components/indicators/utils/custom-indicator-interpreter.ts +702 -0
- package/src/components/indicators/utils/final-indicator-sql.utils.ts +190 -0
- package/src/components/indicators/utils/indicator-conditions-hydration.utils.ts +254 -0
- package/src/components/indicators/utils/indicator-sql-resolution.utils.ts +204 -0
- package/src/components/indicators/utils/indicator-sql.utils.ts +721 -0
- package/src/components/indicators/utils/population-sql.compiler.test.ts +999 -0
- package/src/components/indicators/utils/population-sql.compiler.ts +858 -0
- package/src/components/landing/landing-action-item.component.tsx +50 -0
- package/src/components/landing/landing-action-list.component.tsx +12 -0
- package/src/components/landing/landing-activity-list.component.tsx +47 -0
- package/src/components/landing/landing-footer-grid.component.tsx +45 -0
- package/src/components/landing/landing-hero.component.tsx +18 -0
- package/src/components/landing/landing-section-card.component.tsx +33 -0
- package/src/components/landing/report-builder-landing-page.component.tsx +186 -0
- package/src/components/landing/report-builder-landing-page.scss +131 -0
- package/src/components/legacy-reports/legacy-report-detail-page.component.tsx +300 -0
- package/src/components/legacy-reports/legacy-report-editor-page.component.tsx +324 -0
- package/src/components/legacy-reports/legacy-report-upload-modal.component.tsx +307 -0
- package/src/components/legacy-reports/legacy-reports-page.component.tsx +206 -0
- package/src/components/legacy-reports/legacy-reports-table.component.tsx +154 -0
- package/src/components/legacy-reports/tabs/legacy-report-datasets-tab.component.tsx +344 -0
- package/src/components/legacy-reports/tabs/legacy-report-dimensions-tab.component.tsx +590 -0
- package/src/components/legacy-reports/tabs/legacy-report-indicators-tab.component.tsx +490 -0
- package/src/components/legacy-reports/tabs/legacy-report-json-preview-tab.component.tsx +272 -0
- package/src/components/legacy-reports/tabs/legacy-report-overview-tab.component.tsx +218 -0
- package/src/components/legacy-reports/tabs/legacy-report-parameters-tab.component.tsx +279 -0
- package/src/components/linelist/definition/indicator-to-sql.utils.ts +281 -0
- package/src/components/linelist/definition/linelist-definition-editor.component.tsx +1676 -0
- package/src/components/linelist/definition/linelist-definition-editor.scss +461 -0
- package/src/components/linelist/definition/linelist-definition.types.ts +108 -0
- package/src/components/linelist/definition/panels/column-properties-panel.component.tsx +328 -0
- package/src/components/linelist/definition/panels/column-selector-panel.component.tsx +1082 -0
- package/src/components/linelist/definition/panels/column-selector-panel.scss +100 -0
- package/src/components/linelist/definition/panels/custom-sql-column-modal.component.tsx +248 -0
- package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.component.tsx +300 -0
- package/src/components/linelist/definition/panels/encounter-diagnosis-column-modal.scss +21 -0
- package/src/components/linelist/definition/panels/multi-datasource-selector.component.tsx +332 -0
- package/src/components/linelist/definition/panels/multi-datasource-selector.scss +161 -0
- package/src/components/linelist/definition/panels/observation-column-modal.component.tsx +450 -0
- package/src/components/linelist/definition/panels/observation-column-modal.scss +29 -0
- package/src/components/linelist/definition/panels/parameter-panel.component.tsx +659 -0
- package/src/components/linelist/definition/panels/parameter-panel.scss +64 -0
- package/src/components/linelist/definition/panels/population-source-selector.component.tsx +407 -0
- package/src/components/linelist/definition/panels/population-source-selector.scss +187 -0
- package/src/components/linelist/design/panels/column-category-selector.component.tsx +433 -0
- package/src/components/linelist/design/panels/column-category-selector.scss +329 -0
- package/src/components/linelist/design/panels/column-debug-panel.component.tsx +197 -0
- package/src/components/linelist/design/panels/column-debug-panel.scss +141 -0
- package/src/components/linelist/design/panels/data-catalogue.component.tsx +1062 -0
- package/src/components/linelist/design/panels/data-catalogue.scss +263 -0
- package/src/components/linelist/design/panels/display-settings-panel.component.tsx +405 -0
- package/src/components/linelist/design/panels/display-settings-panel.scss +78 -0
- package/src/components/linelist/design/panels/filtermap-editor.component.tsx +434 -0
- package/src/components/linelist/design/panels/filtermap-editor.scss +176 -0
- package/src/components/linelist/design/panels/json-preview.component.tsx +114 -0
- package/src/components/linelist/design/panels/json-preview.scss +85 -0
- package/src/components/linelist/design/panels/query-config-panel.component.tsx +1137 -0
- package/src/components/linelist/design/panels/query-config-panel.scss +342 -0
- package/src/components/linelist/design/panels/sort-config-panel.component.tsx +330 -0
- package/src/components/linelist/design/panels/sort-config-panel.scss +77 -0
- package/src/components/linelist/design/panels/sql-builder-panel.component.tsx +475 -0
- package/src/components/linelist/design/panels/sql-builder-panel.scss +226 -0
- package/src/components/linelist/design/panels/transformation-panel.component.tsx +687 -0
- package/src/components/linelist/design/panels/transformation-panel.scss +81 -0
- package/src/components/linelist/design/panels/visual-filter-panel.component.tsx +595 -0
- package/src/components/linelist/design/panels/visual-filter-panel.scss +76 -0
- package/src/components/linelist/linelist-dashboard.page.component.tsx +756 -0
- package/src/components/linelist/linelist-dashboard.page.scss +240 -0
- package/src/components/linelist/linelist-editor.page.component.tsx +134 -0
- package/src/components/linelist/linelist-editor.page.scss +28 -0
- package/src/components/linelist/linelist-run-report.page.component.tsx +715 -0
- package/src/components/linelist/linelist-run-report.page.scss +136 -0
- package/src/components/linelist/modals/linelist-report-builder-modal.component.tsx +1454 -0
- package/src/components/linelist/modals/linelist-report-builder-modal.scss +164 -0
- package/src/components/linelist/modals/preview-parameter-modal.component.tsx +361 -0
- package/src/components/report/dashboard/reports-table.component.tsx +186 -0
- package/src/components/report/definition/panels/properties-panel.component.tsx +38 -0
- package/src/components/report/definition/panels/report-structure-panel.component.tsx +61 -0
- package/src/components/report/definition/panels/selection-panel.component.tsx +58 -0
- package/src/components/report/definition/report-definition-editor.component.tsx +99 -0
- package/src/components/report/definition/report-definition.types.ts +66 -0
- package/src/components/report/design/panels/IndicatorPropertiesPanel.scss +33 -0
- package/src/components/report/design/panels/IndicatorPropertiesPanel.tsx +389 -0
- package/src/components/report/design/panels/JsonPreviewPanel.scss +32 -0
- package/src/components/report/design/panels/JsonPreviewPanel.tsx +104 -0
- package/src/components/report/design/panels/mapping-preview-panel.component.tsx +82 -0
- package/src/components/report/design/panels/mapping-preview-panel.scss +55 -0
- package/src/components/report/design/panels/template-structure-panel.component.tsx +296 -0
- package/src/components/report/design/panels/template-structure-panel.scss +81 -0
- package/src/components/report/design/report-design-editor.component.tsx +1170 -0
- package/src/components/report/design/report-design.types.ts +48 -0
- package/src/components/report/design/report-design.utils.ts +86 -0
- package/src/components/report/panels/report-detail.component.tsx +71 -0
- package/src/components/report/panels/report-panel-shell.component.tsx +60 -0
- package/src/components/report/report-dashboard.page.component.tsx +554 -0
- package/src/components/report/report-editor.page.component.tsx +590 -0
- package/src/components/report/report.routes.tsx +32 -0
- package/src/components/report-sections/create-report-section-modal.component.tsx +388 -0
- package/src/components/report-sections/hooks/useAgeCategories.ts +25 -0
- package/src/components/report-sections/hooks/useSectionEditorState.ts +449 -0
- package/src/components/report-sections/panels/Dhis2MappingPanel.tsx +157 -0
- package/src/components/report-sections/panels/DisaggregationPanel.tsx +97 -0
- package/src/components/report-sections/panels/IndicatorPickerPanel.tsx +201 -0
- package/src/components/report-sections/panels/SectionBasicsPanel.tsx +27 -0
- package/src/components/report-sections/report-section-preview-modal.component.tsx +548 -0
- package/src/components/report-sections/report-sections-page.component.tsx +325 -0
- package/src/components/report-sections/section-disaggregation.utils.ts +637 -0
- package/src/components/report-sections/section-types.ts +55 -0
- package/src/components/report-sections/section-utils.ts +50 -0
- package/src/components/report-sections/sections-page.scss +79 -0
- package/src/components/run-reports/run-reports-page.component.tsx +8 -0
- package/src/components/security/access-denied-page.component.tsx +69 -0
- package/src/components/security/access-denied-page.scss +39 -0
- package/src/components/security/privilege-route.component.test.tsx +79 -0
- package/src/components/security/privilege-route.component.tsx +39 -0
- package/src/components/shared/compile-setup-modal.component.tsx +182 -0
- package/src/components/shared/concept-selector.component.tsx +245 -0
- package/src/components/shared/concept-selector.scss +121 -0
- package/src/components/shared/header/builder-header-actions.component.tsx +38 -0
- package/src/components/shared/header/header.component.tsx +85 -0
- package/src/components/shared/header/header.scss +150 -0
- package/src/components/shared/header/illustration.component.tsx +34 -0
- package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.scss +33 -0
- package/src/components/shared/indicator-properties-panel/IndicatorPropertiesPanel.tsx +389 -0
- package/src/components/shared/json-preview-panel/JsonPreviewPanel.scss +32 -0
- package/src/components/shared/json-preview-panel/JsonPreviewPanel.tsx +104 -0
- package/src/components/shared/mapping-preview-panel/mapping-preview-panel.component.tsx +82 -0
- package/src/components/shared/mapping-preview-panel/mapping-preview-panel.scss +55 -0
- package/src/components/shared/preview/query-results-preview.component.tsx +204 -0
- package/src/components/shared/template-structure-panel/template-structure-panel.component.tsx +296 -0
- package/src/components/shared/template-structure-panel/template-structure-panel.scss +81 -0
- package/src/config-schema.ts +43 -0
- package/src/constants/privileges.test.ts +53 -0
- package/src/constants/privileges.ts +118 -0
- package/src/features/dashboards/DashboardRendererPage.tsx +174 -0
- package/src/features/dashboards/DashboardsPage.tsx +164 -0
- package/src/features/dashboards/EtlDashboardPage.tsx +32 -0
- package/src/features/dashboards/components/DashboardEmptyState.tsx +41 -0
- package/src/features/dashboards/components/DashboardGrid.tsx +48 -0
- package/src/features/dashboards/components/DashboardHeader.tsx +58 -0
- package/src/features/dashboards/components/DashboardSection.tsx +46 -0
- package/src/features/dashboards/components/ReportLaunchCard.tsx +51 -0
- package/src/features/dashboards/components/WidgetSlotDispatcher.tsx +61 -0
- package/src/features/dashboards/components/WidgetUnavailableCard.tsx +36 -0
- package/src/features/dashboards/dashboards.scss +196 -0
- package/src/features/dashboards/hooks/useDashboard.ts +61 -0
- package/src/features/dashboards/hooks/useDashboardWidgets.ts +74 -0
- package/src/features/dashboards/hooks/useDashboards.ts +56 -0
- package/src/features/dashboards/hooks/useMonitorDataMap.ts +84 -0
- package/src/features/dashboards/index.ts +11 -0
- package/src/features/dashboards/utils/dashboard-config.util.test.ts +255 -0
- package/src/features/dashboards/utils/dashboard-config.util.ts +382 -0
- package/src/features/report-visualizer/ReportVisualizerPage.tsx +448 -0
- package/src/features/report-visualizer/components/Export/ExportMenu.tsx +141 -0
- package/src/features/report-visualizer/components/Export/index.ts +2 -0
- package/src/features/report-visualizer/components/ReportExplorer/QuickFilters.tsx +152 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportCategoryFilter.tsx +54 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerPanel.tsx +201 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportExplorerRail.tsx +202 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportList.tsx +287 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportSearch.tsx +151 -0
- package/src/features/report-visualizer/components/ReportExplorer/ReportTagFilter.tsx +100 -0
- package/src/features/report-visualizer/components/ReportExplorer/RunReportButton.tsx +42 -0
- package/src/features/report-visualizer/components/ReportExplorer/RuntimeParameterForm.tsx +259 -0
- package/src/features/report-visualizer/components/ReportExplorer/index.ts +8 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportLayer.tsx +33 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportSummary.tsx +88 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportViewTabs.tsx +72 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspaceHeader.tsx +92 -0
- package/src/features/report-visualizer/components/ReportWorkspace/ReportWorkspacePanel.tsx +242 -0
- package/src/features/report-visualizer/components/ReportWorkspace/index.ts +5 -0
- package/src/features/report-visualizer/components/TableView/ColumnVisibilityMenu.tsx +206 -0
- package/src/features/report-visualizer/components/TableView/ReportTable.tsx +253 -0
- package/src/features/report-visualizer/components/TableView/index.ts +2 -0
- package/src/features/report-visualizer/hooks/index.ts +4 -0
- package/src/features/report-visualizer/hooks/useExplorerPreferences.ts +103 -0
- package/src/features/report-visualizer/hooks/useReportCapabilities.ts +112 -0
- package/src/features/report-visualizer/hooks/useReportParameters.ts +152 -0
- package/src/features/report-visualizer/hooks/useReports.ts +135 -0
- package/src/features/report-visualizer/index.ts +6 -0
- package/src/features/report-visualizer/report-visualizer.scss +593 -0
- package/src/features/report-visualizer/types/index.ts +1 -0
- package/src/features/report-visualizer/types/report-visualizer.types.ts +104 -0
- package/src/features/report-visualizer/utils/status-colors.ts +117 -0
- package/src/globals.d.ts +14 -0
- package/src/hooks/address-template.ts +86 -0
- package/src/hooks/concept.ts +112 -0
- package/src/hooks/indicator/index.ts +1 -0
- package/src/hooks/indicator/useIndicatorsByTheme.ts +41 -0
- package/src/hooks/openmrs-reference-data.ts +432 -0
- package/src/hooks/theme/index.ts +4 -0
- package/src/hooks/theme/useDataTheme.ts +61 -0
- package/src/hooks/theme/useDataThemes.ts +28 -0
- package/src/hooks/theme/useMambaTableMeta.ts +34 -0
- package/src/hooks/theme/useMambaTables.ts +95 -0
- package/src/hooks/use-report-builder-privileges.test.tsx +98 -0
- package/src/hooks/use-report-builder-privileges.ts +58 -0
- package/src/index.ts +29 -0
- package/src/resources/address-template/address-template.api.ts +180 -0
- package/src/resources/agegroup/agegroups.api.ts +58 -0
- package/src/resources/concept/concept.api.ts +116 -0
- package/src/resources/concepts/concept-mapper.ts +21 -0
- package/src/resources/concepts/concept-types.ts +31 -0
- package/src/resources/concepts/concepts.resource.ts +44 -0
- package/src/resources/concepts/useConceptSearch.ts +41 -0
- package/src/resources/dashboard/dashboard.api.ts +107 -0
- package/src/resources/etl-monitor/etl-monitor-test.api.ts +409 -0
- package/src/resources/etl-monitor/etl-monitor.api.ts +381 -0
- package/src/resources/etl-source/etl-source.api.ts +65 -0
- package/src/resources/etl-task/etl-task.api.ts +96 -0
- package/src/resources/etl-task/etl-task.types.ts +31 -0
- package/src/resources/indicator/indicators.api.ts +125 -0
- package/src/resources/legacy-reports/legacy-reports.api.ts +144 -0
- package/src/resources/linelist/linelist-reports.api.ts +616 -0
- package/src/resources/location/location.api.ts +135 -0
- package/src/resources/openmrs-api.ts +40 -0
- package/src/resources/patient-identifier-type/patient-identifier-type.api.ts +65 -0
- package/src/resources/person-attribute-type/person-attribute-type.api.ts +71 -0
- package/src/resources/preview/sql-preview.api.ts +54 -0
- package/src/resources/report/reports.api.ts +140 -0
- package/src/resources/report-category/report-category.api.ts +41 -0
- package/src/resources/report-import-export/import-export-api.ts +164 -0
- package/src/resources/report-library/report-library.api.ts +63 -0
- package/src/resources/report-section/report-sections.api.ts +73 -0
- package/src/resources/report-section/section-preview.api.ts +25 -0
- package/src/resources/theme/data-theme.api.ts +88 -0
- package/src/resources/theme/etl-schema.api.ts +101 -0
- package/src/resources/theme/etl-table-meta.api.ts +45 -0
- package/src/root.component.tsx +20 -0
- package/src/routes/report-builder.routes.tsx +160 -0
- package/src/routes/report-builder.scss +56 -0
- package/src/routes.json +28 -0
- package/src/sample-template.ts +121 -0
- package/src/template-utils.ts +110 -0
- package/src/types/carbon-react-inert.d.ts +13 -0
- package/src/types/condition-operators.ts +63 -0
- package/src/types/dashboard/dashboard.types.ts +137 -0
- package/src/types/dashboard/index.ts +4 -0
- package/src/types/etl/etl-types.ts +238 -0
- package/src/types/etl-monitor/etl-monitor-builder.types.ts +555 -0
- package/src/types/etl-monitor/etl-monitor-v2.types.ts +397 -0
- package/src/types/etl-monitor/etl-monitor.types.ts +188 -0
- package/src/types/etl-monitor/index.ts +6 -0
- package/src/types/linelist/compile-config.ts +455 -0
- package/src/types/linelist-types.ts +2059 -0
- package/src/types/theme/data-theme.types.ts +88 -0
- package/src/utils/api-error.utils.test.ts +151 -0
- package/src/utils/api-error.utils.ts +131 -0
- package/src/utils/config-enhancer.ts +62 -0
- package/src/utils/etl-browser/etl-browser.util.test.ts +145 -0
- package/src/utils/etl-browser/export-csv.util.ts +43 -0
- package/src/utils/etl-browser/group-tables.util.ts +84 -0
- package/src/utils/etl-browser/index.ts +4 -0
- package/src/utils/etl-browser/summarize-columns.util.ts +27 -0
- package/src/utils/etl-browser/table-sql.util.ts +20 -0
- package/src/utils/etl-monitor/config-v2-adapter.util.ts +460 -0
- package/src/utils/etl-monitor/endpoint-config-normalizer.util.ts +383 -0
- package/src/utils/etl-monitor/index.ts +10 -0
- package/src/utils/etl-monitor/jsonpath-resolver.util.ts +118 -0
- package/src/utils/etl-monitor/normalization-examples.ts +0 -0
- package/src/utils/etl-monitor/schema-inspector.util.ts +76 -0
- package/src/utils/etl-monitor/semantic-type-detector.util.ts +76 -0
- package/src/utils/etl-monitor/status-mapper.util.ts +105 -0
- package/src/utils/etl-monitor/value-formatters.util.ts +167 -0
- package/src/utils/etl-task/etl-task-card.util.test.ts +239 -0
- package/src/utils/etl-task/etl-task-card.util.ts +276 -0
- package/src/utils/etl-task/index.ts +1 -0
- package/src/utils/html-entities.utils.ts +34 -0
- package/src/utils/parameter-resolution.ts +218 -0
- package/src/views/etl-monitor-builder/add-monitor-modal.component.scss +0 -0
- package/src/views/etl-monitor-builder/add-monitor-modal.component.tsx +210 -0
- package/src/views/etl-monitor-builder/etl-monitor-builder.component.scss +157 -0
- package/src/views/etl-monitor-builder/etl-monitor-builder.component.tsx +252 -0
- package/src/views/etl-monitor-builder/etl-monitor-builder.store.ts +126 -0
- package/tools/scss-stub.js +12 -0
- package/tools/setup-tests.ts +1 -0
- package/translations/en.json +65 -0
- package/tsconfig.json +43 -0
- package/webpack.config.js +30 -0
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the population SQL compiler.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
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.client_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('client_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.client_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 client_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 client_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 client_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 client_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 client_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 client_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 client_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 client_id, but the population output contract is patient_id
|
|
221
|
+
expect(result.sql).toMatch(/client_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 client_id FROM table_a'
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const indicatorB = createIndicator({
|
|
236
|
+
uuid: 'indicator-b',
|
|
237
|
+
code: 'B',
|
|
238
|
+
kind: 'BASE',
|
|
239
|
+
sqlTemplate: 'SELECT DISTINCT client_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.client_id AS client_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.client_id AS client_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.client_id AS client_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\.client_id AS patient_id/);
|
|
306
|
+
expect(result.sql).toMatch(/SELECT DISTINCT b\.client_id AS patient_id/);
|
|
307
|
+
|
|
308
|
+
// ...and every CTE reference uses it — no dangling client_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]\.client_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 client_id FROM table_a'
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const indicatorB = createIndicator({
|
|
325
|
+
uuid: 'indicator-b',
|
|
326
|
+
code: 'B',
|
|
327
|
+
kind: 'BASE',
|
|
328
|
+
sqlTemplate: 'SELECT DISTINCT client_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 client_id FROM base'
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const someOtherBase = createIndicator({
|
|
394
|
+
uuid: 'some-other-base',
|
|
395
|
+
code: 'OTHER',
|
|
396
|
+
kind: 'BASE',
|
|
397
|
+
sqlTemplate: 'SELECT DISTINCT client_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(
|
|
409
|
+
compilePopulationSql(indicatorA, getIndicator)
|
|
410
|
+
).rejects.toThrow(CircularDependencyError);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should detect deep circular dependency (A -> B -> C -> A)', async () => {
|
|
414
|
+
const baseA = createIndicator({
|
|
415
|
+
uuid: 'base-a',
|
|
416
|
+
code: 'BASE_A',
|
|
417
|
+
kind: 'BASE',
|
|
418
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM base_a'
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const indicatorC = createIndicator({
|
|
422
|
+
uuid: 'indicator-c',
|
|
423
|
+
code: 'C',
|
|
424
|
+
kind: 'COMPOSITE',
|
|
425
|
+
configJson: JSON.stringify({
|
|
426
|
+
version: 1,
|
|
427
|
+
unit: 'Patients',
|
|
428
|
+
operator: 'AND',
|
|
429
|
+
indicatorAId: 'indicator-a',
|
|
430
|
+
indicatorBId: 'base-a'
|
|
431
|
+
})
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const indicatorB = createIndicator({
|
|
435
|
+
uuid: 'indicator-b',
|
|
436
|
+
code: 'B',
|
|
437
|
+
kind: 'COMPOSITE',
|
|
438
|
+
configJson: JSON.stringify({
|
|
439
|
+
version: 1,
|
|
440
|
+
unit: 'Patients',
|
|
441
|
+
operator: 'AND',
|
|
442
|
+
indicatorAId: 'indicator-c',
|
|
443
|
+
indicatorBId: 'base-a'
|
|
444
|
+
})
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const indicatorA = createIndicator({
|
|
448
|
+
uuid: 'indicator-a',
|
|
449
|
+
code: 'A',
|
|
450
|
+
kind: 'COMPOSITE',
|
|
451
|
+
configJson: JSON.stringify({
|
|
452
|
+
version: 1,
|
|
453
|
+
unit: 'Patients',
|
|
454
|
+
operator: 'AND',
|
|
455
|
+
indicatorAId: 'indicator-b',
|
|
456
|
+
indicatorBId: 'base-a'
|
|
457
|
+
})
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const indicators = new Map([
|
|
461
|
+
['indicator-a', indicatorA],
|
|
462
|
+
['indicator-b', indicatorB],
|
|
463
|
+
['indicator-c', indicatorC],
|
|
464
|
+
['base-a', baseA]
|
|
465
|
+
]);
|
|
466
|
+
const getIndicator = createMockGetIndicator(indicators);
|
|
467
|
+
|
|
468
|
+
await expect(
|
|
469
|
+
compilePopulationSql(indicatorA, getIndicator)
|
|
470
|
+
).rejects.toThrow(CircularDependencyError);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe('Missing Reference Validation', () => {
|
|
475
|
+
it('should throw error for missing indicatorAId', async () => {
|
|
476
|
+
const composite = createIndicator({
|
|
477
|
+
uuid: 'composite',
|
|
478
|
+
code: 'COMP',
|
|
479
|
+
kind: 'COMPOSITE',
|
|
480
|
+
configJson: JSON.stringify({
|
|
481
|
+
version: 1,
|
|
482
|
+
unit: 'Patients',
|
|
483
|
+
operator: 'AND',
|
|
484
|
+
indicatorBId: 'indicator-b'
|
|
485
|
+
})
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
489
|
+
|
|
490
|
+
await expect(
|
|
491
|
+
compilePopulationSql(composite, getIndicator)
|
|
492
|
+
).rejects.toThrow(MissingReferenceError);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should throw error for missing indicatorBId', async () => {
|
|
496
|
+
const indicatorA = createIndicator({
|
|
497
|
+
uuid: 'indicator-a',
|
|
498
|
+
code: 'A',
|
|
499
|
+
kind: 'BASE',
|
|
500
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM table_a'
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const composite = createIndicator({
|
|
504
|
+
uuid: 'composite',
|
|
505
|
+
code: 'COMP',
|
|
506
|
+
kind: 'COMPOSITE',
|
|
507
|
+
configJson: JSON.stringify({
|
|
508
|
+
version: 1,
|
|
509
|
+
unit: 'Patients',
|
|
510
|
+
operator: 'AND',
|
|
511
|
+
indicatorAId: 'indicator-a'
|
|
512
|
+
})
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const indicators = new Map([['indicator-a', indicatorA]]);
|
|
516
|
+
const getIndicator = createMockGetIndicator(indicators);
|
|
517
|
+
|
|
518
|
+
await expect(
|
|
519
|
+
compilePopulationSql(composite, getIndicator)
|
|
520
|
+
).rejects.toThrow(MissingReferenceError);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should throw error for non-existent indicatorAId', async () => {
|
|
524
|
+
const composite = createIndicator({
|
|
525
|
+
uuid: 'composite',
|
|
526
|
+
code: 'COMP',
|
|
527
|
+
kind: 'COMPOSITE',
|
|
528
|
+
configJson: JSON.stringify({
|
|
529
|
+
version: 1,
|
|
530
|
+
unit: 'Patients',
|
|
531
|
+
operator: 'AND',
|
|
532
|
+
indicatorAId: 'non-existent',
|
|
533
|
+
indicatorBId: 'indicator-b'
|
|
534
|
+
})
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const indicatorB = createIndicator({
|
|
538
|
+
uuid: 'indicator-b',
|
|
539
|
+
code: 'B',
|
|
540
|
+
kind: 'BASE',
|
|
541
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM table_b'
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const indicators = new Map([['indicator-b', indicatorB]]);
|
|
545
|
+
const getIndicator = createMockGetIndicator(indicators);
|
|
546
|
+
|
|
547
|
+
await expect(
|
|
548
|
+
compilePopulationSql(composite, getIndicator)
|
|
549
|
+
).rejects.toThrow(MissingReferenceError);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
describe('Unsupported Operator', () => {
|
|
554
|
+
it('should throw error for unsupported operator', async () => {
|
|
555
|
+
const indicatorA = createIndicator({
|
|
556
|
+
uuid: 'indicator-a',
|
|
557
|
+
code: 'A',
|
|
558
|
+
kind: 'BASE',
|
|
559
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM table_a'
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const indicatorB = createIndicator({
|
|
563
|
+
uuid: 'indicator-b',
|
|
564
|
+
code: 'B',
|
|
565
|
+
kind: 'BASE',
|
|
566
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM table_b'
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const composite = createIndicator({
|
|
570
|
+
uuid: 'composite',
|
|
571
|
+
code: 'COMP',
|
|
572
|
+
kind: 'COMPOSITE',
|
|
573
|
+
configJson: JSON.stringify({
|
|
574
|
+
version: 1,
|
|
575
|
+
unit: 'Patients',
|
|
576
|
+
operator: 'XOR' as any, // Invalid operator
|
|
577
|
+
indicatorAId: 'indicator-a',
|
|
578
|
+
indicatorBId: 'indicator-b'
|
|
579
|
+
})
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const indicators = new Map([
|
|
583
|
+
['indicator-a', indicatorA],
|
|
584
|
+
['indicator-b', indicatorB]
|
|
585
|
+
]);
|
|
586
|
+
const getIndicator = createMockGetIndicator(indicators);
|
|
587
|
+
|
|
588
|
+
await expect(
|
|
589
|
+
compilePopulationSql(composite, getIndicator)
|
|
590
|
+
).rejects.toThrow(Error);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
describe('Retired Indicator Handling', () => {
|
|
595
|
+
it('should throw error when using retired indicator by default', async () => {
|
|
596
|
+
const retiredIndicator = createIndicator({
|
|
597
|
+
uuid: 'retired',
|
|
598
|
+
code: 'RETIRED',
|
|
599
|
+
kind: 'BASE',
|
|
600
|
+
retired: true,
|
|
601
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM retired_table'
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const composite = createIndicator({
|
|
605
|
+
uuid: 'composite',
|
|
606
|
+
code: 'COMP',
|
|
607
|
+
kind: 'COMPOSITE',
|
|
608
|
+
configJson: JSON.stringify({
|
|
609
|
+
version: 1,
|
|
610
|
+
unit: 'Patients',
|
|
611
|
+
operator: 'AND',
|
|
612
|
+
indicatorAId: 'retired',
|
|
613
|
+
indicatorBId: 'active'
|
|
614
|
+
})
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const activeIndicator = createIndicator({
|
|
618
|
+
uuid: 'active',
|
|
619
|
+
code: 'ACTIVE',
|
|
620
|
+
kind: 'BASE',
|
|
621
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM active_table'
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const indicators = new Map([
|
|
625
|
+
['retired', retiredIndicator],
|
|
626
|
+
['active', activeIndicator]
|
|
627
|
+
]);
|
|
628
|
+
const getIndicator = createMockGetIndicator(indicators);
|
|
629
|
+
|
|
630
|
+
await expect(
|
|
631
|
+
compilePopulationSql(composite, getIndicator)
|
|
632
|
+
).rejects.toThrow('Cannot use retired indicator');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('should allow retired indicator when allowRetired is true', async () => {
|
|
636
|
+
const retiredIndicator = createIndicator({
|
|
637
|
+
uuid: 'retired',
|
|
638
|
+
code: 'RETIRED',
|
|
639
|
+
kind: 'BASE',
|
|
640
|
+
retired: true,
|
|
641
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM retired_table'
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const activeIndicator = createIndicator({
|
|
645
|
+
uuid: 'active',
|
|
646
|
+
code: 'ACTIVE',
|
|
647
|
+
kind: 'BASE',
|
|
648
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM active_table'
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const composite = createIndicator({
|
|
652
|
+
uuid: 'composite',
|
|
653
|
+
code: 'COMP',
|
|
654
|
+
kind: 'COMPOSITE',
|
|
655
|
+
configJson: JSON.stringify({
|
|
656
|
+
version: 1,
|
|
657
|
+
unit: 'Patients',
|
|
658
|
+
operator: 'AND',
|
|
659
|
+
indicatorAId: 'retired',
|
|
660
|
+
indicatorBId: 'active'
|
|
661
|
+
})
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const indicators = new Map([
|
|
665
|
+
['retired', retiredIndicator],
|
|
666
|
+
['active', activeIndicator]
|
|
667
|
+
]);
|
|
668
|
+
const getIndicator = createMockGetIndicator(indicators);
|
|
669
|
+
|
|
670
|
+
const result = await compilePopulationSql(composite, getIndicator, new Set(), { allowRetired: true });
|
|
671
|
+
|
|
672
|
+
expect(result.sql).toContain('WITH A AS');
|
|
673
|
+
expect(result.warnings).toContain('Using retired indicator: RETIRED');
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
describe('Scalar Count SQL Generation', () => {
|
|
678
|
+
it('should wrap population SQL in COUNT query', () => {
|
|
679
|
+
const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
|
|
680
|
+
|
|
681
|
+
const scalarSql = generateScalarCountSql(populationSql);
|
|
682
|
+
|
|
683
|
+
expect(scalarSql).toContain('WITH base_population AS');
|
|
684
|
+
expect(scalarSql).toContain('COUNT(DISTINCT patient_id) AS total');
|
|
685
|
+
expect(scalarSql).toContain('FROM base_population');
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
describe('Age/Sex Disaggregation SQL Generation', () => {
|
|
690
|
+
it('should generate disaggregation SQL with correct structure', () => {
|
|
691
|
+
const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
|
|
692
|
+
|
|
693
|
+
const disaggSql = generateAgeSexDisaggregationSql({
|
|
694
|
+
populationSql,
|
|
695
|
+
ageCategoryCode: 'HMIS_106A',
|
|
696
|
+
genders: ['F', 'M']
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
expect(disaggSql).toContain('WITH base_pop AS');
|
|
700
|
+
expect(disaggSql).toContain('ag AS (');
|
|
701
|
+
expect(disaggSql).toContain('report_builder_dim_age_group');
|
|
702
|
+
expect(disaggSql).toContain("ac.code = 'HMIS_106A'");
|
|
703
|
+
expect(disaggSql).toContain('genders AS (');
|
|
704
|
+
expect(disaggSql).toContain("SELECT 'F' AS gender");
|
|
705
|
+
expect(disaggSql).toContain('cnt AS (');
|
|
706
|
+
expect(disaggSql).toContain('COUNT(DISTINCT base_pop.patient_id) AS value');
|
|
707
|
+
expect(disaggSql).toContain('CROSS JOIN genders g');
|
|
708
|
+
expect(disaggSql).toContain('LEFT JOIN cnt');
|
|
709
|
+
expect(disaggSql).toContain('COALESCE(cnt.value, 0)');
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('should handle only female gender', () => {
|
|
713
|
+
const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
|
|
714
|
+
|
|
715
|
+
const disaggSql = generateAgeSexDisaggregationSql({
|
|
716
|
+
populationSql,
|
|
717
|
+
ageCategoryCode: 'HTS',
|
|
718
|
+
genders: ['F']
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
expect(disaggSql).toContain("'F'");
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
describe('Edge Cases', () => {
|
|
726
|
+
it('should handle empty genders array by defaulting to both', () => {
|
|
727
|
+
const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
|
|
728
|
+
|
|
729
|
+
const disaggSql = generateAgeSexDisaggregationSql({
|
|
730
|
+
populationSql,
|
|
731
|
+
ageCategoryCode: 'HMIS_106A',
|
|
732
|
+
genders: []
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Should default to both genders
|
|
736
|
+
expect(disaggSql).toContain("'F'");
|
|
737
|
+
expect(disaggSql).toContain("'M'");
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('should escape single quotes in age category code', () => {
|
|
741
|
+
const populationSql = 'SELECT DISTINCT patient_id FROM patients WHERE active = 1';
|
|
742
|
+
|
|
743
|
+
const disaggSql = generateAgeSexDisaggregationSql({
|
|
744
|
+
populationSql,
|
|
745
|
+
ageCategoryCode: "O'Malley",
|
|
746
|
+
genders: ['F']
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
expect(disaggSql).toContain("ac.code = 'O''Malley'");
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
describe('Caching', () => {
|
|
754
|
+
it('should cache compilation results', async () => {
|
|
755
|
+
const indicator = createIndicator({
|
|
756
|
+
uuid: 'cached-indicator',
|
|
757
|
+
code: 'CACHED',
|
|
758
|
+
kind: 'BASE',
|
|
759
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM table'
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
763
|
+
|
|
764
|
+
// First call
|
|
765
|
+
const result1 = await compilePopulationSql(indicator, getIndicator);
|
|
766
|
+
|
|
767
|
+
// Clear the mock to ensure it's not called again
|
|
768
|
+
const newGetIndicator = createMockGetIndicator(new Map());
|
|
769
|
+
|
|
770
|
+
// Second call with different getIndicator (should use cache)
|
|
771
|
+
const result2 = await compilePopulationSql(indicator, newGetIndicator);
|
|
772
|
+
|
|
773
|
+
expect(result1.sql).toBe(result2.sql);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('should clear cache when requested', async () => {
|
|
777
|
+
const indicator = createIndicator({
|
|
778
|
+
uuid: 'test-uuid',
|
|
779
|
+
code: 'TEST',
|
|
780
|
+
kind: 'BASE',
|
|
781
|
+
sqlTemplate: 'SELECT DISTINCT client_id FROM table'
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
785
|
+
|
|
786
|
+
await compilePopulationSql(indicator, getIndicator);
|
|
787
|
+
clearCompilationCache();
|
|
788
|
+
|
|
789
|
+
// Should re-compile after cache clear
|
|
790
|
+
const result = await compilePopulationSql(indicator, getIndicator);
|
|
791
|
+
|
|
792
|
+
expect(result.sql).toContain('SELECT DISTINCT');
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
describe('Population SQL Validation - Regression Tests', () => {
|
|
797
|
+
it('should validate that population SQL exposes patient ID column', async () => {
|
|
798
|
+
const indicator = createIndicator({
|
|
799
|
+
kind: 'BASE',
|
|
800
|
+
sqlTemplate: 'SELECT DISTINCT a.client_id FROM table_a a WHERE a.date >= :startDate'
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
804
|
+
const result = await compilePopulationSql(indicator, getIndicator);
|
|
805
|
+
|
|
806
|
+
// Should not have an error
|
|
807
|
+
expect(result.error).toBeUndefined();
|
|
808
|
+
// Should expose client_id
|
|
809
|
+
expect(result.sql).toContain('client_id');
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it('should handle cache invalidation when SQL changes', async () => {
|
|
813
|
+
const indicator = createIndicator({
|
|
814
|
+
uuid: 'test-cache-invalidation',
|
|
815
|
+
code: 'CACHE_TEST',
|
|
816
|
+
kind: 'BASE',
|
|
817
|
+
sqlTemplate: 'SELECT COUNT(*) AS total FROM table_a a WHERE a.date >= :startDate'
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
821
|
+
|
|
822
|
+
// First compilation
|
|
823
|
+
const result1 = await compilePopulationSql(indicator, getIndicator);
|
|
824
|
+
expect(result1.sql).toContain('SELECT DISTINCT');
|
|
825
|
+
|
|
826
|
+
// Update the indicator SQL
|
|
827
|
+
indicator.sqlTemplate = 'SELECT COUNT(*) AS total FROM table_b b WHERE b.date >= :startDate';
|
|
828
|
+
|
|
829
|
+
// Second compilation should use fresh SQL (not cached)
|
|
830
|
+
const result2 = await compilePopulationSql(indicator, getIndicator);
|
|
831
|
+
expect(result2.sql).toContain('table_b');
|
|
832
|
+
expect(result2.sql).toContain('SELECT DISTINCT');
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('should include error when COUNT SQL cannot be converted properly', async () => {
|
|
836
|
+
// Create an indicator with COUNT SQL that should be converted to population SQL
|
|
837
|
+
const indicator = createIndicator({
|
|
838
|
+
kind: 'BASE',
|
|
839
|
+
sqlTemplate: 'SELECT COUNT(DISTINCT a.client_id) AS total FROM table_a a'
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
843
|
+
const result = await compilePopulationSql(indicator, getIndicator);
|
|
844
|
+
|
|
845
|
+
// The conversion should work and produce SELECT DISTINCT normalized to patient_id
|
|
846
|
+
expect(result.sql).toContain('SELECT DISTINCT');
|
|
847
|
+
expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('should handle composite indicators with validation', async () => {
|
|
851
|
+
const indicatorA = createIndicator({
|
|
852
|
+
uuid: 'indicator-a',
|
|
853
|
+
code: 'A',
|
|
854
|
+
kind: 'BASE',
|
|
855
|
+
sqlTemplate: 'SELECT DISTINCT a.client_id FROM table_a a WHERE condition_a = 1'
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
const indicatorB = createIndicator({
|
|
859
|
+
uuid: 'indicator-b',
|
|
860
|
+
code: 'B',
|
|
861
|
+
kind: 'BASE',
|
|
862
|
+
sqlTemplate: 'SELECT DISTINCT b.client_id FROM table_b b WHERE condition_b = 1'
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
const composite = createIndicator({
|
|
866
|
+
uuid: 'composite-test',
|
|
867
|
+
code: 'A_AND_B',
|
|
868
|
+
kind: 'COMPOSITE',
|
|
869
|
+
configJson: JSON.stringify({
|
|
870
|
+
version: 1,
|
|
871
|
+
unit: 'Patients',
|
|
872
|
+
operator: 'AND',
|
|
873
|
+
indicatorAId: 'indicator-a',
|
|
874
|
+
indicatorBId: 'indicator-b'
|
|
875
|
+
})
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
const indicators = new Map([
|
|
879
|
+
['indicator-a', indicatorA],
|
|
880
|
+
['indicator-b', indicatorB]
|
|
881
|
+
]);
|
|
882
|
+
const getIndicator = createMockGetIndicator(indicators);
|
|
883
|
+
|
|
884
|
+
const result = await compilePopulationSql(composite, getIndicator);
|
|
885
|
+
|
|
886
|
+
// Should not have validation errors
|
|
887
|
+
expect(result.error).toBeUndefined();
|
|
888
|
+
// Should contain population SQL
|
|
889
|
+
expect(result.sql).toContain('SELECT DISTINCT');
|
|
890
|
+
// Should not contain COUNT(*)
|
|
891
|
+
expect(result.sql).not.toContain('COUNT(*)');
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('should convert COUNT(DISTINCT a.client_id) to population SQL normalized to patient_id', async () => {
|
|
895
|
+
const indicator = createIndicator({
|
|
896
|
+
kind: 'BASE',
|
|
897
|
+
sqlTemplate: 'SELECT COUNT(DISTINCT a.client_id) AS total FROM table_a a'
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
901
|
+
const result = await compilePopulationSql(indicator, getIndicator);
|
|
902
|
+
|
|
903
|
+
// Should convert to SELECT DISTINCT and normalize to patient_id
|
|
904
|
+
expect(result.sql).toContain('SELECT DISTINCT');
|
|
905
|
+
expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
|
|
906
|
+
expect(result.sql).not.toContain('COUNT(DISTINCT');
|
|
907
|
+
expect(result.sql).not.toContain('AS total');
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('should convert COUNT(DISTINCT a.client_id) with WHERE clause', async () => {
|
|
911
|
+
const indicator = createIndicator({
|
|
912
|
+
kind: 'BASE',
|
|
913
|
+
sqlTemplate: 'SELECT COUNT(DISTINCT a.client_id) AS total FROM table_a a WHERE a.active = 1'
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
917
|
+
const result = await compilePopulationSql(indicator, getIndicator);
|
|
918
|
+
|
|
919
|
+
// Should convert to SELECT DISTINCT, preserve WHERE clause, and normalize to patient_id
|
|
920
|
+
expect(result.sql).toContain('SELECT DISTINCT');
|
|
921
|
+
expect(result.sql).toContain('WHERE a.active = 1');
|
|
922
|
+
expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('should handle COUNT(DISTINCT) with patient_id column (stays as patient_id)', async () => {
|
|
926
|
+
const indicator = createIndicator({
|
|
927
|
+
kind: 'BASE',
|
|
928
|
+
sqlTemplate: 'SELECT COUNT(DISTINCT p.patient_id) AS total FROM patients p'
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
932
|
+
const result = await compilePopulationSql(indicator, getIndicator);
|
|
933
|
+
|
|
934
|
+
// Should convert to SELECT DISTINCT and stay as patient_id
|
|
935
|
+
expect(result.sql).toContain('SELECT DISTINCT');
|
|
936
|
+
expect(result.sql).toContain('AS patient_id');
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('should handle COUNT(DISTINCT) with encounter_id column (normalized to patient_id)', async () => {
|
|
940
|
+
const indicator = createIndicator({
|
|
941
|
+
kind: 'BASE',
|
|
942
|
+
sqlTemplate: 'SELECT COUNT(DISTINCT e.encounter_id) AS total FROM encounters e'
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
946
|
+
const result = await compilePopulationSql(indicator, getIndicator);
|
|
947
|
+
|
|
948
|
+
// Should convert to SELECT DISTINCT and normalize to patient_id
|
|
949
|
+
expect(result.sql).toContain('SELECT DISTINCT');
|
|
950
|
+
expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
it('should preserve WITH CTEs when converting COUNT(DISTINCT)', async () => {
|
|
954
|
+
const indicator = createIndicator({
|
|
955
|
+
kind: 'BASE',
|
|
956
|
+
sqlTemplate: `
|
|
957
|
+
WITH base AS (
|
|
958
|
+
SELECT client_id FROM some_table
|
|
959
|
+
)
|
|
960
|
+
SELECT COUNT(DISTINCT a.client_id) AS total FROM base a
|
|
961
|
+
`.trim()
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
965
|
+
const result = await compilePopulationSql(indicator, getIndicator);
|
|
966
|
+
|
|
967
|
+
// Should preserve the WITH clause and normalize to patient_id
|
|
968
|
+
expect(result.sql).toContain('WITH base AS');
|
|
969
|
+
expect(result.sql).toContain('SELECT DISTINCT');
|
|
970
|
+
expect(result.sql).toContain('AS patient_id'); // Normalized to patient_id
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('should convert COUNT(DISTINCT) with subquery (no AS clause) and normalize to patient_id', async () => {
|
|
974
|
+
const indicator = createIndicator({
|
|
975
|
+
kind: 'BASE',
|
|
976
|
+
sqlTemplate: `
|
|
977
|
+
SELECT COUNT(DISTINCT a.client_id)
|
|
978
|
+
FROM (
|
|
979
|
+
SELECT
|
|
980
|
+
client_id,
|
|
981
|
+
TIMESTAMPDIFF(DAY, MAX(return_visit_date), :endDate) AS ltfp_days
|
|
982
|
+
FROM mamba_fact_encounter_hiv_art_card
|
|
983
|
+
WHERE encounter_date <= :endDate
|
|
984
|
+
AND return_visit_date >= :startDate
|
|
985
|
+
GROUP BY client_id
|
|
986
|
+
) a
|
|
987
|
+
`.trim()
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
const getIndicator = createMockGetIndicator(new Map());
|
|
991
|
+
const result = await compilePopulationSql(indicator, getIndicator);
|
|
992
|
+
|
|
993
|
+
// Should convert to SELECT DISTINCT and normalize to patient_id
|
|
994
|
+
expect(result.sql).toContain('SELECT DISTINCT a.client_id AS patient_id');
|
|
995
|
+
expect(result.sql).toContain('FROM (');
|
|
996
|
+
expect(result.sql).not.toContain('COUNT(DISTINCT');
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
});
|