fhirpath-ts 0.0.0-bootstrap.0
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/LICENSE +201 -0
- package/README.md +391 -0
- package/THIRD-PARTY-NOTICES.md +296 -0
- package/dist/analyzer/analyze-dto.d.ts +45 -0
- package/dist/analyzer/analyze-dto.d.ts.map +1 -0
- package/dist/analyzer/analyze-dto.js +147 -0
- package/dist/analyzer/analyze-dto.js.map +1 -0
- package/dist/analyzer/analyze.d.ts +98 -0
- package/dist/analyzer/analyze.d.ts.map +1 -0
- package/dist/analyzer/analyze.js +925 -0
- package/dist/analyzer/analyze.js.map +1 -0
- package/dist/analyzer/expression-policy.d.ts +210 -0
- package/dist/analyzer/expression-policy.d.ts.map +1 -0
- package/dist/analyzer/expression-policy.js +254 -0
- package/dist/analyzer/expression-policy.js.map +1 -0
- package/dist/analyzer/index.d.ts +3 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +6 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/analyzer/regex-safety.d.ts +10 -0
- package/dist/analyzer/regex-safety.d.ts.map +1 -0
- package/dist/analyzer/regex-safety.js +84 -0
- package/dist/analyzer/regex-safety.js.map +1 -0
- package/dist/analyzer/signatures.d.ts +72 -0
- package/dist/analyzer/signatures.d.ts.map +1 -0
- package/dist/analyzer/signatures.js +195 -0
- package/dist/analyzer/signatures.js.map +1 -0
- package/dist/api/bundle.d.ts +29 -0
- package/dist/api/bundle.d.ts.map +1 -0
- package/dist/api/bundle.js +108 -0
- package/dist/api/bundle.js.map +1 -0
- package/dist/api/cache.d.ts +10 -0
- package/dist/api/cache.d.ts.map +1 -0
- package/dist/api/cache.js +38 -0
- package/dist/api/cache.js.map +1 -0
- package/dist/api/column-signature.d.ts +53 -0
- package/dist/api/column-signature.d.ts.map +1 -0
- package/dist/api/column-signature.js +43 -0
- package/dist/api/column-signature.js.map +1 -0
- package/dist/api/compile.d.ts +120 -0
- package/dist/api/compile.d.ts.map +1 -0
- package/dist/api/compile.js +167 -0
- package/dist/api/compile.js.map +1 -0
- package/dist/api/constraints.d.ts +48 -0
- package/dist/api/constraints.d.ts.map +1 -0
- package/dist/api/constraints.js +52 -0
- package/dist/api/constraints.js.map +1 -0
- package/dist/api/dto.d.ts +134 -0
- package/dist/api/dto.d.ts.map +1 -0
- package/dist/api/dto.js +295 -0
- package/dist/api/dto.js.map +1 -0
- package/dist/api/engine.d.ts +136 -0
- package/dist/api/engine.d.ts.map +1 -0
- package/dist/api/engine.js +183 -0
- package/dist/api/engine.js.map +1 -0
- package/dist/api/evaluate.d.ts +7 -0
- package/dist/api/evaluate.d.ts.map +1 -0
- package/dist/api/evaluate.js +10 -0
- package/dist/api/evaluate.js.map +1 -0
- package/dist/api/project.d.ts +89 -0
- package/dist/api/project.d.ts.map +1 -0
- package/dist/api/project.js +150 -0
- package/dist/api/project.js.map +1 -0
- package/dist/api/tagged.d.ts +14 -0
- package/dist/api/tagged.d.ts.map +1 -0
- package/dist/api/tagged.js +13 -0
- package/dist/api/tagged.js.map +1 -0
- package/dist/cli/dto-check.d.ts +25 -0
- package/dist/cli/dto-check.d.ts.map +1 -0
- package/dist/cli/dto-check.js +77 -0
- package/dist/cli/dto-check.js.map +1 -0
- package/dist/cli/fhirpath-check.d.ts +3 -0
- package/dist/cli/fhirpath-check.d.ts.map +1 -0
- package/dist/cli/fhirpath-check.js +149 -0
- package/dist/cli/fhirpath-check.js.map +1 -0
- package/dist/cli/ts-loader.d.mts +3 -0
- package/dist/cli/ts-loader.d.mts.map +1 -0
- package/dist/cli/ts-loader.mjs +48 -0
- package/dist/cli/ts-loader.mjs.map +1 -0
- package/dist/engine/context.d.ts +173 -0
- package/dist/engine/context.d.ts.map +1 -0
- package/dist/engine/context.js +118 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/evaluator.d.ts +7 -0
- package/dist/engine/evaluator.d.ts.map +1 -0
- package/dist/engine/evaluator.js +196 -0
- package/dist/engine/evaluator.js.map +1 -0
- package/dist/engine/navigation.d.ts +11 -0
- package/dist/engine/navigation.d.ts.map +1 -0
- package/dist/engine/navigation.js +102 -0
- package/dist/engine/navigation.js.map +1 -0
- package/dist/engine/operators/collections.d.ts +12 -0
- package/dist/engine/operators/collections.d.ts.map +1 -0
- package/dist/engine/operators/collections.js +31 -0
- package/dist/engine/operators/collections.js.map +1 -0
- package/dist/engine/operators/comparison.d.ts +13 -0
- package/dist/engine/operators/comparison.d.ts.map +1 -0
- package/dist/engine/operators/comparison.js +77 -0
- package/dist/engine/operators/comparison.js.map +1 -0
- package/dist/engine/operators/equality.d.ts +19 -0
- package/dist/engine/operators/equality.d.ts.map +1 -0
- package/dist/engine/operators/equality.js +203 -0
- package/dist/engine/operators/equality.js.map +1 -0
- package/dist/engine/operators/index.d.ts +18 -0
- package/dist/engine/operators/index.d.ts.map +1 -0
- package/dist/engine/operators/index.js +30 -0
- package/dist/engine/operators/index.js.map +1 -0
- package/dist/engine/operators/logic.d.ts +8 -0
- package/dist/engine/operators/logic.d.ts.map +1 -0
- package/dist/engine/operators/logic.js +38 -0
- package/dist/engine/operators/logic.js.map +1 -0
- package/dist/engine/operators/math.d.ts +13 -0
- package/dist/engine/operators/math.d.ts.map +1 -0
- package/dist/engine/operators/math.js +198 -0
- package/dist/engine/operators/math.js.map +1 -0
- package/dist/engine/operators/strings.d.ts +8 -0
- package/dist/engine/operators/strings.d.ts.map +1 -0
- package/dist/engine/operators/strings.js +22 -0
- package/dist/engine/operators/strings.js.map +1 -0
- package/dist/engine/operators/types.d.ts +3 -0
- package/dist/engine/operators/types.d.ts.map +1 -0
- package/dist/engine/operators/types.js +15 -0
- package/dist/engine/operators/types.js.map +1 -0
- package/dist/engine/type-matching.d.ts +20 -0
- package/dist/engine/type-matching.d.ts.map +1 -0
- package/dist/engine/type-matching.js +122 -0
- package/dist/engine/type-matching.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +21 -0
- package/dist/errors.js.map +1 -0
- package/dist/eslint/index.d.ts +12 -0
- package/dist/eslint/index.d.ts.map +1 -0
- package/dist/eslint/index.js +316 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/fhir/html-checks.d.ts +7 -0
- package/dist/fhir/html-checks.d.ts.map +1 -0
- package/dist/fhir/html-checks.js +256 -0
- package/dist/fhir/html-checks.js.map +1 -0
- package/dist/fhir/model-navigation.d.ts +11 -0
- package/dist/fhir/model-navigation.d.ts.map +1 -0
- package/dist/fhir/model-navigation.js +142 -0
- package/dist/fhir/model-navigation.js.map +1 -0
- package/dist/functions/aggregate.d.ts +2 -0
- package/dist/functions/aggregate.d.ts.map +1 -0
- package/dist/functions/aggregate.js +127 -0
- package/dist/functions/aggregate.js.map +1 -0
- package/dist/functions/boundary.d.ts +2 -0
- package/dist/functions/boundary.d.ts.map +1 -0
- package/dist/functions/boundary.js +189 -0
- package/dist/functions/boundary.js.map +1 -0
- package/dist/functions/combining.d.ts +2 -0
- package/dist/functions/combining.d.ts.map +1 -0
- package/dist/functions/combining.js +13 -0
- package/dist/functions/combining.js.map +1 -0
- package/dist/functions/control.d.ts +2 -0
- package/dist/functions/control.d.ts.map +1 -0
- package/dist/functions/control.js +25 -0
- package/dist/functions/control.js.map +1 -0
- package/dist/functions/conversion.d.ts +3 -0
- package/dist/functions/conversion.d.ts.map +1 -0
- package/dist/functions/conversion.js +316 -0
- package/dist/functions/conversion.js.map +1 -0
- package/dist/functions/date-components.d.ts +2 -0
- package/dist/functions/date-components.d.ts.map +1 -0
- package/dist/functions/date-components.js +81 -0
- package/dist/functions/date-components.js.map +1 -0
- package/dist/functions/existence.d.ts +4 -0
- package/dist/functions/existence.d.ts.map +1 -0
- package/dist/functions/existence.js +97 -0
- package/dist/functions/existence.js.map +1 -0
- package/dist/functions/fhir.d.ts +2 -0
- package/dist/functions/fhir.d.ts.map +1 -0
- package/dist/functions/fhir.js +222 -0
- package/dist/functions/fhir.js.map +1 -0
- package/dist/functions/filtering.d.ts +2 -0
- package/dist/functions/filtering.d.ts.map +1 -0
- package/dist/functions/filtering.js +112 -0
- package/dist/functions/filtering.js.map +1 -0
- package/dist/functions/install.d.ts +18 -0
- package/dist/functions/install.d.ts.map +1 -0
- package/dist/functions/install.js +20 -0
- package/dist/functions/install.js.map +1 -0
- package/dist/functions/iteration.d.ts +7 -0
- package/dist/functions/iteration.d.ts.map +1 -0
- package/dist/functions/iteration.js +9 -0
- package/dist/functions/iteration.js.map +1 -0
- package/dist/functions/math.d.ts +2 -0
- package/dist/functions/math.d.ts.map +1 -0
- package/dist/functions/math.js +152 -0
- package/dist/functions/math.js.map +1 -0
- package/dist/functions/reflection.d.ts +2 -0
- package/dist/functions/reflection.d.ts.map +1 -0
- package/dist/functions/reflection.js +23 -0
- package/dist/functions/reflection.js.map +1 -0
- package/dist/functions/registry.d.ts +28 -0
- package/dist/functions/registry.d.ts.map +1 -0
- package/dist/functions/registry.js +46 -0
- package/dist/functions/registry.js.map +1 -0
- package/dist/functions/sort.d.ts +2 -0
- package/dist/functions/sort.d.ts.map +1 -0
- package/dist/functions/sort.js +48 -0
- package/dist/functions/sort.js.map +1 -0
- package/dist/functions/string.d.ts +2 -0
- package/dist/functions/string.d.ts.map +1 -0
- package/dist/functions/string.js +353 -0
- package/dist/functions/string.js.map +1 -0
- package/dist/functions/subsetting.d.ts +2 -0
- package/dist/functions/subsetting.d.ts.map +1 -0
- package/dist/functions/subsetting.js +72 -0
- package/dist/functions/subsetting.js.map +1 -0
- package/dist/functions/tree.d.ts +8 -0
- package/dist/functions/tree.d.ts.map +1 -0
- package/dist/functions/tree.js +109 -0
- package/dist/functions/tree.js.map +1 -0
- package/dist/functions/type-specifier.d.ts +8 -0
- package/dist/functions/type-specifier.d.ts.map +1 -0
- package/dist/functions/type-specifier.js +27 -0
- package/dist/functions/type-specifier.js.map +1 -0
- package/dist/functions/utility.d.ts +2 -0
- package/dist/functions/utility.d.ts.map +1 -0
- package/dist/functions/utility.js +76 -0
- package/dist/functions/utility.js.map +1 -0
- package/dist/functions/variables.d.ts +2 -0
- package/dist/functions/variables.d.ts.map +1 -0
- package/dist/functions/variables.js +35 -0
- package/dist/functions/variables.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer/lexer.d.ts +4 -0
- package/dist/lexer/lexer.d.ts.map +1 -0
- package/dist/lexer/lexer.js +318 -0
- package/dist/lexer/lexer.js.map +1 -0
- package/dist/lexer/tokens.d.ts +23 -0
- package/dist/lexer/tokens.d.ts.map +1 -0
- package/dist/lexer/tokens.js +40 -0
- package/dist/lexer/tokens.js.map +1 -0
- package/dist/model/provider.d.ts +36 -0
- package/dist/model/provider.d.ts.map +1 -0
- package/dist/model/provider.js +2 -0
- package/dist/model/provider.js.map +1 -0
- package/dist/parser/ast.d.ts +104 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +2 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/parser.d.ts +4 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/parser/parser.js +299 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/parser/precedence.d.ts +24 -0
- package/dist/parser/precedence.d.ts.map +1 -0
- package/dist/parser/precedence.js +52 -0
- package/dist/parser/precedence.js.map +1 -0
- package/dist/parser/printer.d.ts +4 -0
- package/dist/parser/printer.d.ts.map +1 -0
- package/dist/parser/printer.js +112 -0
- package/dist/parser/printer.js.map +1 -0
- package/dist/r4/generated/resources-data.d.ts +3 -0
- package/dist/r4/generated/resources-data.d.ts.map +1 -0
- package/dist/r4/generated/resources-data.js +28628 -0
- package/dist/r4/generated/resources-data.js.map +1 -0
- package/dist/r4/generated/type-maps.d.ts +43153 -0
- package/dist/r4/generated/type-maps.d.ts.map +1 -0
- package/dist/r4/generated/type-maps.js +4 -0
- package/dist/r4/generated/type-maps.js.map +1 -0
- package/dist/r4/generated/types-data.d.ts +3 -0
- package/dist/r4/generated/types-data.d.ts.map +1 -0
- package/dist/r4/generated/types-data.js +2205 -0
- package/dist/r4/generated/types-data.js.map +1 -0
- package/dist/r4/index.d.ts +9 -0
- package/dist/r4/index.d.ts.map +1 -0
- package/dist/r4/index.js +94 -0
- package/dist/r4/index.js.map +1 -0
- package/dist/r4/model-data.d.ts +17 -0
- package/dist/r4/model-data.d.ts.map +1 -0
- package/dist/r4/model-data.js +2 -0
- package/dist/r4/model-data.js.map +1 -0
- package/dist/sites/index.d.ts +31 -0
- package/dist/sites/index.d.ts.map +1 -0
- package/dist/sites/index.js +205 -0
- package/dist/sites/index.js.map +1 -0
- package/dist/typed/infer.d.ts +234 -0
- package/dist/typed/infer.d.ts.map +1 -0
- package/dist/typed/infer.js +85 -0
- package/dist/typed/infer.js.map +1 -0
- package/dist/typed/perf-fixture.types.d.ts +136 -0
- package/dist/typed/perf-fixture.types.d.ts.map +1 -0
- package/dist/typed/perf-fixture.types.js +2 -0
- package/dist/typed/perf-fixture.types.js.map +1 -0
- package/dist/values/collection.d.ts +24 -0
- package/dist/values/collection.d.ts.map +1 -0
- package/dist/values/collection.js +51 -0
- package/dist/values/collection.js.map +1 -0
- package/dist/values/datetime.d.ts +54 -0
- package/dist/values/datetime.d.ts.map +1 -0
- package/dist/values/datetime.js +226 -0
- package/dist/values/datetime.js.map +1 -0
- package/dist/values/decimal.d.ts +48 -0
- package/dist/values/decimal.d.ts.map +1 -0
- package/dist/values/decimal.js +188 -0
- package/dist/values/decimal.js.map +1 -0
- package/dist/values/numeric.d.ts +24 -0
- package/dist/values/numeric.d.ts.map +1 -0
- package/dist/values/numeric.js +53 -0
- package/dist/values/numeric.js.map +1 -0
- package/dist/values/quantity.d.ts +49 -0
- package/dist/values/quantity.d.ts.map +1 -0
- package/dist/values/quantity.js +297 -0
- package/dist/values/quantity.js.map +1 -0
- package/dist/values/temporal-arithmetic.d.ts +17 -0
- package/dist/values/temporal-arithmetic.d.ts.map +1 -0
- package/dist/values/temporal-arithmetic.js +183 -0
- package/dist/values/temporal-arithmetic.js.map +1 -0
- package/dist/values/temporal-compare.d.ts +9 -0
- package/dist/values/temporal-compare.d.ts.map +1 -0
- package/dist/values/temporal-compare.js +59 -0
- package/dist/values/temporal-compare.js.map +1 -0
- package/dist/values/type-compat.d.ts +47 -0
- package/dist/values/type-compat.d.ts.map +1 -0
- package/dist/values/type-compat.js +100 -0
- package/dist/values/type-compat.js.map +1 -0
- package/dist/values/typed-value.d.ts +48 -0
- package/dist/values/typed-value.d.ts.map +1 -0
- package/dist/values/typed-value.js +117 -0
- package/dist/values/typed-value.js.map +1 -0
- package/dist/values/ucum.d.ts +14 -0
- package/dist/values/ucum.d.ts.map +1 -0
- package/dist/values/ucum.js +146 -0
- package/dist/values/ucum.js.map +1 -0
- package/package.json +137 -0
- package/src/analyzer/analyze-dto.ts +195 -0
- package/src/analyzer/analyze.ts +1181 -0
- package/src/analyzer/expression-policy.ts +434 -0
- package/src/analyzer/index.ts +23 -0
- package/src/analyzer/regex-safety.ts +85 -0
- package/src/analyzer/signatures.ts +260 -0
- package/src/api/bundle.ts +132 -0
- package/src/api/cache.ts +40 -0
- package/src/api/column-signature.ts +71 -0
- package/src/api/compile.ts +308 -0
- package/src/api/constraints.ts +104 -0
- package/src/api/dto.ts +445 -0
- package/src/api/engine.ts +328 -0
- package/src/api/evaluate.ts +11 -0
- package/src/api/project.ts +255 -0
- package/src/api/tagged.ts +35 -0
- package/src/cli/dto-check.ts +94 -0
- package/src/cli/fhirpath-check.ts +168 -0
- package/src/cli/ts-loader.mjs +49 -0
- package/src/engine/context.ts +282 -0
- package/src/engine/evaluator.ts +246 -0
- package/src/engine/navigation.ts +106 -0
- package/src/engine/operators/collections.ts +34 -0
- package/src/engine/operators/comparison.ts +82 -0
- package/src/engine/operators/equality.ts +232 -0
- package/src/engine/operators/index.ts +65 -0
- package/src/engine/operators/logic.ts +53 -0
- package/src/engine/operators/math.ts +225 -0
- package/src/engine/operators/strings.ts +25 -0
- package/src/engine/operators/types.ts +16 -0
- package/src/engine/type-matching.ts +148 -0
- package/src/errors.ts +33 -0
- package/src/eslint/index.ts +388 -0
- package/src/fhir/html-checks.ts +266 -0
- package/src/fhir/model-navigation.ts +148 -0
- package/src/functions/aggregate.ts +136 -0
- package/src/functions/boundary.ts +209 -0
- package/src/functions/combining.ts +15 -0
- package/src/functions/control.ts +25 -0
- package/src/functions/conversion.ts +349 -0
- package/src/functions/date-components.ts +88 -0
- package/src/functions/existence.ts +107 -0
- package/src/functions/fhir.ts +247 -0
- package/src/functions/filtering.ts +120 -0
- package/src/functions/install.ts +19 -0
- package/src/functions/iteration.ts +21 -0
- package/src/functions/math.ts +167 -0
- package/src/functions/reflection.ts +24 -0
- package/src/functions/registry.ts +72 -0
- package/src/functions/sort.ts +68 -0
- package/src/functions/string.ts +413 -0
- package/src/functions/subsetting.ts +89 -0
- package/src/functions/tree.ts +112 -0
- package/src/functions/type-specifier.ts +29 -0
- package/src/functions/utility.ts +82 -0
- package/src/functions/variables.ts +36 -0
- package/src/index.ts +30 -0
- package/src/lexer/lexer.ts +341 -0
- package/src/lexer/tokens.ts +69 -0
- package/src/model/provider.ts +36 -0
- package/src/parser/ast.ts +164 -0
- package/src/parser/parser.ts +321 -0
- package/src/parser/precedence.ts +52 -0
- package/src/parser/printer.ts +114 -0
- package/src/r4/generated/resources-data.ts +28631 -0
- package/src/r4/generated/type-maps.ts +22145 -0
- package/src/r4/generated/types-data.ts +2208 -0
- package/src/r4/index.ts +107 -0
- package/src/r4/model-data.ts +17 -0
- package/src/sites/index.ts +285 -0
- package/src/typed/infer.ts +433 -0
- package/src/typed/perf-fixture.types.ts +143 -0
- package/src/values/collection.ts +57 -0
- package/src/values/datetime.ts +285 -0
- package/src/values/decimal.ts +213 -0
- package/src/values/numeric.ts +63 -0
- package/src/values/quantity.ts +358 -0
- package/src/values/temporal-arithmetic.ts +231 -0
- package/src/values/temporal-compare.ts +65 -0
- package/src/values/type-compat.ts +133 -0
- package/src/values/typed-value.ts +151 -0
- package/src/values/ucum.ts +181 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imports DTO modules, records the engines they create, and checks each exported
|
|
3
|
+
* DTO with loaded runtime context. The default module pattern is `*.dto.ts`.
|
|
4
|
+
* Importing runs module initialization; `--no-import` skips this pass.
|
|
5
|
+
*/
|
|
6
|
+
/* v8 ignore file -- covered end-to-end as a subprocess in fhirpath-check.test.ts, which is the only way to exercise a module loader and engine discovery in a fresh process */
|
|
7
|
+
import { glob } from 'node:fs/promises'
|
|
8
|
+
import { register } from 'node:module'
|
|
9
|
+
import { relative } from 'node:path'
|
|
10
|
+
import { pathToFileURL } from 'node:url'
|
|
11
|
+
|
|
12
|
+
import { type AnalyzedContext, analyzeDto, type DtoDiagnostic } from '../analyzer/analyze-dto.ts'
|
|
13
|
+
import { type DtoClass, isDtoClass } from '../api/dto.ts'
|
|
14
|
+
import { type FhirPathEngine, recordEngines } from '../api/engine.ts'
|
|
15
|
+
|
|
16
|
+
/** Where DTO classes live unless `--dtos` says otherwise. */
|
|
17
|
+
export const DEFAULT_DTO_GLOB = '**/*.dto.ts'
|
|
18
|
+
|
|
19
|
+
const IGNORED = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/coverage/**']
|
|
20
|
+
|
|
21
|
+
/** One finding, with the DTO and file it belongs to and the expression that produced it. */
|
|
22
|
+
export interface DtoFinding extends DtoDiagnostic {
|
|
23
|
+
/** The DTO class name. */
|
|
24
|
+
dto: string
|
|
25
|
+
/** The module the DTO was imported from, relative to the working directory. */
|
|
26
|
+
file: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DtoCheckResult {
|
|
30
|
+
findings: DtoFinding[]
|
|
31
|
+
/** Modules that matched and were imported, relative to the working directory. */
|
|
32
|
+
files: string[]
|
|
33
|
+
/** How many engines the imported modules constructed. */
|
|
34
|
+
engines: number
|
|
35
|
+
/** Every DTO analyzed, by module. */
|
|
36
|
+
dtos: { file: string; dto: string }[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Imports matching modules and checks their exported DTOs against recorded engines. */
|
|
40
|
+
export async function checkDtoModules(patterns: readonly string[], cwd: string): Promise<DtoCheckResult> {
|
|
41
|
+
const recorded = recordEngines()
|
|
42
|
+
register(new URL('ts-loader.mjs', import.meta.url))
|
|
43
|
+
const files: string[] = []
|
|
44
|
+
const dtos: { file: string; dto: string; cls: DtoClass }[] = []
|
|
45
|
+
for await (const match of glob(patterns.length > 0 ? [...patterns] : [DEFAULT_DTO_GLOB], { cwd, exclude: IGNORED })) {
|
|
46
|
+
const file = relative(cwd, new URL(match, pathToFileURL(`${cwd}/`)).pathname)
|
|
47
|
+
files.push(file)
|
|
48
|
+
const module: Record<string, unknown> = await import(pathToFileURL(`${cwd}/${match}`).href)
|
|
49
|
+
for (const [name, value] of Object.entries(module)) {
|
|
50
|
+
if (isDtoClass(value)) {
|
|
51
|
+
dtos.push({ file, dto: value.name || name, cls: value })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const engines = recorded()
|
|
56
|
+
const findings = dtos.flatMap(({ file, dto, cls }) =>
|
|
57
|
+
analyzeFor(cls, engines).map(finding => ({ ...finding, dto, file }))
|
|
58
|
+
)
|
|
59
|
+
return { findings, files, engines: engines.length, dtos: dtos.map(({ file, dto }) => ({ file, dto })) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A DTO against the engine that registered it, or — when no engine claims it, as
|
|
64
|
+
* for a row shape that is only ever projected — against everything the project's
|
|
65
|
+
* engines make available. A DTO checked against one engine of several would
|
|
66
|
+
* report calls that resolve perfectly well in the engine it actually runs on, and
|
|
67
|
+
* "some engine in this project declares this" is the most that can be said
|
|
68
|
+
* without being told which. Merging says exactly that, once, in engine order.
|
|
69
|
+
*/
|
|
70
|
+
function analyzeFor(dto: DtoClass, engines: readonly FhirPathEngine[]): DtoDiagnostic[] {
|
|
71
|
+
const owner = engines.find(engine => engine.dtos.includes(dto))
|
|
72
|
+
if (owner !== undefined) {
|
|
73
|
+
return analyzeDto(dto, { engine: owner })
|
|
74
|
+
}
|
|
75
|
+
if (engines.length === 0) {
|
|
76
|
+
return analyzeDto(dto)
|
|
77
|
+
}
|
|
78
|
+
return analyzeDto(dto, { engine: merged(engines) })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Every engine's context as one: the union of their registered functions and env
|
|
83
|
+
* names. The first engine's `model` stands for all of them — a project binds one
|
|
84
|
+
* FHIR version, and an unregistered DTO names no engine that could pick another.
|
|
85
|
+
*/
|
|
86
|
+
function merged(engines: readonly FhirPathEngine[]): AnalyzedContext {
|
|
87
|
+
return {
|
|
88
|
+
defaults: {
|
|
89
|
+
...engines[0]?.defaults,
|
|
90
|
+
functions: Object.assign({}, ...engines.map(engine => engine.defaults.functions)),
|
|
91
|
+
env: Object.assign({}, ...engines.map(engine => engine.defaults.env)),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Checks source literals with the TypeScript site finder, then imports DTO
|
|
4
|
+
* modules for checks that need their engine context. Errors produce a non-zero
|
|
5
|
+
* exit status. Use `--no-import` for source only and `--dtos` to change the DTO
|
|
6
|
+
* module glob.
|
|
7
|
+
*
|
|
8
|
+
* Usage: fhirpath-check [--dtos <glob>]... [--no-import] <file...>
|
|
9
|
+
*/
|
|
10
|
+
/* v8 ignore file -- covered end-to-end as a subprocess in fhirpath-check.test.ts */
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
|
|
13
|
+
import ts from 'typescript'
|
|
14
|
+
|
|
15
|
+
import { analyzeSite } from '../analyzer/analyze.ts'
|
|
16
|
+
import { r4Model } from '../r4/index.ts'
|
|
17
|
+
import { createSiteFinder, type ExpressionSite } from '../sites/index.ts'
|
|
18
|
+
import { checkDtoModules, DEFAULT_DTO_GLOB, type DtoFinding } from './dto-check.ts'
|
|
19
|
+
|
|
20
|
+
const findExpressionSites = createSiteFinder(ts)
|
|
21
|
+
|
|
22
|
+
interface Args {
|
|
23
|
+
files: string[]
|
|
24
|
+
dtoGlobs: string[]
|
|
25
|
+
imports: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv: readonly string[]): Args {
|
|
29
|
+
const args: Args = { files: [], dtoGlobs: [], imports: true }
|
|
30
|
+
for (let index = 0; index < argv.length; index++) {
|
|
31
|
+
const arg = argv[index]!
|
|
32
|
+
if (arg === '--no-import') {
|
|
33
|
+
args.imports = false
|
|
34
|
+
} else if (arg === '--dtos') {
|
|
35
|
+
const glob = argv[++index]
|
|
36
|
+
if (glob === undefined) {
|
|
37
|
+
console.error('fhirpath-check: --dtos needs a glob')
|
|
38
|
+
process.exit(2)
|
|
39
|
+
}
|
|
40
|
+
args.dtoGlobs.push(glob)
|
|
41
|
+
} else {
|
|
42
|
+
args.files.push(arg)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return args
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const args = parseArgs(process.argv.slice(2))
|
|
49
|
+
if (args.files.length === 0 && !args.imports) {
|
|
50
|
+
console.error('usage: fhirpath-check [--dtos <glob>]... [--no-import] <file...>')
|
|
51
|
+
process.exit(2)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let failures = 0
|
|
55
|
+
|
|
56
|
+
function report(location: string, diagnostic: { severity: string; code: string; message: string }): void {
|
|
57
|
+
if (diagnostic.severity === 'error') {
|
|
58
|
+
failures += 1
|
|
59
|
+
}
|
|
60
|
+
const prefix = diagnostic.severity === 'warning' ? 'warning:' : ''
|
|
61
|
+
console.error(`${location} [${prefix}${diagnostic.code}] ${diagnostic.message}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Where a diagnostic falls in the file, given the site its expression came from.
|
|
66
|
+
* A span's own line/column are relative to the expression text, so only a
|
|
67
|
+
* first-line column is an offset from the site's; a later line starts at its own
|
|
68
|
+
* column 1.
|
|
69
|
+
*/
|
|
70
|
+
function positionIn(site: { line: number; column: number }, span: { line: number; column: number }): string {
|
|
71
|
+
const line = site.line + (span.line - 1)
|
|
72
|
+
const column = span.line === 1 ? site.column + span.column - 1 : span.column
|
|
73
|
+
return `${line}:${column}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Expression sites cached so each source file is read once. */
|
|
77
|
+
const sitesByFile = new Map<string, ExpressionSite[]>()
|
|
78
|
+
|
|
79
|
+
function sitesOf(file: string): ExpressionSite[] {
|
|
80
|
+
const cached = sitesByFile.get(file)
|
|
81
|
+
if (cached !== undefined) {
|
|
82
|
+
return cached
|
|
83
|
+
}
|
|
84
|
+
const sites = findExpressionSites(readFileSync(file, 'utf8'), file)
|
|
85
|
+
sitesByFile.set(file, sites)
|
|
86
|
+
return sites
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check source literals first.
|
|
90
|
+
for (const file of args.files) {
|
|
91
|
+
let sites: ExpressionSite[]
|
|
92
|
+
try {
|
|
93
|
+
sites = sitesOf(file)
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`fhirpath-check: cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`)
|
|
96
|
+
process.exit(2)
|
|
97
|
+
}
|
|
98
|
+
for (const site of sites) {
|
|
99
|
+
for (const diagnostic of analyzeSite(site, { model: r4Model })) {
|
|
100
|
+
report(`${file}:${positionIn(site, diagnostic.span)}`, diagnostic)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A DTO finding's position in source: `analyzeDto` knows the member and the
|
|
107
|
+
* expression, and the site finder knows where each expression literal sits, so
|
|
108
|
+
* the expression text joins the two. Identical expressions in one file share the
|
|
109
|
+
* first match — a cosmetic tie, not a wrong finding. A file that cannot be read,
|
|
110
|
+
* or an expression with no literal to point at (one built at runtime), degrades
|
|
111
|
+
* to naming the member.
|
|
112
|
+
*/
|
|
113
|
+
function locate(finding: DtoFinding): string {
|
|
114
|
+
const member = `${finding.dto}.${finding.member}`
|
|
115
|
+
let site: ExpressionSite | undefined
|
|
116
|
+
try {
|
|
117
|
+
site = sitesOf(finding.file).find(candidate => candidate.expression === finding.expression)
|
|
118
|
+
} catch {
|
|
119
|
+
return `${finding.file} ${member}`
|
|
120
|
+
}
|
|
121
|
+
return site === undefined
|
|
122
|
+
? `${finding.file} ${member}`
|
|
123
|
+
: `${finding.file}:${positionIn(site, finding.span)} ${member}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- 2. the project's DTO modules, imported ---
|
|
127
|
+
if (args.imports) {
|
|
128
|
+
const globs = args.dtoGlobs.length > 0 ? args.dtoGlobs : [DEFAULT_DTO_GLOB]
|
|
129
|
+
let result
|
|
130
|
+
try {
|
|
131
|
+
result = await checkDtoModules(globs, process.cwd())
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(
|
|
134
|
+
`fhirpath-check: cannot import DTO modules (${globs.join(', ')}): ${error instanceof Error ? error.message : String(error)}`
|
|
135
|
+
)
|
|
136
|
+
process.exit(2)
|
|
137
|
+
}
|
|
138
|
+
for (const finding of result.findings) {
|
|
139
|
+
// With no engine in reach, an unresolved column call is unresolvable rather
|
|
140
|
+
// than wrong, so it is reported without failing the run.
|
|
141
|
+
const unresolvable = result.engines === 0 && finding.code === 'unknown-function'
|
|
142
|
+
report(locate(finding), unresolvable ? { ...finding, severity: 'warning' } : finding)
|
|
143
|
+
}
|
|
144
|
+
if (result.files.length === 0) {
|
|
145
|
+
console.error(
|
|
146
|
+
`fhirpath-check: no DTO modules matched ${globs.join(', ')} — DTOs live in *.dto.ts, or pass --dtos <glob>`
|
|
147
|
+
)
|
|
148
|
+
} else if (result.dtos.length === 0) {
|
|
149
|
+
console.error(`fhirpath-check: ${result.files.length} DTO module(s) matched but export no DTO class`)
|
|
150
|
+
} else if (result.engines === 0) {
|
|
151
|
+
// Without an engine, a call into another DTO's column cannot resolve, so the
|
|
152
|
+
// DTO half would report valid code. Say so instead of failing the run.
|
|
153
|
+
console.error(
|
|
154
|
+
'fhirpath-check: no engine was constructed by the imported modules — column function calls cannot be resolved; ' +
|
|
155
|
+
'point --dtos at the module that builds your FhirPathEngine'
|
|
156
|
+
)
|
|
157
|
+
} else {
|
|
158
|
+
console.log(
|
|
159
|
+
`fhirpath-check: analyzed ${result.dtos.length} DTO(s) from ${result.files.length} module(s) against ${result.engines} engine(s)`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (failures > 0) {
|
|
165
|
+
console.error(`fhirpath-check: ${failures} problem(s) found`)
|
|
166
|
+
process.exit(1)
|
|
167
|
+
}
|
|
168
|
+
console.log('fhirpath-check: no problems found')
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Node loader for DTO checks. It compiles standard decorators to ES2022
|
|
3
|
+
* before importing TypeScript modules. It does not read tsconfig paths or run
|
|
4
|
+
* type checks; projects that need custom module resolution should compile first.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
import ts from 'typescript'
|
|
10
|
+
|
|
11
|
+
const COMPILER_OPTIONS = {
|
|
12
|
+
target: ts.ScriptTarget.ES2022,
|
|
13
|
+
module: ts.ModuleKind.ESNext,
|
|
14
|
+
useDefineForClassFields: true,
|
|
15
|
+
experimentalDecorators: false,
|
|
16
|
+
inlineSourceMap: true,
|
|
17
|
+
inlineSources: true,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
21
|
+
try {
|
|
22
|
+
return await nextResolve(specifier, context)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
// A relative import written without an extension: try the TypeScript file
|
|
25
|
+
// the project meant, then its directory index.
|
|
26
|
+
if (specifier.startsWith('.') && !/\.[cm]?[jt]sx?$/.test(specifier)) {
|
|
27
|
+
for (const candidate of [`${specifier}.ts`, `${specifier}/index.ts`]) {
|
|
28
|
+
try {
|
|
29
|
+
return await nextResolve(candidate, context)
|
|
30
|
+
} catch {
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function load(url, context, nextLoad) {
|
|
40
|
+
if (!url.startsWith('file:') || !/\.[cm]?ts$/.test(url)) {
|
|
41
|
+
return nextLoad(url, context)
|
|
42
|
+
}
|
|
43
|
+
const fileName = fileURLToPath(url)
|
|
44
|
+
const { outputText } = ts.transpileModule(await readFile(fileName, 'utf8'), {
|
|
45
|
+
fileName,
|
|
46
|
+
compilerOptions: COMPILER_OPTIONS,
|
|
47
|
+
})
|
|
48
|
+
return { format: 'module', shortCircuit: true, source: outputText }
|
|
49
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { FhirPathTypeError } from '../errors.ts'
|
|
2
|
+
import { functions as builtinFunctions } from '../functions/registry.ts'
|
|
3
|
+
import type { ModelProvider } from '../model/provider.ts'
|
|
4
|
+
import type { AstNode } from '../parser/ast.ts'
|
|
5
|
+
import { SYSTEM_STRING, toCollection, type TypedValue } from '../values/typed-value.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The runtime side of a native host-supplied function: arity and implementation.
|
|
9
|
+
* The API layer's CustomFunction (api/compile.ts) adds the optional analyzer
|
|
10
|
+
* signature; this module only needs what evaluation uses.
|
|
11
|
+
*/
|
|
12
|
+
export interface HostNativeFunction {
|
|
13
|
+
/** Inclusive argument count range, checked before invocation. Defaults: 0 to unlimited. */
|
|
14
|
+
minArity?: number
|
|
15
|
+
maxArity?: number
|
|
16
|
+
/**
|
|
17
|
+
* Model type names the call's focus must be able to hold, spelled as the
|
|
18
|
+
* caller wrote them. Either a local name or a canonical one works, because
|
|
19
|
+
* `unsatisfiedInput` (values/type-compat.ts) canonicalizes the names it
|
|
20
|
+
* compares. Leave this out to accept any focus, which is what every function
|
|
21
|
+
* that does not declare `signature.input.types` does.
|
|
22
|
+
*/
|
|
23
|
+
inputTypes?: readonly string[]
|
|
24
|
+
/**
|
|
25
|
+
* The implementation. `input` is the unwrapped input collection; each
|
|
26
|
+
* argument is eagerly evaluated against `$this` — the enclosing context
|
|
27
|
+
* item, like built-in value arguments — and arrives as an unwrapped
|
|
28
|
+
* collection. Return a plain value, an array of plain values, or undefined
|
|
29
|
+
* for empty — the engine converts back to its typed representation.
|
|
30
|
+
*/
|
|
31
|
+
fn: (input: unknown[], ...args: unknown[][]) => unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The runtime side of an expression-defined function (CustomFunction's
|
|
36
|
+
* `expression` form, pre-parsed by the API layer). Zero arguments; the body
|
|
37
|
+
* evaluates as if spliced at the call site, with the call's input as focus.
|
|
38
|
+
*/
|
|
39
|
+
export interface HostExpressionFunction {
|
|
40
|
+
ast: AstNode
|
|
41
|
+
/** See HostNativeFunction.inputTypes. Same meaning, checked the same way. */
|
|
42
|
+
inputTypes?: readonly string[]
|
|
43
|
+
/**
|
|
44
|
+
* Environment variables the body reads that the caller does not supply, laid
|
|
45
|
+
* over the caller's env for the length of the call and gone again after it
|
|
46
|
+
* (see `withEnvOverlay`). This is how a DTO's `env` reaches its own column
|
|
47
|
+
* bodies without being published to every expression the engine evaluates.
|
|
48
|
+
* Already in collection form, since the same overlay serves every call.
|
|
49
|
+
*/
|
|
50
|
+
env?: ReadonlyMap<string, TypedValue[]>
|
|
51
|
+
/**
|
|
52
|
+
* Apply the criteria rule to the body's result, so the function always returns
|
|
53
|
+
* exactly one Boolean. That rule is `criteriaBoolean`: §4.5 singleton
|
|
54
|
+
* evaluation, with an empty result read as false. It is what makes a
|
|
55
|
+
* `@criteria` mean the same thing whether it is projected as a column or
|
|
56
|
+
* called from an expression.
|
|
57
|
+
*/
|
|
58
|
+
criteria?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One host-supplied function, in either form. */
|
|
62
|
+
export type HostSingleFunction = HostNativeFunction | HostExpressionFunction
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Several functions registered under one name, told apart by the focus each was
|
|
66
|
+
* written for. A call runs the first whose `inputTypes` the focus satisfies (see
|
|
67
|
+
* `resolveHostCall`), so two DTOs may both declare a `displayText` column as
|
|
68
|
+
* long as a CodeableConcept can never be a Coding. Every member must declare
|
|
69
|
+
* `inputTypes` for that to mean anything, which is what `withDtos` (api/dto.ts)
|
|
70
|
+
* checks before it builds one.
|
|
71
|
+
*/
|
|
72
|
+
export interface HostOverloadedFunction {
|
|
73
|
+
overloads: readonly HostSingleFunction[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type HostFunction = HostSingleFunction | HostOverloadedFunction
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A pluggable regular-expression engine for matches()/matchesFull()/
|
|
80
|
+
* replaceMatches(). The default is the built-in RegExp, which backtracks and
|
|
81
|
+
* cannot be timed out synchronously — hosts evaluating untrusted expressions
|
|
82
|
+
* can supply a linear-time engine (e.g. an RE2 binding) here instead.
|
|
83
|
+
*/
|
|
84
|
+
export interface RegexEngine {
|
|
85
|
+
/**
|
|
86
|
+
* Compile `pattern` with `flags` (a subset of 's' and 'g'; matchesFull
|
|
87
|
+
* wraps the pattern in `^(?:...)$` before compiling). Throw on invalid
|
|
88
|
+
* patterns — the engine converts that to the spec's type error.
|
|
89
|
+
*/
|
|
90
|
+
compile(
|
|
91
|
+
pattern: string,
|
|
92
|
+
flags: string
|
|
93
|
+
): {
|
|
94
|
+
test(subject: string): boolean
|
|
95
|
+
/** Replace every match (the 'g' flag is passed for replaceMatches). */
|
|
96
|
+
replace(subject: string, substitution: string): string
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** `$this` / `$index` / `$total` bindings; iteration functions push one frame per element. */
|
|
101
|
+
export interface Frame {
|
|
102
|
+
parent: Frame | undefined
|
|
103
|
+
thisValue: TypedValue[]
|
|
104
|
+
index: number | undefined
|
|
105
|
+
total: TypedValue[] | undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface EvaluationContext {
|
|
109
|
+
/** The original input node: `%context`. */
|
|
110
|
+
root: TypedValue[]
|
|
111
|
+
/** Environment variables by name (without the `%`). Values are collections. */
|
|
112
|
+
env: ReadonlyMap<string, TypedValue[]>
|
|
113
|
+
model: ModelProvider | undefined
|
|
114
|
+
/** Fixed evaluation clock so now()/today()/timeOfDay() are stable within one evaluation. */
|
|
115
|
+
now: Date
|
|
116
|
+
/**
|
|
117
|
+
* Sink for trace(). Default is a no-op: traced values may contain patient data,
|
|
118
|
+
* so writing them to console or log files is the caller's deliberate choice.
|
|
119
|
+
*/
|
|
120
|
+
trace: (name: string, values: TypedValue[]) => void
|
|
121
|
+
/**
|
|
122
|
+
* Variables from defineVariable(), seeded with the call's `vars` bindings.
|
|
123
|
+
* The map is local to one expression chain: dots thread it along, while
|
|
124
|
+
* operator operands, function arguments, and iteration frames evaluate
|
|
125
|
+
* against a copy (see forkVariables).
|
|
126
|
+
*/
|
|
127
|
+
variables: Map<string, TypedValue[]>
|
|
128
|
+
/** Host-supplied functions by name; never contains a built-in name (createContext rejects overrides). */
|
|
129
|
+
functions: ReadonlyMap<string, HostFunction>
|
|
130
|
+
/**
|
|
131
|
+
* Names of expression-defined functions currently on the call stack. One
|
|
132
|
+
* shared Set per evaluation (context copies keep the reference), so a
|
|
133
|
+
* definition that reaches itself — directly or through another definition —
|
|
134
|
+
* fails as recursion instead of overflowing the stack.
|
|
135
|
+
*/
|
|
136
|
+
activeExpressionFunctions: Set<string>
|
|
137
|
+
/** Regex engine for the matches() family; undefined means the built-in RegExp. */
|
|
138
|
+
regex: RegexEngine | undefined
|
|
139
|
+
frame: Frame
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const BUILTIN_CONSTANTS: ReadonlyMap<string, string> = new Map([
|
|
143
|
+
['ucum', 'http://unitsofmeasure.org'],
|
|
144
|
+
['sct', 'http://snomed.info/sct'],
|
|
145
|
+
['loinc', 'http://loinc.org'],
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Environment variables every evaluation defines. The static analyzer resolves
|
|
150
|
+
* and protects exactly these names, so keep this list next to the code that
|
|
151
|
+
* seeds them (createContext) — a builtin added here is known there for free.
|
|
152
|
+
*/
|
|
153
|
+
export const BUILTIN_ENV_VARIABLE_NAMES: ReadonlySet<string> = new Set([
|
|
154
|
+
...BUILTIN_CONSTANTS.keys(),
|
|
155
|
+
'context',
|
|
156
|
+
'resource',
|
|
157
|
+
'rootResource',
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Env records accept variable names with or without the leading `%`
|
|
162
|
+
* (`{ '%loinc': … }` or `{ loinc: … }`). Normalize to bare names — the form the
|
|
163
|
+
* context binds — so code that merges or overrides env records treats both
|
|
164
|
+
* spellings as one namespace. Later entries win on the same bare name.
|
|
165
|
+
*/
|
|
166
|
+
export function normalizeEnvKeys<T>(env: Record<string, T> | undefined): Record<string, T> {
|
|
167
|
+
const normalized: Record<string, T> = {}
|
|
168
|
+
for (const [name, value] of Object.entries(env ?? {})) {
|
|
169
|
+
normalized[name.startsWith('%') ? name.slice(1) : name] = value
|
|
170
|
+
}
|
|
171
|
+
return normalized
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* An env record as the collections a context binds: names normalized, values
|
|
176
|
+
* wrapped. The one conversion from the host's env shape into the engine's, used
|
|
177
|
+
* both to seed a context and to build a function's own overlay.
|
|
178
|
+
*/
|
|
179
|
+
export function envCollections(env: Record<string, unknown> | undefined): Map<string, TypedValue[]> {
|
|
180
|
+
const collections = new Map<string, TypedValue[]>()
|
|
181
|
+
for (const [name, value] of Object.entries(normalizeEnvKeys(env))) {
|
|
182
|
+
collections.set(name, toCollection(value))
|
|
183
|
+
}
|
|
184
|
+
return collections
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Merge two env-shaped records per name: both key spellings normalize first, and `override` wins. */
|
|
188
|
+
export function mergeEnvKeys<T>(
|
|
189
|
+
base: Record<string, T> | undefined,
|
|
190
|
+
override: Record<string, T> | undefined
|
|
191
|
+
): Record<string, T> {
|
|
192
|
+
return { ...normalizeEnvKeys(base), ...normalizeEnvKeys(override) }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createContext(options: {
|
|
196
|
+
root: TypedValue[]
|
|
197
|
+
env?: Record<string, unknown> | undefined
|
|
198
|
+
model?: ModelProvider | undefined
|
|
199
|
+
now?: Date | undefined
|
|
200
|
+
trace?: ((name: string, values: TypedValue[]) => void) | undefined
|
|
201
|
+
functions?: Record<string, HostFunction> | undefined
|
|
202
|
+
regex?: RegexEngine | undefined
|
|
203
|
+
}): EvaluationContext {
|
|
204
|
+
const env = new Map<string, TypedValue[]>()
|
|
205
|
+
for (const [name, url] of BUILTIN_CONSTANTS) {
|
|
206
|
+
env.set(name, [{ type: SYSTEM_STRING, value: url }])
|
|
207
|
+
}
|
|
208
|
+
env.set('context', options.root)
|
|
209
|
+
// FHIR-defined variables; contained-resource re-rooting is a later refinement.
|
|
210
|
+
env.set('resource', options.root)
|
|
211
|
+
env.set('rootResource', options.root)
|
|
212
|
+
for (const [name, value] of envCollections(options.env)) {
|
|
213
|
+
env.set(name, value)
|
|
214
|
+
}
|
|
215
|
+
const hostFunctions = new Map<string, HostFunction>()
|
|
216
|
+
for (const [name, fn] of Object.entries(options.functions ?? {})) {
|
|
217
|
+
// Overriding a built-in would silently change spec behavior — fail loudly.
|
|
218
|
+
if (builtinFunctions.has(name)) {
|
|
219
|
+
throw new FhirPathTypeError(`Cannot override the built-in function '${name}'`)
|
|
220
|
+
}
|
|
221
|
+
hostFunctions.set(name, fn)
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
root: options.root,
|
|
225
|
+
env,
|
|
226
|
+
model: options.model,
|
|
227
|
+
now: options.now ?? new Date(),
|
|
228
|
+
trace: options.trace ?? (() => {}),
|
|
229
|
+
variables: new Map(),
|
|
230
|
+
functions: hostFunctions,
|
|
231
|
+
activeExpressionFunctions: new Set(),
|
|
232
|
+
regex: options.regex,
|
|
233
|
+
frame: { parent: undefined, thisValue: options.root, index: undefined, total: undefined },
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Adds environment values for one expression function body. Existing variables
|
|
239
|
+
* still take priority because variable lookup happens before environment lookup.
|
|
240
|
+
*/
|
|
241
|
+
export function withEnvOverlay(
|
|
242
|
+
context: EvaluationContext,
|
|
243
|
+
overlay: ReadonlyMap<string, TypedValue[]>
|
|
244
|
+
): EvaluationContext {
|
|
245
|
+
return { ...context, env: new Map([...context.env, ...overlay]) }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** A context whose defineVariable() scope is detached from the parent chain. */
|
|
249
|
+
export function forkVariables(context: EvaluationContext): EvaluationContext {
|
|
250
|
+
return { ...context, variables: new Map(context.variables) }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Resolve `%name`; referencing an undefined environment variable is an error (spec §9). */
|
|
254
|
+
export function resolveEnvironmentVariable(context: EvaluationContext, name: string): TypedValue[] {
|
|
255
|
+
const value = context.variables.get(name) ?? context.env.get(name)
|
|
256
|
+
if (value !== undefined) {
|
|
257
|
+
return value
|
|
258
|
+
}
|
|
259
|
+
// FHIR-defined families: %`vs-[name]` and %`ext-[name]` expand to HL7 urls.
|
|
260
|
+
if (name.startsWith('vs-')) {
|
|
261
|
+
return [{ type: SYSTEM_STRING, value: `http://hl7.org/fhir/ValueSet/${name.slice(3)}` }]
|
|
262
|
+
}
|
|
263
|
+
if (name.startsWith('ext-')) {
|
|
264
|
+
return [{ type: SYSTEM_STRING, value: `http://hl7.org/fhir/StructureDefinition/${name.slice(4)}` }]
|
|
265
|
+
}
|
|
266
|
+
throw new FhirPathTypeError(`Undefined environment variable %${name}`)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Run `body` with a frame binding `$this` (and optionally `$index` / `$total`). */
|
|
270
|
+
export function withFrame<T>(
|
|
271
|
+
context: EvaluationContext,
|
|
272
|
+
frame: { thisValue: TypedValue[]; index?: number; total?: TypedValue[] },
|
|
273
|
+
body: (context: EvaluationContext) => T
|
|
274
|
+
): T {
|
|
275
|
+
const child: Frame = {
|
|
276
|
+
parent: context.frame,
|
|
277
|
+
thisValue: frame.thisValue,
|
|
278
|
+
index: frame.index,
|
|
279
|
+
total: frame.total,
|
|
280
|
+
}
|
|
281
|
+
return body({ ...context, frame: child, variables: new Map(context.variables) })
|
|
282
|
+
}
|