fhirpath-ts 0.1.0 → 0.2.1
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/README.md +50 -18
- package/dist/analyzer/analyze-dto.d.ts +4 -0
- package/dist/analyzer/analyze-dto.d.ts.map +1 -1
- package/dist/analyzer/analyze-dto.js +12 -14
- package/dist/analyzer/analyze-dto.js.map +1 -1
- package/dist/analyzer/analyze.d.ts +22 -6
- package/dist/analyzer/analyze.d.ts.map +1 -1
- package/dist/analyzer/analyze.js +242 -98
- package/dist/analyzer/analyze.js.map +1 -1
- package/dist/analyzer/declarations.d.ts +26 -0
- package/dist/analyzer/declarations.d.ts.map +1 -0
- package/dist/analyzer/declarations.js +68 -0
- package/dist/analyzer/declarations.js.map +1 -0
- package/dist/analyzer/operator-rules.d.ts +127 -0
- package/dist/analyzer/operator-rules.d.ts.map +1 -0
- package/dist/analyzer/operator-rules.js +70 -0
- package/dist/analyzer/operator-rules.js.map +1 -0
- package/dist/analyzer/signatures.d.ts +1097 -10
- package/dist/analyzer/signatures.d.ts.map +1 -1
- package/dist/analyzer/signatures.js +70 -29
- package/dist/analyzer/signatures.js.map +1 -1
- package/dist/api/compile.d.ts +32 -8
- package/dist/api/compile.d.ts.map +1 -1
- package/dist/api/compile.js +2 -0
- package/dist/api/compile.js.map +1 -1
- package/dist/api/engine.d.ts +42 -16
- package/dist/api/engine.d.ts.map +1 -1
- package/dist/api/engine.js +2 -1
- package/dist/api/engine.js.map +1 -1
- package/dist/api/project.d.ts +8 -8
- package/dist/api/project.d.ts.map +1 -1
- package/dist/api/project.js +31 -19
- package/dist/api/project.js.map +1 -1
- package/dist/api/strict.d.ts +6 -0
- package/dist/api/strict.d.ts.map +1 -0
- package/dist/api/strict.js +71 -0
- package/dist/api/strict.js.map +1 -0
- package/dist/api/tagged.d.ts +4 -4
- package/dist/api/tagged.d.ts.map +1 -1
- package/dist/api/tagged.js.map +1 -1
- package/dist/cli/dto-check.js +7 -3
- package/dist/cli/dto-check.js.map +1 -1
- package/dist/engine/context.d.ts +3 -1
- package/dist/engine/context.d.ts.map +1 -1
- package/dist/engine/context.js +5 -1
- package/dist/engine/context.js.map +1 -1
- package/dist/engine/navigation.d.ts.map +1 -1
- package/dist/engine/navigation.js +7 -48
- package/dist/engine/navigation.js.map +1 -1
- package/dist/engine/type-matching.d.ts.map +1 -1
- package/dist/engine/type-matching.js +6 -16
- package/dist/engine/type-matching.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +102 -73
- package/dist/parser/parser.js.map +1 -1
- package/dist/parser/precedence.d.ts +368 -4
- package/dist/parser/precedence.d.ts.map +1 -1
- package/dist/parser/precedence.js +146 -32
- package/dist/parser/precedence.js.map +1 -1
- package/dist/r4/generated/resources-data.d.ts +6 -2
- package/dist/r4/generated/resources-data.d.ts.map +1 -1
- package/dist/r4/generated/resources-data.js +626 -28627
- package/dist/r4/generated/resources-data.js.map +1 -1
- package/dist/r4/generated/type-maps.d.ts +1367 -0
- package/dist/r4/generated/type-maps.d.ts.map +1 -1
- package/dist/r4/generated/types-data.d.ts +6 -2
- package/dist/r4/generated/types-data.d.ts.map +1 -1
- package/dist/r4/generated/types-data.js +84 -2204
- package/dist/r4/generated/types-data.js.map +1 -1
- package/dist/r4/index.d.ts +3 -1
- package/dist/r4/index.d.ts.map +1 -1
- package/dist/r4/index.js +10 -4
- package/dist/r4/index.js.map +1 -1
- package/dist/r4/model-data.d.ts +17 -0
- package/dist/r4/model-data.d.ts.map +1 -1
- package/dist/r4/model-data.js +118 -1
- package/dist/r4/model-data.js.map +1 -1
- package/dist/typed/context-maps.d.ts +23 -0
- package/dist/typed/context-maps.d.ts.map +1 -0
- package/dist/typed/context-maps.js +2 -0
- package/dist/typed/context-maps.js.map +1 -0
- package/dist/typed/infer.d.ts +99 -229
- package/dist/typed/infer.d.ts.map +1 -1
- package/dist/typed/infer.js +1 -84
- package/dist/typed/infer.js.map +1 -1
- package/dist/typed/inference-limits.d.ts +7 -0
- package/dist/typed/inference-limits.d.ts.map +1 -0
- package/dist/typed/inference-limits.js +6 -0
- package/dist/typed/inference-limits.js.map +1 -0
- package/dist/typed/metadata-compact.d.ts +189 -0
- package/dist/typed/metadata-compact.d.ts.map +1 -0
- package/dist/typed/metadata-compact.js +3 -0
- package/dist/typed/metadata-compact.js.map +1 -0
- package/dist/typed/parser.d.ts +515 -0
- package/dist/typed/parser.d.ts.map +1 -0
- package/dist/typed/parser.js +2 -0
- package/dist/typed/parser.js.map +1 -0
- package/dist/values/type-compat.d.ts +8 -0
- package/dist/values/type-compat.d.ts.map +1 -1
- package/dist/values/type-compat.js +44 -1
- package/dist/values/type-compat.js.map +1 -1
- package/package.json +20 -1
- package/src/analyzer/analyze-dto.ts +21 -17
- package/src/analyzer/analyze.ts +335 -109
- package/src/analyzer/declarations.ts +120 -0
- package/src/analyzer/operator-rules.ts +88 -0
- package/src/analyzer/signatures.ts +99 -41
- package/src/api/compile.ts +66 -11
- package/src/api/engine.ts +97 -33
- package/src/api/project.ts +54 -30
- package/src/api/strict.ts +112 -0
- package/src/api/tagged.ts +5 -5
- package/src/cli/dto-check.ts +7 -3
- package/src/engine/context.ts +7 -2
- package/src/engine/navigation.ts +7 -52
- package/src/engine/type-matching.ts +11 -17
- package/src/index.ts +25 -2
- package/src/parser/parser.ts +106 -74
- package/src/parser/precedence.ts +250 -33
- package/src/r4/generated/resources-data.ts +624 -28628
- package/src/r4/generated/type-maps.ts +1557 -0
- package/src/r4/generated/types-data.ts +82 -2205
- package/src/r4/index.ts +11 -5
- package/src/r4/model-data.ts +127 -0
- package/src/typed/context-maps.ts +41 -0
- package/src/typed/infer.ts +157 -425
- package/src/typed/inference-limits.ts +9 -0
- package/src/typed/metadata-compact.ts +307 -0
- package/src/typed/parser.ts +2277 -0
- package/src/values/type-compat.ts +48 -1
- package/dist/typed/perf-fixture.types.d.ts +0 -136
- package/dist/typed/perf-fixture.types.d.ts.map +0 -1
- package/dist/typed/perf-fixture.types.js +0 -2
- package/dist/typed/perf-fixture.types.js.map +0 -1
- package/src/typed/perf-fixture.types.ts +0 -143
package/src/api/engine.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { mergeEnvKeys } from '../engine/context.ts'
|
|
2
2
|
import type { R4TypeOf } from '../r4/generated/type-maps.ts'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
EmptyFhirpathTypeContext,
|
|
5
|
+
FhirpathInput,
|
|
6
|
+
FhirpathResultForContext,
|
|
7
|
+
FhirpathRootOf,
|
|
8
|
+
FhirpathTypeContextOf,
|
|
9
|
+
MergeFhirpathTypeContexts,
|
|
10
|
+
} from '../typed/infer.ts'
|
|
4
11
|
import { criteriaBoolean } from '../values/collection.ts'
|
|
5
12
|
import type { TypedValue } from '../values/typed-value.ts'
|
|
6
13
|
import { type BundleLike, isBundle, normalizeInput, toSubjects } from './bundle.ts'
|
|
@@ -10,6 +17,7 @@ import {
|
|
|
10
17
|
type Compiler,
|
|
11
18
|
createCachedCompiler,
|
|
12
19
|
type CustomFunction,
|
|
20
|
+
type Declaring,
|
|
13
21
|
type EvaluateOptions,
|
|
14
22
|
type SingleCustomFunction,
|
|
15
23
|
} from './compile.ts'
|
|
@@ -30,6 +38,47 @@ export type EngineInput<Expr extends string = string> = FhirpathInput<Expr> | re
|
|
|
30
38
|
/** Per-call options that declare a result type when inference returns `unknown`. Runtime code ignores `type`. */
|
|
31
39
|
export type TypedEvaluateOptions<T extends keyof R4TypeOf> = EvaluateOptions & { type: T }
|
|
32
40
|
|
|
41
|
+
/** Literal text or a compatible compiled expression accepted by engine evaluation methods. */
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- engine methods infer their own input and result
|
|
43
|
+
export type EngineExpression<Expr extends string> = Expr | CompiledExpression<Expr, any, any, any>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* evaluate() reaches this shape through normalizeInput(), while project()
|
|
47
|
+
* reaches it through toSubjects(). A bare Bundle is expression-dependent or
|
|
48
|
+
* expands to heterogeneous entries, so it stays opaque; an array keeps each
|
|
49
|
+
* item raw, including a Bundle deliberately wrapped as `[bundle]`.
|
|
50
|
+
*/
|
|
51
|
+
export type EngineInputRoot<Input> = Input extends readonly (infer Item)[]
|
|
52
|
+
? FhirpathRootOf<Item>
|
|
53
|
+
: Input extends { readonly resourceType: 'Bundle' }
|
|
54
|
+
? 'opaque'
|
|
55
|
+
: FhirpathRootOf<Input>
|
|
56
|
+
|
|
57
|
+
/** The inferred result returned by an engine or bound expression call. */
|
|
58
|
+
export type EngineResult<Expr extends string, Input, Defaults, Options> = FhirpathResultForContext<
|
|
59
|
+
Expr,
|
|
60
|
+
EngineInputRoot<Input>,
|
|
61
|
+
MergeFhirpathTypeContexts<FhirpathTypeContextOf<Defaults>, FhirpathTypeContextOf<Options>>
|
|
62
|
+
>
|
|
63
|
+
|
|
64
|
+
/** The merged static declarations visible while project() evaluates a row. */
|
|
65
|
+
export type EngineProjectionContext<Defaults, Options> = MergeFhirpathTypeContexts<
|
|
66
|
+
MergeFhirpathTypeContexts<FhirpathTypeContextOf<Defaults>, FhirpathTypeContextOf<Options>>,
|
|
67
|
+
{
|
|
68
|
+
env: {
|
|
69
|
+
rowIndex: { type: 'System.Integer' }
|
|
70
|
+
rowTotal: { type: 'System.Integer' }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
|
|
75
|
+
/** The inferred row returned by project(), including its built-in row variables. */
|
|
76
|
+
export type EngineProjection<Columns extends ProjectionColumns, Input, Defaults, Options> = Projection<
|
|
77
|
+
Columns,
|
|
78
|
+
EngineInputRoot<Input>,
|
|
79
|
+
EngineProjectionContext<Defaults, Options>
|
|
80
|
+
>
|
|
81
|
+
|
|
33
82
|
/** Engines created during the current recording session. */
|
|
34
83
|
let session: FhirPathEngine[] | undefined
|
|
35
84
|
|
|
@@ -81,7 +130,7 @@ export interface EngineOptions extends EvaluateOptions {
|
|
|
81
130
|
* replace defaults, while `env`, `vars`, and `functions` merge by name. Keep an
|
|
82
131
|
* engine for reuse because its parse cache is private to that instance.
|
|
83
132
|
*/
|
|
84
|
-
export class FhirPathEngine {
|
|
133
|
+
export class FhirPathEngine<const Defaults extends EngineOptions = EmptyFhirpathTypeContext> {
|
|
85
134
|
/** The per-call options bound at construction; engine-only settings are not part of them. */
|
|
86
135
|
readonly defaults: EvaluateOptions
|
|
87
136
|
/**
|
|
@@ -92,24 +141,29 @@ export class FhirPathEngine {
|
|
|
92
141
|
readonly dtos: readonly DtoClass[]
|
|
93
142
|
private readonly compileCached: Compiler
|
|
94
143
|
|
|
95
|
-
constructor(
|
|
144
|
+
constructor(options: Declaring<Defaults, EngineOptions> = {} as Declaring<Defaults, EngineOptions>) {
|
|
145
|
+
const { cacheSize, resourceDtos, ...defaults } = options
|
|
96
146
|
this.compileCached = createCachedCompiler(cacheSize)
|
|
97
147
|
this.dtos = resourceDtos ?? []
|
|
98
|
-
this.defaults = this.precompiled(withDtos(defaults, this.dtos, this.compileCached))
|
|
148
|
+
this.defaults = this.precompiled(withDtos(defaults as EvaluateOptions, this.dtos, this.compileCached))
|
|
99
149
|
recordEngine(this)
|
|
100
150
|
}
|
|
101
151
|
|
|
102
152
|
/** Compile (LRU-cached by expression text) and evaluate in one call; typed like `compile().evaluate()`. */
|
|
103
153
|
evaluate<const Expr extends string, T extends keyof R4TypeOf>(
|
|
104
|
-
expression:
|
|
154
|
+
expression: EngineExpression<Expr>,
|
|
105
155
|
input: EngineInput<Expr> | undefined,
|
|
106
156
|
options: TypedEvaluateOptions<T>
|
|
107
157
|
): R4TypeOf[T][]
|
|
108
|
-
evaluate<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
158
|
+
evaluate<
|
|
159
|
+
const Expr extends string,
|
|
160
|
+
const Input extends EngineInput<Expr> | undefined = undefined,
|
|
161
|
+
const Options extends object = EmptyFhirpathTypeContext,
|
|
162
|
+
>(
|
|
163
|
+
expression: EngineExpression<Expr>,
|
|
164
|
+
input?: Input,
|
|
165
|
+
options?: Declaring<Options>
|
|
166
|
+
): EngineResult<Expr, Input, Defaults, Options>
|
|
113
167
|
evaluate(expression: AnyExpression, input?: unknown, options?: EvaluateOptions): unknown[] {
|
|
114
168
|
const compiled = this.compileCached(expression)
|
|
115
169
|
const merged = this.merged(options)
|
|
@@ -124,21 +178,25 @@ export class FhirPathEngine {
|
|
|
124
178
|
}
|
|
125
179
|
|
|
126
180
|
/** Parse once for reuse, with this engine's defaults bound. Does not touch the parse cache. */
|
|
127
|
-
compile<const Expr extends string>(expression: Expr): BoundExpression<Expr> {
|
|
181
|
+
compile<const Expr extends string>(expression: Expr): BoundExpression<Expr, Defaults> {
|
|
128
182
|
return new BoundExpression(this, new CompiledExpression(expression))
|
|
129
183
|
}
|
|
130
184
|
|
|
131
185
|
/** The first result, or undefined when the expression comes up empty. */
|
|
132
186
|
first<const Expr extends string, T extends keyof R4TypeOf>(
|
|
133
|
-
expression:
|
|
187
|
+
expression: EngineExpression<Expr>,
|
|
134
188
|
input: EngineInput<Expr> | undefined,
|
|
135
189
|
options: TypedEvaluateOptions<T>
|
|
136
190
|
): R4TypeOf[T] | undefined
|
|
137
|
-
first<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
191
|
+
first<
|
|
192
|
+
const Expr extends string,
|
|
193
|
+
const Input extends EngineInput<Expr> | undefined = undefined,
|
|
194
|
+
const Options extends object = EmptyFhirpathTypeContext,
|
|
195
|
+
>(
|
|
196
|
+
expression: EngineExpression<Expr>,
|
|
197
|
+
input?: Input,
|
|
198
|
+
options?: Declaring<Options>
|
|
199
|
+
): EngineResult<Expr, Input, Defaults, Options>[number] | undefined
|
|
142
200
|
first(expression: AnyExpression, input?: unknown, options?: EvaluateOptions): unknown {
|
|
143
201
|
return this.evaluate(expression, input, options)[0]
|
|
144
202
|
}
|
|
@@ -180,16 +238,16 @@ export class FhirPathEngine {
|
|
|
180
238
|
options?: EvaluateOptions
|
|
181
239
|
): InstanceType<C>[]
|
|
182
240
|
project<C extends DtoClass>(input: unknown, dto: C, options?: EvaluateOptions): InstanceType<C>
|
|
183
|
-
project<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
):
|
|
188
|
-
project<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
):
|
|
241
|
+
project<
|
|
242
|
+
const Input extends readonly unknown[] | BundleLike,
|
|
243
|
+
const Columns extends ProjectionColumns,
|
|
244
|
+
const Options extends object = EmptyFhirpathTypeContext,
|
|
245
|
+
>(input: Input, columns: Columns, options?: Declaring<Options>): EngineProjection<Columns, Input, Defaults, Options>[]
|
|
246
|
+
project<
|
|
247
|
+
const Input,
|
|
248
|
+
const Columns extends ProjectionColumns,
|
|
249
|
+
const Options extends object = EmptyFhirpathTypeContext,
|
|
250
|
+
>(input: Input, columns: Columns, options?: Declaring<Options>): EngineProjection<Columns, Input, Defaults, Options>
|
|
193
251
|
project(input: unknown, columns: ProjectionColumns | DtoClass, options?: EvaluateOptions): unknown {
|
|
194
252
|
if (typeof columns === 'function') {
|
|
195
253
|
assertInputMatchesDto(input, columns)
|
|
@@ -282,16 +340,16 @@ export class FhirPathEngine {
|
|
|
282
340
|
}
|
|
283
341
|
|
|
284
342
|
/** A compiled expression carrying an engine's defaults, so `evaluate(input)` needs nothing else. */
|
|
285
|
-
export class BoundExpression<Expr extends string = string> {
|
|
343
|
+
export class BoundExpression<Expr extends string = string, Defaults extends EngineOptions = EmptyFhirpathTypeContext> {
|
|
286
344
|
readonly expression: CompiledExpression<Expr>
|
|
287
|
-
private readonly engine: FhirPathEngine
|
|
345
|
+
private readonly engine: FhirPathEngine<Defaults>
|
|
288
346
|
|
|
289
|
-
constructor(engine: FhirPathEngine
|
|
347
|
+
constructor(engine: FhirPathEngine<Defaults>, expression: CompiledExpression<Expr>) {
|
|
290
348
|
this.engine = engine
|
|
291
349
|
this.expression = expression
|
|
292
350
|
}
|
|
293
351
|
|
|
294
|
-
get source():
|
|
352
|
+
get source(): Expr {
|
|
295
353
|
return this.expression.source
|
|
296
354
|
}
|
|
297
355
|
|
|
@@ -299,7 +357,10 @@ export class BoundExpression<Expr extends string = string> {
|
|
|
299
357
|
input: EngineInput<Expr> | undefined,
|
|
300
358
|
options: TypedEvaluateOptions<T>
|
|
301
359
|
): R4TypeOf[T][]
|
|
302
|
-
evaluate
|
|
360
|
+
evaluate<
|
|
361
|
+
const Input extends EngineInput<Expr> | undefined = undefined,
|
|
362
|
+
const Options extends object = EmptyFhirpathTypeContext,
|
|
363
|
+
>(input?: Input, options?: Declaring<Options>): EngineResult<Expr, Input, Defaults, Options>
|
|
303
364
|
evaluate(input?: EngineInput<Expr>, options?: EvaluateOptions): unknown {
|
|
304
365
|
return this.engine.evaluate(this.expression, input, options)
|
|
305
366
|
}
|
|
@@ -312,7 +373,10 @@ export class BoundExpression<Expr extends string = string> {
|
|
|
312
373
|
input: EngineInput<Expr> | undefined,
|
|
313
374
|
options: TypedEvaluateOptions<T>
|
|
314
375
|
): R4TypeOf[T] | undefined
|
|
315
|
-
first
|
|
376
|
+
first<
|
|
377
|
+
const Input extends EngineInput<Expr> | undefined = undefined,
|
|
378
|
+
const Options extends object = EmptyFhirpathTypeContext,
|
|
379
|
+
>(input?: Input, options?: Declaring<Options>): EngineResult<Expr, Input, Defaults, Options>[number] | undefined
|
|
316
380
|
first(input?: EngineInput<Expr>, options?: EvaluateOptions): unknown {
|
|
317
381
|
return this.engine.first(this.expression, input, options)
|
|
318
382
|
}
|
package/src/api/project.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { type EvaluationContext, forkVariables } from '../engine/context.ts'
|
|
|
2
2
|
import { evaluateNode } from '../engine/evaluator.ts'
|
|
3
3
|
import { FhirPathRuntimeError } from '../errors.ts'
|
|
4
4
|
import type { R4TypeOf } from '../r4/generated/type-maps.ts'
|
|
5
|
-
import type {
|
|
5
|
+
import type { EmptyFhirpathTypeContext, FhirpathResultForContext } from '../typed/infer.ts'
|
|
6
6
|
import { criteriaBoolean } from '../values/collection.ts'
|
|
7
7
|
import { toCollection, type TypedValue, unwrap } from '../values/typed-value.ts'
|
|
8
8
|
import { toSubjects } from './bundle.ts'
|
|
9
9
|
import { type Compiler, contextFactory, type EvaluateOptions } from './compile.ts'
|
|
10
|
+
import { assertStrictExpression } from './strict.ts'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* One `project()` column. A string or `{ path }` returns one optional value.
|
|
@@ -38,7 +39,9 @@ type ColumnPath<Column> = Column extends string
|
|
|
38
39
|
: never
|
|
39
40
|
|
|
40
41
|
/** Infers the values after the selected conversion, or from the path when no conversion is set. */
|
|
41
|
-
type ColumnValues<Column, Root extends string> = Column extends {
|
|
42
|
+
type ColumnValues<Column, Root extends string, Context extends object> = Column extends {
|
|
43
|
+
as: (value: never) => infer R
|
|
44
|
+
}
|
|
42
45
|
? R[]
|
|
43
46
|
: Column extends { as: 'Date' }
|
|
44
47
|
? Date[]
|
|
@@ -52,25 +55,30 @@ type ColumnValues<Column, Root extends string> = Column extends { as: (value: ne
|
|
|
52
55
|
? V[]
|
|
53
56
|
: Column extends { type: infer T extends keyof R4TypeOf }
|
|
54
57
|
? R4TypeOf[T][]
|
|
55
|
-
:
|
|
58
|
+
: FhirpathResultForContext<ColumnPath<Column>, Root, Context>
|
|
56
59
|
|
|
57
60
|
/** A column's output type. A default replaces `undefined`; a collection returns every value. */
|
|
58
61
|
export type ColumnResult<
|
|
59
62
|
Column extends string | { path: string } | { test: string },
|
|
60
63
|
Root extends string = 'opaque',
|
|
64
|
+
Context extends object = EmptyFhirpathTypeContext,
|
|
61
65
|
> = Column extends {
|
|
62
66
|
test: string
|
|
63
67
|
}
|
|
64
68
|
? boolean
|
|
65
69
|
: Column extends { collection: true }
|
|
66
|
-
? ColumnValues<Column, Root>
|
|
70
|
+
? ColumnValues<Column, Root, Context>
|
|
67
71
|
: Column extends { default: infer D }
|
|
68
|
-
? ColumnValues<Column, Root>[number] | D
|
|
69
|
-
: ColumnValues<Column, Root>[number] | undefined
|
|
72
|
+
? ColumnValues<Column, Root, Context>[number] | D
|
|
73
|
+
: ColumnValues<Column, Root, Context>[number] | undefined
|
|
70
74
|
|
|
71
75
|
/** The row shape `project()` produces: each column's type inferred from its expression. */
|
|
72
|
-
export type Projection<
|
|
73
|
-
|
|
76
|
+
export type Projection<
|
|
77
|
+
Columns extends ProjectionColumns,
|
|
78
|
+
Root extends string = 'opaque',
|
|
79
|
+
Context extends object = EmptyFhirpathTypeContext,
|
|
80
|
+
> = {
|
|
81
|
+
-readonly [K in keyof Columns]: ColumnResult<Columns[K], Root, Context>
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/** The `as: 'Date'` coercion; a value that is not a parseable date string becomes empty. */
|
|
@@ -183,6 +191,11 @@ function assertShaperOptions(name: string, spec: Extract<ProjectionColumn, { pat
|
|
|
183
191
|
*/
|
|
184
192
|
type ColumnReader = (root: TypedValue[], context: EvaluationContext, at: RowPosition) => unknown
|
|
185
193
|
|
|
194
|
+
interface PlannedColumn {
|
|
195
|
+
ast: ReturnType<Compiler>['ast']
|
|
196
|
+
read: ColumnReader
|
|
197
|
+
}
|
|
198
|
+
|
|
186
199
|
/** Where a row sits in the batch, for the errors that need to point at one of many. */
|
|
187
200
|
interface RowPosition {
|
|
188
201
|
index: number
|
|
@@ -199,32 +212,38 @@ function inRow({ index, total }: RowPosition): string {
|
|
|
199
212
|
}
|
|
200
213
|
|
|
201
214
|
/** Take the column union apart once, at plan time; rows only run the result. */
|
|
202
|
-
function planColumn(name: string, column: ProjectionColumn, compile: Compiler):
|
|
215
|
+
function planColumn(name: string, column: ProjectionColumn, compile: Compiler): PlannedColumn {
|
|
203
216
|
if (typeof column !== 'string' && 'test' in column) {
|
|
204
217
|
const criteria = compile(column.test)
|
|
205
|
-
return
|
|
218
|
+
return {
|
|
219
|
+
ast: criteria.ast,
|
|
220
|
+
read: (root, context) => criteriaBoolean(evaluateNode(criteria.ast, forkVariables(context), root)),
|
|
221
|
+
}
|
|
206
222
|
}
|
|
207
223
|
const spec: Extract<ProjectionColumn, { path: string }> = typeof column === 'string' ? { path: column } : column
|
|
208
224
|
assertShaperOptions(name, spec)
|
|
209
225
|
const expression = compile(spec.path)
|
|
210
226
|
const applyAs = coercion(spec)
|
|
211
227
|
const empty = 'default' in spec ? spec.default : undefined
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
return {
|
|
229
|
+
ast: expression.ast,
|
|
230
|
+
read: (root, context, at) => {
|
|
231
|
+
const values = evaluateNode(expression.ast, forkVariables(context), root).map(unwrap)
|
|
232
|
+
if (spec.collection === true) {
|
|
233
|
+
return applyAs(values)
|
|
234
|
+
}
|
|
235
|
+
// The scalar-column rule counts the expression's values, before any `as`
|
|
236
|
+
// coercion or `choices` miss drops them. One row of a batch can be the only
|
|
237
|
+
// one that breaks it, so the message says which — `%rowIndex` numbering,
|
|
238
|
+
// the same the columns see.
|
|
239
|
+
if (values.length > 1) {
|
|
240
|
+
throw new FhirPathRuntimeError(
|
|
241
|
+
`project(): column '${name}' yielded ${values.length} values${inRow(at)}; append first() or set collection: true`
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
const coerced = applyAs(values)
|
|
245
|
+
return coerced.length > 0 ? coerced[0] : empty
|
|
246
|
+
},
|
|
228
247
|
}
|
|
229
248
|
}
|
|
230
249
|
|
|
@@ -239,16 +258,21 @@ export function projectRows(
|
|
|
239
258
|
options: EvaluateOptions,
|
|
240
259
|
compile: Compiler
|
|
241
260
|
): Record<string, unknown>[] {
|
|
242
|
-
const
|
|
261
|
+
const planned = Object.entries(columns).map(([name, column]) => [name, planColumn(name, column, compile)] as const)
|
|
243
262
|
const makeContext = contextFactory(options)
|
|
244
263
|
const subjects = toSubjects(input)
|
|
245
264
|
return subjects.map((subject, index) => {
|
|
246
265
|
const root = toCollection(subject.value)
|
|
247
266
|
const at: RowPosition = { index, total: subjects.length }
|
|
248
|
-
const
|
|
267
|
+
const rowEnv = { rowIndex: index, rowTotal: subjects.length }
|
|
268
|
+
const strictOptions = options.strict === true ? { ...options, env: { ...options.env, ...rowEnv } } : options
|
|
269
|
+
for (const [, column] of planned) {
|
|
270
|
+
assertStrictExpression(column.ast, root, strictOptions)
|
|
271
|
+
}
|
|
272
|
+
const context = makeContext(root, rowEnv)
|
|
249
273
|
const row: Record<string, unknown> = {}
|
|
250
|
-
for (const [name,
|
|
251
|
-
row[name] = read(root, context, at)
|
|
274
|
+
for (const [name, column] of planned) {
|
|
275
|
+
row[name] = column.read(root, context, at)
|
|
252
276
|
}
|
|
253
277
|
return row
|
|
254
278
|
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnalyzerDiagnostic,
|
|
3
|
+
type AnalyzerRoot,
|
|
4
|
+
analyzeRuntimeAstDetailed,
|
|
5
|
+
type RuntimeAnalyzeOptions,
|
|
6
|
+
} from '../analyzer/analyze.ts'
|
|
7
|
+
import {
|
|
8
|
+
analyzerVariable,
|
|
9
|
+
type AnalyzerVariableState,
|
|
10
|
+
runtimeAnalyzerEnvironmentVariables,
|
|
11
|
+
runtimeAnalyzerVariable,
|
|
12
|
+
} from '../analyzer/declarations.ts'
|
|
13
|
+
import { normalizeEnvKeys } from '../engine/context.ts'
|
|
14
|
+
import { FhirPathTypeError } from '../errors.ts'
|
|
15
|
+
import type { ModelProvider } from '../model/provider.ts'
|
|
16
|
+
import type { AstNode } from '../parser/ast.ts'
|
|
17
|
+
import { parse } from '../parser/parser.ts'
|
|
18
|
+
import type { TypedValue } from '../values/typed-value.ts'
|
|
19
|
+
import type { AnyExpression, EvaluateOptions } from './compile.ts'
|
|
20
|
+
|
|
21
|
+
interface StrictFinding {
|
|
22
|
+
diagnostic: AnalyzerDiagnostic
|
|
23
|
+
subject?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Reject every error-severity analyzer finding before strict evaluation begins. */
|
|
27
|
+
export function assertStrictExpression(ast: AstNode, root: TypedValue[], options: EvaluateOptions | undefined): void {
|
|
28
|
+
if (options?.strict !== true) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const model = options.model
|
|
33
|
+
const analyzerRoot = runtimeRoot(root, model)
|
|
34
|
+
const variables: Record<string, AnalyzerVariableState> = runtimeAnalyzerEnvironmentVariables(
|
|
35
|
+
options.env,
|
|
36
|
+
options.envTypes,
|
|
37
|
+
model
|
|
38
|
+
)
|
|
39
|
+
const declarations = normalizeEnvKeys(options.varTypes)
|
|
40
|
+
const findings: StrictFinding[] = []
|
|
41
|
+
|
|
42
|
+
for (const [name, value] of Object.entries(normalizeEnvKeys(options.vars))) {
|
|
43
|
+
const declaration = declarations[name]
|
|
44
|
+
if (isResolvedCollection(value)) {
|
|
45
|
+
variables[name] = runtimeAnalyzerVariable(value, model, declaration)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const details = analyzeRuntimeAstDetailed(astOf(value), analyzerOptions(options, variables), analyzerRoot)
|
|
50
|
+
findings.push(
|
|
51
|
+
...details.diagnostics
|
|
52
|
+
.filter(diagnostic => diagnostic.severity === 'error')
|
|
53
|
+
.map(diagnostic => ({ diagnostic, subject: `vars.${name}` }))
|
|
54
|
+
)
|
|
55
|
+
variables[name] =
|
|
56
|
+
declaration === undefined
|
|
57
|
+
? {
|
|
58
|
+
...(details.result.types !== undefined && { types: details.result.types }),
|
|
59
|
+
...(details.result.single !== undefined && { single: details.result.single }),
|
|
60
|
+
}
|
|
61
|
+
: analyzerVariable(declaration)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const [name, declaration] of Object.entries(declarations)) {
|
|
65
|
+
variables[name] ??= analyzerVariable(declaration)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const details = analyzeRuntimeAstDetailed(ast, analyzerOptions(options, variables), analyzerRoot)
|
|
69
|
+
findings.push(
|
|
70
|
+
...details.diagnostics.filter(diagnostic => diagnostic.severity === 'error').map(diagnostic => ({ diagnostic }))
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if (findings.length > 0) {
|
|
74
|
+
throw new FhirPathTypeError(formatFindings(findings))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function analyzerOptions(
|
|
79
|
+
options: EvaluateOptions,
|
|
80
|
+
variables: Record<string, AnalyzerVariableState>
|
|
81
|
+
): RuntimeAnalyzeOptions {
|
|
82
|
+
return {
|
|
83
|
+
...(options.model !== undefined && { model: options.model }),
|
|
84
|
+
...(options.functions !== undefined && { functions: options.functions }),
|
|
85
|
+
variables,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runtimeRoot(root: TypedValue[], model: ModelProvider | undefined): AnalyzerRoot {
|
|
90
|
+
const variable = runtimeAnalyzerVariable(root, model)
|
|
91
|
+
return {
|
|
92
|
+
types: variable.types,
|
|
93
|
+
single: variable.single,
|
|
94
|
+
exactTypes: variable.exactTypes,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isResolvedCollection(value: AnyExpression | readonly TypedValue[]): value is readonly TypedValue[] {
|
|
99
|
+
return Array.isArray(value)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function astOf(expression: AnyExpression): AstNode {
|
|
103
|
+
return typeof expression === 'string' ? parse(expression) : expression.ast
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatFindings(findings: StrictFinding[]): string {
|
|
107
|
+
const lines = findings.map(({ diagnostic, subject }) => {
|
|
108
|
+
const prefix = subject === undefined ? '' : `${subject}: `
|
|
109
|
+
return `- [${diagnostic.code}] ${prefix}${diagnostic.message} (line ${diagnostic.span.line}, column ${diagnostic.span.column})`
|
|
110
|
+
})
|
|
111
|
+
return `Strict evaluation failed:\n${lines.join('\n')}`
|
|
112
|
+
}
|
package/src/api/tagged.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FhirPathError } from '../errors.ts'
|
|
2
2
|
import type { R4TypeOf } from '../r4/generated/type-maps.ts'
|
|
3
|
-
import type { FhirpathInput,
|
|
4
|
-
import { CompiledExpression } from './compile.ts'
|
|
3
|
+
import type { FhirpathInput, FhirTypeName } from '../typed/infer.ts'
|
|
4
|
+
import { CompiledExpression, type InferredExpressionResult } from './compile.ts'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Compiles a FHIRPath expression. The call form infers supported literal
|
|
@@ -13,12 +13,12 @@ import { CompiledExpression } from './compile.ts'
|
|
|
13
13
|
export function fhirpath<
|
|
14
14
|
const Expr extends string,
|
|
15
15
|
const Root extends FhirTypeName,
|
|
16
|
-
TResult extends unknown[] =
|
|
17
|
-
>(expression: Expr, inputType: Root): CompiledExpression<Expr, R4TypeOf[Root], TResult>
|
|
16
|
+
TResult extends unknown[] | InferredExpressionResult = InferredExpressionResult,
|
|
17
|
+
>(expression: Expr, inputType: Root): CompiledExpression<Expr, R4TypeOf[Root], TResult, Root>
|
|
18
18
|
export function fhirpath<
|
|
19
19
|
const Expr extends string,
|
|
20
20
|
TInput = FhirpathInput<Expr>,
|
|
21
|
-
TResult extends unknown[] =
|
|
21
|
+
TResult extends unknown[] | InferredExpressionResult = InferredExpressionResult,
|
|
22
22
|
>(expression: Expr): CompiledExpression<Expr, TInput, TResult>
|
|
23
23
|
export function fhirpath(strings: TemplateStringsArray, ...substitutions: never[]): CompiledExpression
|
|
24
24
|
export function fhirpath(input: string | TemplateStringsArray, ...rest: unknown[]): CompiledExpression {
|
package/src/cli/dto-check.ts
CHANGED
|
@@ -79,9 +79,10 @@ function analyzeFor(dto: DtoClass, engines: readonly FhirPathEngine[]): DtoDiagn
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* Every engine's context as one: the union of their registered functions
|
|
83
|
-
*
|
|
84
|
-
* FHIR version, and an unregistered DTO names
|
|
82
|
+
* Every engine's context as one: the union of their registered functions,
|
|
83
|
+
* environment declarations, and vars. The first engine's `model` stands for
|
|
84
|
+
* all of them — a project binds one FHIR version, and an unregistered DTO names
|
|
85
|
+
* no engine that could pick another.
|
|
85
86
|
*/
|
|
86
87
|
function merged(engines: readonly FhirPathEngine[]): AnalyzedContext {
|
|
87
88
|
return {
|
|
@@ -89,6 +90,9 @@ function merged(engines: readonly FhirPathEngine[]): AnalyzedContext {
|
|
|
89
90
|
...engines[0]?.defaults,
|
|
90
91
|
functions: Object.assign({}, ...engines.map(engine => engine.defaults.functions)),
|
|
91
92
|
env: Object.assign({}, ...engines.map(engine => engine.defaults.env)),
|
|
93
|
+
envTypes: Object.assign({}, ...engines.map(engine => engine.defaults.envTypes)),
|
|
94
|
+
vars: Object.assign({}, ...engines.map(engine => engine.defaults.vars)),
|
|
95
|
+
varTypes: Object.assign({}, ...engines.map(engine => engine.defaults.varTypes)),
|
|
92
96
|
},
|
|
93
97
|
}
|
|
94
98
|
}
|
package/src/engine/context.ts
CHANGED
|
@@ -157,16 +157,21 @@ export const BUILTIN_ENV_VARIABLE_NAMES: ReadonlySet<string> = new Set([
|
|
|
157
157
|
'rootResource',
|
|
158
158
|
])
|
|
159
159
|
|
|
160
|
+
/** The canonical key used for environment and variable maps. */
|
|
161
|
+
export function bareEnvironmentName(name: string): string {
|
|
162
|
+
return name.startsWith('%') ? name.slice(1) : name
|
|
163
|
+
}
|
|
164
|
+
|
|
160
165
|
/**
|
|
161
166
|
* Env records accept variable names with or without the leading `%`
|
|
162
167
|
* (`{ '%loinc': … }` or `{ loinc: … }`). Normalize to bare names — the form the
|
|
163
168
|
* context binds — so code that merges or overrides env records treats both
|
|
164
169
|
* spellings as one namespace. Later entries win on the same bare name.
|
|
165
170
|
*/
|
|
166
|
-
export function normalizeEnvKeys<T>(env: Record<string, T
|
|
171
|
+
export function normalizeEnvKeys<T>(env: Readonly<Record<string, T>> | undefined): Record<string, T> {
|
|
167
172
|
const normalized: Record<string, T> = {}
|
|
168
173
|
for (const [name, value] of Object.entries(env ?? {})) {
|
|
169
|
-
normalized[
|
|
174
|
+
normalized[bareEnvironmentName(name)] = value
|
|
170
175
|
}
|
|
171
176
|
return normalized
|
|
172
177
|
}
|
package/src/engine/navigation.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FhirPathTypeError } from '../errors.ts'
|
|
2
1
|
import { readModelProperty } from '../fhir/model-navigation.ts'
|
|
3
|
-
import {
|
|
2
|
+
import { rootTypeMatches } from '../values/type-compat.ts'
|
|
3
|
+
import { toTypedValue, type TypedValue } from '../values/typed-value.ts'
|
|
4
4
|
import type { EvaluationContext } from './context.ts'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -11,22 +11,15 @@ import type { EvaluationContext } from './context.ts'
|
|
|
11
11
|
export function navigateIdentifier(context: EvaluationContext, name: string, input: TypedValue[]): TypedValue[] {
|
|
12
12
|
const results: TypedValue[] = []
|
|
13
13
|
for (const item of input) {
|
|
14
|
-
if (
|
|
14
|
+
if (rootTypeMatches(context.model, item.type, name)) {
|
|
15
15
|
results.push(item)
|
|
16
16
|
} else if (context.model && item.type.startsWith(`${context.model.namespace}.`)) {
|
|
17
17
|
const modelRead = readModelProperty(context.model, item, name)
|
|
18
18
|
if (modelRead === undefined) {
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// (custom resourceTypes) read like raw JSON.
|
|
24
|
-
// See: https://hl7.org/fhirpath/en/index.html#type-safety-and-strict-evaluation
|
|
25
|
-
if (isChoiceKeyMisuse(context, item.type, name)) {
|
|
26
|
-
throw new FhirPathTypeError(
|
|
27
|
-
`Element '${name}' is not defined on ${item.type}; choice elements use their stem name`
|
|
28
|
-
)
|
|
29
|
-
}
|
|
19
|
+
// Unknown model elements navigate to empty. Strict evaluation runs the
|
|
20
|
+
// analyzer before reaching this point, including for choice-key misuse.
|
|
21
|
+
// Types the model has never heard of (custom resourceTypes) still read
|
|
22
|
+
// like raw JSON.
|
|
30
23
|
if (
|
|
31
24
|
name === 'resourceType' ||
|
|
32
25
|
(context.model.listElements !== undefined && context.model.listElements(item.type) === undefined)
|
|
@@ -46,44 +39,6 @@ export function navigateIdentifier(context: EvaluationContext, name: string, inp
|
|
|
46
39
|
return results
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
/** True for `valueQuantity`-style keys whose stem is a choice element of the type. */
|
|
50
|
-
function isChoiceKeyMisuse(context: EvaluationContext, type: string, name: string): boolean {
|
|
51
|
-
const model = context.model as NonNullable<EvaluationContext['model']>
|
|
52
|
-
for (let position = 1; position < name.length; position++) {
|
|
53
|
-
if (!/[A-Z]/.test(name[position] as string)) {
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
const stem = name.slice(0, position)
|
|
57
|
-
if (model.getElement(type, stem)?.isChoice === true) {
|
|
58
|
-
return true
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function matchesTypeName(context: EvaluationContext, item: TypedValue, name: string): boolean {
|
|
65
|
-
// Only the namespace strips off: the backbone type FHIR.ValueSet.expansion.contains
|
|
66
|
-
// must not answer to the element name 'contains'. Lowercase primitive type names
|
|
67
|
-
// (FHIR.code) never self-match either — `children().code` means the element.
|
|
68
|
-
if (!/^[A-Z]/.test(name)) {
|
|
69
|
-
// Lowercase names are always elements: `children().code` never filters by the
|
|
70
|
-
// primitive type FHIR.code.
|
|
71
|
-
return false
|
|
72
|
-
}
|
|
73
|
-
const separator = item.type.indexOf('.')
|
|
74
|
-
const local = separator === -1 ? item.type : item.type.slice(separator + 1)
|
|
75
|
-
if (local === name && item.type !== OBJECT_TYPE) {
|
|
76
|
-
return true
|
|
77
|
-
}
|
|
78
|
-
if (context.model) {
|
|
79
|
-
const canonical = context.model.resolveType(name)
|
|
80
|
-
if (canonical !== undefined) {
|
|
81
|
-
return context.model.isSubtypeOf(item.type, canonical)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return false
|
|
85
|
-
}
|
|
86
|
-
|
|
87
42
|
/** Read one child element from a complex value, flattening arrays. Missing → empty. */
|
|
88
43
|
export function getProperty(item: TypedValue, name: string): TypedValue[] {
|
|
89
44
|
const value = item.value
|