@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,858 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive Population SQL Compiler for Indicators
|
|
3
|
+
*
|
|
4
|
+
* This module implements a recursive compiler that builds population SQL
|
|
5
|
+
* directly from indicator configuration, avoiding the fragile approach of
|
|
6
|
+
* parsing/modifying generated SQL queries.
|
|
7
|
+
*
|
|
8
|
+
* Key design principles:
|
|
9
|
+
* 1. Every indicator (atomic or composite) compiles to population SQL whose output column
|
|
10
|
+
* is always aliased AS patient_id (see convertCountToPopulation), regardless of the
|
|
11
|
+
* source indicator's configured id column
|
|
12
|
+
* 2. Scalar counts and disaggregations are wrappers around population SQL
|
|
13
|
+
* 3. Nested composite indicators are handled recursively
|
|
14
|
+
* 4. Circular dependencies are detected
|
|
15
|
+
* 5. Missing references are validated
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { IndicatorDto } from '../../../resources/indicator/indicators.api';
|
|
19
|
+
import type { CompositeOperator } from '../types/composite-indicator.types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Result of compiling an indicator to population SQL.
|
|
23
|
+
*/
|
|
24
|
+
export type PopulationCompileResult = {
|
|
25
|
+
/** The population SQL that returns the patient ID column */
|
|
26
|
+
sql: string;
|
|
27
|
+
/** The column name used for patient identification (e.g., 'client_id', 'patient_id', 'encounter_id') */
|
|
28
|
+
patientIdColumn: string;
|
|
29
|
+
/** Any warnings or information about the compilation */
|
|
30
|
+
warnings?: string[];
|
|
31
|
+
/** Whether this indicator was retired (for informational purposes) */
|
|
32
|
+
retired?: boolean;
|
|
33
|
+
/** Compilation error, if any */
|
|
34
|
+
error?: PopulationSqlError;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Error codes for population SQL compilation failures.
|
|
39
|
+
*/
|
|
40
|
+
export const POPULATION_ERRORS = {
|
|
41
|
+
AGGREGATE_SQL: 'AGGREGATE_SQL',
|
|
42
|
+
ID_NOT_EXPOSED: 'ID_NOT_EXPOSED',
|
|
43
|
+
EMPTY_SQL: 'EMPTY_SQL',
|
|
44
|
+
COMPILATION_FAILED: 'COMPILATION_FAILED'
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compiler options for population SQL generation.
|
|
49
|
+
*/
|
|
50
|
+
export type CompilerOptions = {
|
|
51
|
+
/** Whether to allow using retired indicators as operands */
|
|
52
|
+
allowRetired?: boolean;
|
|
53
|
+
/** Maximum nesting depth to prevent infinite recursion (default: 10) */
|
|
54
|
+
maxDepth?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Circular dependency error.
|
|
59
|
+
*/
|
|
60
|
+
export class CircularDependencyError extends Error {
|
|
61
|
+
constructor(public chain: string[]) {
|
|
62
|
+
super(`Circular indicator dependency detected: ${chain.join(' → ')}`);
|
|
63
|
+
this.name = 'CircularDependencyError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Missing reference error.
|
|
69
|
+
*/
|
|
70
|
+
export class MissingReferenceError extends Error {
|
|
71
|
+
constructor(
|
|
72
|
+
public parentIndicator: string,
|
|
73
|
+
public missingRef: string,
|
|
74
|
+
public refType: 'indicatorAId' | 'indicatorBId'
|
|
75
|
+
) {
|
|
76
|
+
super(`Missing ${refType} in indicator ${parentIndicator}: ${missingRef}`);
|
|
77
|
+
this.name = 'MissingReferenceError';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Unsupported operator error.
|
|
83
|
+
*/
|
|
84
|
+
export class UnsupportedOperatorError extends Error {
|
|
85
|
+
constructor(
|
|
86
|
+
public indicator: string,
|
|
87
|
+
public operator: string
|
|
88
|
+
) {
|
|
89
|
+
super(`Unsupported operator '${operator}' in indicator ${indicator}`);
|
|
90
|
+
this.name = 'UnsupportedOperatorError';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Population SQL error.
|
|
96
|
+
* Used when the compiler produces invalid SQL (aggregate instead of population).
|
|
97
|
+
*/
|
|
98
|
+
export class PopulationSqlError extends Error {
|
|
99
|
+
constructor(
|
|
100
|
+
public code: string,
|
|
101
|
+
message: string,
|
|
102
|
+
public indicator?: { uuid: string; name: string; code: string }
|
|
103
|
+
) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = 'PopulationSqlError';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Atomic indicator compilation error.
|
|
111
|
+
*/
|
|
112
|
+
export class AtomicIndicatorError extends Error {
|
|
113
|
+
constructor(
|
|
114
|
+
public indicator: string,
|
|
115
|
+
public reason: string
|
|
116
|
+
) {
|
|
117
|
+
super(`Cannot compile atomic indicator ${indicator}: ${reason}`);
|
|
118
|
+
this.name = 'AtomicIndicatorError';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Configuration for a composite indicator from config_json.
|
|
124
|
+
*/
|
|
125
|
+
type CompositeIndicatorConfig = {
|
|
126
|
+
version?: number;
|
|
127
|
+
unit?: 'Patients' | 'Encounters';
|
|
128
|
+
operator?: CompositeOperator;
|
|
129
|
+
indicatorAId?: string;
|
|
130
|
+
indicatorBId?: string;
|
|
131
|
+
indicatorACode?: string;
|
|
132
|
+
indicatorBCode?: string;
|
|
133
|
+
// Legacy fields that might be present
|
|
134
|
+
sqlPreview?: string;
|
|
135
|
+
sqlTemplate?: string;
|
|
136
|
+
base?: {
|
|
137
|
+
sqlPreview?: string;
|
|
138
|
+
sqlTemplate?: string;
|
|
139
|
+
themeConfig?: {
|
|
140
|
+
patientIdColumn?: string;
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
themeConfig?: {
|
|
144
|
+
patientIdColumn?: string;
|
|
145
|
+
};
|
|
146
|
+
authoring?: {
|
|
147
|
+
base?: {
|
|
148
|
+
sqlPreview?: string;
|
|
149
|
+
sqlTemplate?: string;
|
|
150
|
+
themeConfig?: {
|
|
151
|
+
patientIdColumn?: string;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
baseIndicator?: {
|
|
156
|
+
sqlPreview?: string;
|
|
157
|
+
sqlTemplate?: string;
|
|
158
|
+
themeConfig?: {
|
|
159
|
+
patientIdColumn?: string;
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
type ValidationResult = {
|
|
165
|
+
valid: boolean;
|
|
166
|
+
error?: { code: string; message: string };
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validates that SQL is proper population SQL (not aggregate).
|
|
171
|
+
*
|
|
172
|
+
* Population SQL must:
|
|
173
|
+
* 1. Expose the patient ID column in the SELECT clause
|
|
174
|
+
* 2. Not be an aggregate query (COUNT, SUM, AVG in final projection)
|
|
175
|
+
* 3. Be a SELECT statement (may start with WITH CTEs)
|
|
176
|
+
*
|
|
177
|
+
* @param sql - The SQL to validate
|
|
178
|
+
* @param patientIdColumn - The patient ID column name that should be exposed
|
|
179
|
+
* @returns Validation result
|
|
180
|
+
*/
|
|
181
|
+
function validatePopulationSql(sql: string, patientIdColumn: string): ValidationResult {
|
|
182
|
+
const trimmed = sql.trim();
|
|
183
|
+
|
|
184
|
+
// Check for empty SQL
|
|
185
|
+
if (!trimmed) {
|
|
186
|
+
return {
|
|
187
|
+
valid: false,
|
|
188
|
+
error: { code: POPULATION_ERRORS.EMPTY_SQL, message: 'Population SQL is empty' }
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check it's a SELECT statement (may have WITH CTEs first)
|
|
193
|
+
const upperSql = trimmed.toUpperCase();
|
|
194
|
+
if (!upperSql.startsWith('WITH') && !upperSql.startsWith('SELECT')) {
|
|
195
|
+
return {
|
|
196
|
+
valid: false,
|
|
197
|
+
error: { code: POPULATION_ERRORS.COMPILATION_FAILED, message: 'Population SQL must be a SELECT statement' }
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for aggregate functions in final projection
|
|
202
|
+
// This pattern matches SELECT COUNT(...), SELECT SUM(...), etc. at the start (after WITH)
|
|
203
|
+
// Need to skip past WITH clauses when checking
|
|
204
|
+
let sqlToCheck = trimmed;
|
|
205
|
+
if (upperSql.startsWith('WITH')) {
|
|
206
|
+
// Find the main SELECT after the CTEs
|
|
207
|
+
const selectMatch = trimmed.match(/\)\s*SELECT\s+/i);
|
|
208
|
+
if (selectMatch) {
|
|
209
|
+
const selectIndex = trimmed.indexOf(selectMatch[0]) + selectMatch[0].length;
|
|
210
|
+
sqlToCheck = trimmed.substring(selectIndex).trim();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check for aggregate in final projection
|
|
215
|
+
const aggregatePattern = /^SELECT\s+(?:COUNT|SUM|AVG|MAX|MIN)\s*\(/i;
|
|
216
|
+
if (aggregatePattern.test(sqlToCheck)) {
|
|
217
|
+
return {
|
|
218
|
+
valid: false,
|
|
219
|
+
error: {
|
|
220
|
+
code: POPULATION_ERRORS.AGGREGATE_SQL,
|
|
221
|
+
message: 'Population SQL cannot use aggregate functions (COUNT, SUM, AVG) in final projection. Use SELECT DISTINCT instead.'
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check that patient ID column is exposed
|
|
227
|
+
// Pattern should match SELECT DISTINCT ... patient_idColumn ... or SELECT ... AS patient_idColumn
|
|
228
|
+
const columnPattern = new RegExp(
|
|
229
|
+
`SELECT\\s+DISTINCT\\s+[\\w\\.]+\\s+AS\\s+${patientIdColumn}\\b|` +
|
|
230
|
+
`SELECT\\s+DISTINCT\\s+(?:[\\w]+\\.)?${patientIdColumn}\\b`,
|
|
231
|
+
'i'
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Also check if it's in the main SELECT (after WITH)
|
|
235
|
+
if (upperSql.startsWith('WITH')) {
|
|
236
|
+
const mainSelectPattern = new RegExp(`\\bSELECT\\s+DISTINCT\\s+(?:[\\w]+\\.)?${patientIdColumn}\\b`, 'i');
|
|
237
|
+
if (!mainSelectPattern.test(trimmed) && !columnPattern.test(trimmed)) {
|
|
238
|
+
return {
|
|
239
|
+
valid: false,
|
|
240
|
+
error: {
|
|
241
|
+
code: POPULATION_ERRORS.ID_NOT_EXPOSED,
|
|
242
|
+
message: `Population SQL must expose '${patientIdColumn}' column in the main SELECT (e.g., SELECT DISTINCT a.${patientIdColumn} AS ${patientIdColumn})`
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
} else if (!columnPattern.test(trimmed)) {
|
|
247
|
+
return {
|
|
248
|
+
valid: false,
|
|
249
|
+
error: {
|
|
250
|
+
code: POPULATION_ERRORS.ID_NOT_EXPOSED,
|
|
251
|
+
message: `Population SQL must expose '${patientIdColumn}' column (e.g., SELECT DISTINCT a.${patientIdColumn} AS ${patientIdColumn})`
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { valid: true };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Simple hash function for strings.
|
|
261
|
+
* Used to create cache keys from SQL content.
|
|
262
|
+
*/
|
|
263
|
+
function hashString(str: string): string {
|
|
264
|
+
let hash = 0;
|
|
265
|
+
for (let i = 0; i < str.length; i++) {
|
|
266
|
+
const char = str.charCodeAt(i);
|
|
267
|
+
hash = ((hash << 5) - hash) + char;
|
|
268
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
269
|
+
}
|
|
270
|
+
return Math.abs(hash).toString(36);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Generates a cache key for an indicator.
|
|
275
|
+
* Includes UUID, SQL content hash, and config hash to prevent stale results.
|
|
276
|
+
*/
|
|
277
|
+
function getCacheKey(indicator: IndicatorDto, options: CompilerOptions): string {
|
|
278
|
+
const sqlHash = hashString(indicator.sqlTemplate || '');
|
|
279
|
+
const configHash = hashString(indicator.configJson || '');
|
|
280
|
+
const allowRetired = options.allowRetired ? 'allowRetired' : '';
|
|
281
|
+
return `${indicator.uuid}|${sqlHash}|${configHash}|${allowRetired}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Cache to avoid re-compiling the same indicator multiple times.
|
|
286
|
+
* Map<cacheKey, PopulationCompileResult>
|
|
287
|
+
*/
|
|
288
|
+
const compilationCache = new Map<string, PopulationCompileResult>();
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Clear the compilation cache. Useful for testing or when indicators change.
|
|
292
|
+
*/
|
|
293
|
+
export function clearCompilationCache(): void {
|
|
294
|
+
compilationCache.clear();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Recursively compile an indicator to population SQL.
|
|
299
|
+
*
|
|
300
|
+
* This function follows these rules:
|
|
301
|
+
* 1. Check cache first
|
|
302
|
+
* 2. Detect circular dependencies
|
|
303
|
+
* 3. Parse config_json to get configuration
|
|
304
|
+
* 4. For COMPOSITE: recursively compile A and B, then combine
|
|
305
|
+
* 5. For BASE: extract population SQL from sql_template
|
|
306
|
+
* 6. Fix common typos like `:stratDate` -> `:startDate`
|
|
307
|
+
*
|
|
308
|
+
* @param indicator - The indicator to compile
|
|
309
|
+
* @param getIndicator - Function to fetch referenced indicators by UUID
|
|
310
|
+
* @param visited - Set of already-visited indicator UUIDs (for cycle detection)
|
|
311
|
+
* @param options - Compiler options
|
|
312
|
+
* @returns Population SQL that returns client_id
|
|
313
|
+
*/
|
|
314
|
+
export async function compilePopulationSql(
|
|
315
|
+
indicator: IndicatorDto,
|
|
316
|
+
getIndicator: (uuid: string) => Promise<IndicatorDto | null>,
|
|
317
|
+
visited: Set<string> = new Set(),
|
|
318
|
+
options: CompilerOptions = {}
|
|
319
|
+
): Promise<PopulationCompileResult> {
|
|
320
|
+
const { allowRetired = false, maxDepth = 10 } = options;
|
|
321
|
+
|
|
322
|
+
// Check cache using improved key (includes SQL content hash)
|
|
323
|
+
const cacheKey = getCacheKey(indicator, options);
|
|
324
|
+
if (compilationCache.has(cacheKey)) {
|
|
325
|
+
return compilationCache.get(cacheKey)!;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Check depth limit
|
|
329
|
+
if (visited.size >= maxDepth) {
|
|
330
|
+
throw new Error(`Maximum compilation depth ${maxDepth} exceeded. Possible circular dependency.`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check for circular dependency
|
|
334
|
+
if (visited.has(indicator.uuid)) {
|
|
335
|
+
throw new CircularDependencyError([...visited, indicator.uuid]);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Add to visited set
|
|
339
|
+
const newVisited = new Set(visited);
|
|
340
|
+
newVisited.add(indicator.uuid);
|
|
341
|
+
|
|
342
|
+
// Check if retired
|
|
343
|
+
if (indicator.retired && !allowRetired) {
|
|
344
|
+
throw new Error(`Cannot use retired indicator ${indicator.code || indicator.uuid} as operand`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const warnings: string[] = [];
|
|
348
|
+
if (indicator.retired) {
|
|
349
|
+
warnings.push(`Using retired indicator: ${indicator.code || indicator.name}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Parse config_json
|
|
353
|
+
let config: CompositeIndicatorConfig | null = null;
|
|
354
|
+
try {
|
|
355
|
+
config = indicator.configJson ? JSON.parse(indicator.configJson) : null;
|
|
356
|
+
} catch (e) {
|
|
357
|
+
warnings.push(`Failed to parse config_json: ${e}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Handle different indicator types
|
|
361
|
+
if (indicator.kind === 'COMPOSITE' && config) {
|
|
362
|
+
// Composite indicator - compile recursively
|
|
363
|
+
const result = await compileCompositePopulation(indicator, config, getIndicator, newVisited, options);
|
|
364
|
+
result.warnings = [...warnings, ...(result.warnings || [])];
|
|
365
|
+
compilationCache.set(cacheKey, result);
|
|
366
|
+
return result;
|
|
367
|
+
} else if (indicator.kind === 'BASE') {
|
|
368
|
+
// Base indicator - extract population SQL
|
|
369
|
+
const result = compileBasePopulation(indicator, config);
|
|
370
|
+
result.warnings = warnings;
|
|
371
|
+
compilationCache.set(cacheKey, result);
|
|
372
|
+
return result;
|
|
373
|
+
} else {
|
|
374
|
+
// FINAL indicator or unsupported type
|
|
375
|
+
// For FINAL indicators, we might need different handling
|
|
376
|
+
// For now, try to extract population SQL like a base indicator
|
|
377
|
+
try {
|
|
378
|
+
const result = compileBasePopulation(indicator, config);
|
|
379
|
+
result.warnings = [...warnings, 'FINAL indicator treated as BASE'];
|
|
380
|
+
compilationCache.set(cacheKey, result);
|
|
381
|
+
return result;
|
|
382
|
+
} catch (e) {
|
|
383
|
+
throw new AtomicIndicatorError(
|
|
384
|
+
indicator.code || indicator.uuid,
|
|
385
|
+
`Unsupported indicator kind: ${indicator.kind}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Compile a composite indicator's population SQL.
|
|
393
|
+
*/
|
|
394
|
+
async function compileCompositePopulation(
|
|
395
|
+
indicator: IndicatorDto,
|
|
396
|
+
config: CompositeIndicatorConfig,
|
|
397
|
+
getIndicator: (uuid: string) => Promise<IndicatorDto | null>,
|
|
398
|
+
visited: Set<string>,
|
|
399
|
+
options: CompilerOptions
|
|
400
|
+
): Promise<PopulationCompileResult> {
|
|
401
|
+
const warnings: string[] = [];
|
|
402
|
+
|
|
403
|
+
// Validate required fields
|
|
404
|
+
if (!config.indicatorAId) {
|
|
405
|
+
throw new MissingReferenceError(
|
|
406
|
+
indicator.code || indicator.uuid,
|
|
407
|
+
'missing',
|
|
408
|
+
'indicatorAId'
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!config.indicatorBId) {
|
|
413
|
+
throw new MissingReferenceError(
|
|
414
|
+
indicator.code || indicator.uuid,
|
|
415
|
+
'missing',
|
|
416
|
+
'indicatorBId'
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!config.operator) {
|
|
421
|
+
throw new UnsupportedOperatorError(indicator.code || indicator.uuid, 'missing');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Get referenced indicators
|
|
425
|
+
const indicatorA = await getIndicator(config.indicatorAId);
|
|
426
|
+
const indicatorB = await getIndicator(config.indicatorBId);
|
|
427
|
+
|
|
428
|
+
if (!indicatorA) {
|
|
429
|
+
throw new MissingReferenceError(
|
|
430
|
+
indicator.code || indicator.uuid,
|
|
431
|
+
config.indicatorAId,
|
|
432
|
+
'indicatorAId'
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!indicatorB) {
|
|
437
|
+
throw new MissingReferenceError(
|
|
438
|
+
indicator.code || indicator.uuid,
|
|
439
|
+
config.indicatorBId,
|
|
440
|
+
'indicatorBId'
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Recursively compile operand A
|
|
445
|
+
const resultA = await compilePopulationSql(indicatorA, getIndicator, visited, options);
|
|
446
|
+
|
|
447
|
+
// Recursively compile operand B
|
|
448
|
+
const resultB = await compilePopulationSql(indicatorB, getIndicator, visited, options);
|
|
449
|
+
|
|
450
|
+
// Combine with the appropriate operator
|
|
451
|
+
const sql = combineWithOperator(
|
|
452
|
+
config.operator!,
|
|
453
|
+
resultA.sql,
|
|
454
|
+
resultB.sql
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Validate the combined SQL
|
|
458
|
+
const validation = validatePopulationSql(sql, resultA.patientIdColumn);
|
|
459
|
+
if (!validation.valid) {
|
|
460
|
+
return {
|
|
461
|
+
sql,
|
|
462
|
+
patientIdColumn: resultA.patientIdColumn,
|
|
463
|
+
warnings: [...warnings, ...(resultA.warnings || []), ...(resultB.warnings || [])],
|
|
464
|
+
retired: indicator.retired,
|
|
465
|
+
error: new PopulationSqlError(
|
|
466
|
+
validation.error.code,
|
|
467
|
+
validation.error.message,
|
|
468
|
+
{ uuid: indicator.uuid, name: indicator.name, code: indicator.code || indicator.uuid }
|
|
469
|
+
)
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
sql,
|
|
475
|
+
patientIdColumn: resultA.patientIdColumn, // Use the patientIdColumn from operand A
|
|
476
|
+
warnings: [...warnings, ...(resultA.warnings || []), ...(resultB.warnings || [])],
|
|
477
|
+
retired: indicator.retired
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Combine two population SQL queries with the given operator.
|
|
483
|
+
*
|
|
484
|
+
* Handles the case where population SQL may already contain WITH clauses
|
|
485
|
+
* by using unique CTE names that won't conflict with inner CTEs.
|
|
486
|
+
*/
|
|
487
|
+
function combineWithOperator(
|
|
488
|
+
operator: CompositeOperator,
|
|
489
|
+
populationSqlA: string,
|
|
490
|
+
populationSqlB: string
|
|
491
|
+
): string {
|
|
492
|
+
// Contract: convertCountToPopulation normalizes ALL population SQL to expose a single
|
|
493
|
+
// output column aliased AS patient_id, so the combine step must reference patient_id
|
|
494
|
+
// regardless of the source indicators' configured id column or unit.
|
|
495
|
+
const idField = 'patient_id';
|
|
496
|
+
|
|
497
|
+
// Clean the SQL - remove trailing semicolons
|
|
498
|
+
const cleanA = populationSqlA.trim().replace(/;+\s*$/, '');
|
|
499
|
+
const cleanB = populationSqlB.trim().replace(/;+\s*$/, '');
|
|
500
|
+
|
|
501
|
+
// Check if the population SQL contains WITH clauses (nested CTEs)
|
|
502
|
+
const aHasWith = /^\s*WITH\s+/i.test(cleanA);
|
|
503
|
+
const bHasWith = /^\s*WITH\s+/i.test(cleanB);
|
|
504
|
+
|
|
505
|
+
// Use unique CTE names to avoid conflicts with inner CTEs
|
|
506
|
+
// If the population SQL has WITH clauses, use more specific names
|
|
507
|
+
const aName = aHasWith ? 'operand_a_base' : 'A';
|
|
508
|
+
const bName = bHasWith ? 'operand_b_base' : 'B';
|
|
509
|
+
|
|
510
|
+
if (operator === 'AND') {
|
|
511
|
+
// Intersection: A INNER JOIN B on client_id
|
|
512
|
+
return `
|
|
513
|
+
WITH ${aName} AS (
|
|
514
|
+
${indent(cleanA, 0)}
|
|
515
|
+
),
|
|
516
|
+
${bName} AS (
|
|
517
|
+
${indent(cleanB, 0)}
|
|
518
|
+
)
|
|
519
|
+
SELECT DISTINCT
|
|
520
|
+
${aName}.${idField}
|
|
521
|
+
FROM ${aName}
|
|
522
|
+
INNER JOIN ${bName}
|
|
523
|
+
ON ${bName}.${idField} = ${aName}.${idField}
|
|
524
|
+
`.trim();
|
|
525
|
+
} else if (operator === 'OR') {
|
|
526
|
+
// Union: A UNION B
|
|
527
|
+
return `
|
|
528
|
+
WITH ${aName} AS (
|
|
529
|
+
${indent(cleanA, 0)}
|
|
530
|
+
),
|
|
531
|
+
${bName} AS (
|
|
532
|
+
${indent(cleanB, 0)}
|
|
533
|
+
)
|
|
534
|
+
SELECT ${idField} FROM ${aName}
|
|
535
|
+
UNION
|
|
536
|
+
SELECT ${idField} FROM ${bName}
|
|
537
|
+
`.trim();
|
|
538
|
+
} else if (operator === 'A_AND_NOT_B') {
|
|
539
|
+
// Set difference: A LEFT JOIN B WHERE B IS NULL
|
|
540
|
+
return `
|
|
541
|
+
WITH ${aName} AS (
|
|
542
|
+
${indent(cleanA, 0)}
|
|
543
|
+
),
|
|
544
|
+
${bName} AS (
|
|
545
|
+
${indent(cleanB, 0)}
|
|
546
|
+
)
|
|
547
|
+
SELECT DISTINCT
|
|
548
|
+
${aName}.${idField}
|
|
549
|
+
FROM ${aName}
|
|
550
|
+
LEFT JOIN ${bName}
|
|
551
|
+
ON ${bName}.${idField} = ${aName}.${idField}
|
|
552
|
+
WHERE ${bName}.${idField} IS NULL
|
|
553
|
+
`.trim();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Compile a base indicator's population SQL.
|
|
561
|
+
*
|
|
562
|
+
* For base indicators, we extract the population SQL from the sql_template
|
|
563
|
+
* or from config_json fields. The population SQL should return the patient ID column.
|
|
564
|
+
*/
|
|
565
|
+
function compileBasePopulation(
|
|
566
|
+
indicator: IndicatorDto,
|
|
567
|
+
config: CompositeIndicatorConfig | null
|
|
568
|
+
): PopulationCompileResult {
|
|
569
|
+
// Try to get SQL from various sources
|
|
570
|
+
let sql = indicator.sqlTemplate || '';
|
|
571
|
+
|
|
572
|
+
if (!sql && config) {
|
|
573
|
+
// Try config_json fields
|
|
574
|
+
sql = config.sqlPreview || config.sqlTemplate || config.base?.sqlPreview || config.base?.sqlTemplate || '';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!sql) {
|
|
578
|
+
throw new AtomicIndicatorError(
|
|
579
|
+
indicator.code || indicator.uuid,
|
|
580
|
+
'No SQL template found'
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Fix common typos
|
|
585
|
+
sql = fixCommonTypos(sql);
|
|
586
|
+
|
|
587
|
+
// Get the patient ID column from config (e.g., 'client_id' for HIV indicators)
|
|
588
|
+
const patientIdColumn = getPatientIdColumnFromConfig(config);
|
|
589
|
+
|
|
590
|
+
// Convert COUNT SQL to population SQL if needed
|
|
591
|
+
const populationSql = convertCountToPopulation(sql, patientIdColumn);
|
|
592
|
+
|
|
593
|
+
// Validate the output is proper population SQL
|
|
594
|
+
const validation = validatePopulationSql(populationSql, patientIdColumn);
|
|
595
|
+
if (!validation.valid) {
|
|
596
|
+
return {
|
|
597
|
+
sql: populationSql,
|
|
598
|
+
patientIdColumn,
|
|
599
|
+
retired: indicator.retired,
|
|
600
|
+
error: new PopulationSqlError(
|
|
601
|
+
validation.error.code,
|
|
602
|
+
validation.error.message,
|
|
603
|
+
{ uuid: indicator.uuid, name: indicator.name, code: indicator.code || indicator.uuid }
|
|
604
|
+
)
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
sql: populationSql,
|
|
610
|
+
patientIdColumn,
|
|
611
|
+
retired: indicator.retired
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get the patient ID column from indicator config.
|
|
617
|
+
* Defaults to 'client_id' if not specified.
|
|
618
|
+
*/
|
|
619
|
+
function getPatientIdColumnFromConfig(config: CompositeIndicatorConfig | null): string {
|
|
620
|
+
try {
|
|
621
|
+
const cfg =
|
|
622
|
+
config?.themeConfig ||
|
|
623
|
+
config?.base?.themeConfig ||
|
|
624
|
+
config?.authoring?.base?.themeConfig ||
|
|
625
|
+
config?.baseIndicator?.themeConfig ||
|
|
626
|
+
null;
|
|
627
|
+
const pid = cfg?.patientIdColumn;
|
|
628
|
+
return pid ? String(pid) : 'client_id';
|
|
629
|
+
} catch {
|
|
630
|
+
return 'client_id';
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Convert COUNT SQL to population SQL.
|
|
636
|
+
*
|
|
637
|
+
* This handles the case where a base indicator has COUNT(*) SQL
|
|
638
|
+
* or COUNT(DISTINCT column) SQL and needs to be converted to return
|
|
639
|
+
* patient_id or client_id instead.
|
|
640
|
+
*
|
|
641
|
+
* IMPORTANT: The output column is ALWAYS aliased as the patientIdColumn value
|
|
642
|
+
* (e.g., 'client_id' or 'patient_id') for consistency with downstream queries.
|
|
643
|
+
*/
|
|
644
|
+
function convertCountToPopulation(sql: string, patientIdColumn: string = 'client_id'): string {
|
|
645
|
+
let trimmed = sql.trim();
|
|
646
|
+
|
|
647
|
+
// Handle escaped newlines BEFORE pattern matching
|
|
648
|
+
// This ensures multi-line SQL with GROUP BY, HAVING, etc. on separate lines is preserved
|
|
649
|
+
trimmed = trimmed.replace(/\\n/g, '\n');
|
|
650
|
+
|
|
651
|
+
// Remove any trailing semicolons first (they cause issues when used as CTE)
|
|
652
|
+
const withoutSemicolon = trimmed.replace(/;+\s*$/, '');
|
|
653
|
+
|
|
654
|
+
// Check if it's already a population query (SELECT DISTINCT with patient_id, client_id, or encounter_id)
|
|
655
|
+
const populationCheck = new RegExp(
|
|
656
|
+
`SELECT\\s+DISTINCT\\s+(?:\\w+\\.)?(?:${patientIdColumn}|client_id|patient_id|encounter_id)`,
|
|
657
|
+
'i'
|
|
658
|
+
);
|
|
659
|
+
if (populationCheck.test(withoutSemicolon)) {
|
|
660
|
+
// Ensure the output is normalized to patient_id for disaggregation compatibility
|
|
661
|
+
const fixed = fixCommonTypos(withoutSemicolon);
|
|
662
|
+
// If it's already aliasing as patient_id, return as-is
|
|
663
|
+
const aliasCheck = new RegExp(`AS\\s+patient_id\\b`, 'im');
|
|
664
|
+
if (aliasCheck.test(fixed)) {
|
|
665
|
+
return fixed;
|
|
666
|
+
}
|
|
667
|
+
// Fix the alias to normalize to patient_id (handles client_id, encounter_id, etc.)
|
|
668
|
+
// Handles both qualified (a.client_id) and unqualified (client_id) column references,
|
|
669
|
+
// with or without an existing alias. The contract requires EVERY population SQL to
|
|
670
|
+
// expose patient_id, otherwise combineWithOperator/disaggregation reference a column
|
|
671
|
+
// the CTE does not expose.
|
|
672
|
+
const replacePattern = /SELECT\s+DISTINCT\s+(?:(\w+)\.)?(client_id|patient_id|encounter_id)(?:\s+AS\s+\w+)?/i;
|
|
673
|
+
return fixed.replace(
|
|
674
|
+
replacePattern,
|
|
675
|
+
(_match, tableAlias: string | undefined, column: string) =>
|
|
676
|
+
`SELECT DISTINCT ${tableAlias ? `${tableAlias}.` : ''}${column} AS patient_id`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Check for COUNT(DISTINCT column) pattern - e.g., SELECT COUNT(DISTINCT a.client_id) AS total FROM ...
|
|
681
|
+
// Also handles patterns without AS or with subqueries: SELECT COUNT(DISTINCT a.client_id) FROM (...)
|
|
682
|
+
const countDistinctPattern = /SELECT\s+COUNT\s*\(\s*DISTINCT\s+(\w+\.(?:client_id|patient_id|encounter_id))\s*\)(?:\s+AS\s+\w+)?\s+FROM/i;
|
|
683
|
+
const countDistinctMatch = withoutSemicolon.match(countDistinctPattern);
|
|
684
|
+
if (countDistinctMatch) {
|
|
685
|
+
const columnReference = countDistinctMatch[1]; // e.g., "a.client_id"
|
|
686
|
+
// Extract just the column name without the alias
|
|
687
|
+
const columnName = columnReference.split('.').pop()!;
|
|
688
|
+
const alias = columnReference.split('.')[0];
|
|
689
|
+
|
|
690
|
+
// Replace with SELECT DISTINCT alias.columnName AS patient_id FROM
|
|
691
|
+
// Keep the rest of the SQL after FROM (including subqueries)
|
|
692
|
+
const result = withoutSemicolon.replace(
|
|
693
|
+
/SELECT\s+COUNT\s*\(\s*DISTINCT\s+\w+\.(?:client_id|patient_id|encounter_id)\s*\)(?:\s+AS\s+\w+)?\s+FROM/i,
|
|
694
|
+
`SELECT DISTINCT ${alias}.${columnName} AS patient_id FROM`
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
return fixCommonTypos(result);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Check for COUNT(*) pattern - e.g., SELECT COUNT(*) AS total FROM ...
|
|
701
|
+
const countPattern = /SELECT\s+.*?COUNT\s*\(\s*\*\s*\)\s+AS\s+total/i;
|
|
702
|
+
if (countPattern.test(withoutSemicolon)) {
|
|
703
|
+
// Try to extract the table and alias from the FROM clause
|
|
704
|
+
const fromMatch = withoutSemicolon.match(/FROM\s+(\S+)\s+(\S+)/i);
|
|
705
|
+
if (fromMatch) {
|
|
706
|
+
const alias = fromMatch[2];
|
|
707
|
+
|
|
708
|
+
// Build population SQL - normalize to patient_id
|
|
709
|
+
const result = withoutSemicolon
|
|
710
|
+
.replace(/SELECT\s+.*?COUNT\s*\(\s*\*\s*\)\s+AS\s+total\s*/i, '')
|
|
711
|
+
.replace(/FROM\s+/i, `SELECT DISTINCT ${alias}.${patientIdColumn} AS patient_id FROM `);
|
|
712
|
+
|
|
713
|
+
return fixCommonTypos(result);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// If we can't parse it, try a simple replacement - normalize to patient_id
|
|
717
|
+
const result = withoutSemicolon.replace(
|
|
718
|
+
/SELECT\s+.*?COUNT\s*\(\s*\*\s*\)\s+AS\s+total\s+FROM\s+/i,
|
|
719
|
+
`SELECT DISTINCT patient_id FROM `
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
return fixCommonTypos(result);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Not a COUNT query we recognize - assume it's already correct but fix typos
|
|
726
|
+
return fixCommonTypos(withoutSemicolon);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Fix common typos in generated SQL.
|
|
731
|
+
*/
|
|
732
|
+
function fixCommonTypos(sql: string): string {
|
|
733
|
+
let fixed = sql;
|
|
734
|
+
// Handle escaped newlines - convert literal \n to actual newlines
|
|
735
|
+
// This can happen when SQL is serialized through JSON in the backend
|
|
736
|
+
fixed = fixed.replace(/\\n/g, '\n');
|
|
737
|
+
// Fix :stratDate -> :startDate
|
|
738
|
+
fixed = fixed.replace(/:stratDate\b/g, ':startDate');
|
|
739
|
+
// Remove trailing semicolons - population SQL used as CTE shouldn't have semicolons
|
|
740
|
+
fixed = fixed.replace(/;+\s*$/, '');
|
|
741
|
+
return fixed;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Indent SQL by a number of spaces.
|
|
746
|
+
*/
|
|
747
|
+
function indent(sql: string, spaces: number): string {
|
|
748
|
+
const pad = ' '.repeat(spaces);
|
|
749
|
+
return sql
|
|
750
|
+
.split('\n')
|
|
751
|
+
.map((line) => (line.trim() ? pad + line : line))
|
|
752
|
+
.join('\n');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Generate scalar COUNT SQL from population SQL.
|
|
757
|
+
*
|
|
758
|
+
* This wraps the population SQL in a COUNT query.
|
|
759
|
+
* Population SQL must expose the patient_id column per the compiler contract.
|
|
760
|
+
*/
|
|
761
|
+
export function generateScalarCountSql(populationSql: string): string {
|
|
762
|
+
const clean = populationSql.trim().replace(/;+\s*$/, '');
|
|
763
|
+
|
|
764
|
+
return `
|
|
765
|
+
WITH base_population AS (
|
|
766
|
+
${indent(clean, 0)}
|
|
767
|
+
)
|
|
768
|
+
SELECT
|
|
769
|
+
COUNT(DISTINCT patient_id) AS total
|
|
770
|
+
FROM base_population;
|
|
771
|
+
`.trim();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Generate age/sex disaggregation SQL from population SQL.
|
|
776
|
+
*
|
|
777
|
+
* This wraps the population SQL in the disaggregation CTE structure.
|
|
778
|
+
*
|
|
779
|
+
* @deprecated The gender defaulting behavior (defaulting to both F and M when none selected)
|
|
780
|
+
* will be removed in a future version. Always specify genders explicitly.
|
|
781
|
+
*/
|
|
782
|
+
export function generateAgeSexDisaggregationSql(args: {
|
|
783
|
+
populationSql: string;
|
|
784
|
+
ageCategoryCode: string;
|
|
785
|
+
genders: Array<'F' | 'M'>;
|
|
786
|
+
}): string {
|
|
787
|
+
const { populationSql, ageCategoryCode, genders } = args;
|
|
788
|
+
|
|
789
|
+
// Default to both genders if none selected (for backward compatibility)
|
|
790
|
+
// TODO: Remove this default in a future version and require explicit gender selection
|
|
791
|
+
const selectedGenders = (genders || []).length ? genders : (['F', 'M'] as Array<'F' | 'M'>);
|
|
792
|
+
|
|
793
|
+
// Log deprecation warning when defaulting
|
|
794
|
+
if (!genders || genders.length === 0) {
|
|
795
|
+
console.warn('[generateAgeSexDisaggregationSql] No genders specified, defaulting to both F and M. This default will be removed in a future version.');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const clean = populationSql.trim().replace(/;+\s*$/, '');
|
|
799
|
+
|
|
800
|
+
// All population SQL is normalized to patient_id, so hardcode patient_id here
|
|
801
|
+
const patientIdColumn = 'patient_id';
|
|
802
|
+
|
|
803
|
+
return `
|
|
804
|
+
WITH base_pop AS (
|
|
805
|
+
${indent(clean, 2)}
|
|
806
|
+
),
|
|
807
|
+
ag AS (
|
|
808
|
+
SELECT
|
|
809
|
+
ag.age_group_id,
|
|
810
|
+
ag.label,
|
|
811
|
+
ag.min_age_days,
|
|
812
|
+
ag.max_age_days,
|
|
813
|
+
ag.sort_order
|
|
814
|
+
FROM report_builder_dim_age_group ag
|
|
815
|
+
JOIN report_builder_dim_age_category ac
|
|
816
|
+
ON ac.age_category_id = ag.age_category_id
|
|
817
|
+
WHERE ac.code = '${escapeSql(ageCategoryCode)}'
|
|
818
|
+
AND ag.is_active = 1
|
|
819
|
+
),
|
|
820
|
+
genders AS (
|
|
821
|
+
SELECT 'F' AS gender
|
|
822
|
+
UNION ALL SELECT 'M' AS gender
|
|
823
|
+
),
|
|
824
|
+
cnt AS (
|
|
825
|
+
SELECT
|
|
826
|
+
ag.age_group_id AS age_group_id,
|
|
827
|
+
mdp.gender AS gender,
|
|
828
|
+
COUNT(DISTINCT base_pop.${patientIdColumn}) AS value
|
|
829
|
+
FROM base_pop
|
|
830
|
+
JOIN mamba_fact_patients_latest_patient_demographics mdp
|
|
831
|
+
ON mdp.${patientIdColumn} = base_pop.${patientIdColumn}
|
|
832
|
+
JOIN ag
|
|
833
|
+
ON TIMESTAMPDIFF(DAY, mdp.birthdate, :endDate)
|
|
834
|
+
BETWEEN ag.min_age_days AND ag.max_age_days
|
|
835
|
+
WHERE mdp.birthdate IS NOT NULL
|
|
836
|
+
AND mdp.gender IS NOT NULL
|
|
837
|
+
AND mdp.gender IN (${selectedGenders.map((g) => `'${g}'`).join(',')})
|
|
838
|
+
GROUP BY ag.age_group_id, mdp.gender
|
|
839
|
+
)
|
|
840
|
+
SELECT
|
|
841
|
+
ag.label AS age_group,
|
|
842
|
+
g.gender AS gender,
|
|
843
|
+
COALESCE(cnt.value, 0) AS value
|
|
844
|
+
FROM ag
|
|
845
|
+
CROSS JOIN genders g
|
|
846
|
+
LEFT JOIN cnt
|
|
847
|
+
ON cnt.age_group_id = ag.age_group_id
|
|
848
|
+
AND cnt.gender = g.gender
|
|
849
|
+
ORDER BY ag.sort_order, g.gender;
|
|
850
|
+
`.trim();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Escape SQL literal values.
|
|
855
|
+
*/
|
|
856
|
+
function escapeSql(s: string): string {
|
|
857
|
+
return String(s).replace(/'/g, "''");
|
|
858
|
+
}
|