@atomic-ehr/fhirpath 0.0.1 → 0.0.2-canary.2fee5d9.20250808110507
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 +2 -0
- package/dist/index.d.ts +195 -77
- package/dist/index.js +2541 -981
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/analyzer.ts +536 -64
- package/src/boxing.ts +124 -0
- package/src/errors.ts +246 -0
- package/src/index.ts +57 -9
- package/src/inspect.ts +307 -174
- package/src/interpreter.ts +299 -48
- package/src/model-provider.ts +110 -8
- package/src/operations/abs-function.ts +19 -10
- package/src/operations/aggregate-function.ts +11 -3
- package/src/operations/all-function.ts +18 -8
- package/src/operations/allFalse-function.ts +9 -6
- package/src/operations/allTrue-function.ts +9 -6
- package/src/operations/and-operator.ts +20 -11
- package/src/operations/anyFalse-function.ts +6 -4
- package/src/operations/anyTrue-function.ts +6 -4
- package/src/operations/as-operator.ts +1 -0
- package/src/operations/ceiling-function.ts +9 -5
- package/src/operations/children-function.ts +94 -0
- package/src/operations/combine-function.ts +4 -2
- package/src/operations/combine-operator.ts +18 -4
- package/src/operations/contains-function.ts +20 -8
- package/src/operations/contains-operator.ts +14 -6
- package/src/operations/count-function.ts +2 -1
- package/src/operations/defineVariable-function.ts +5 -3
- package/src/operations/descendants-function.ts +62 -0
- package/src/operations/distinct-function.ts +16 -4
- package/src/operations/div-operator.ts +15 -2
- package/src/operations/divide-operator.ts +10 -5
- package/src/operations/dot-operator.ts +3 -1
- package/src/operations/empty-function.ts +2 -1
- package/src/operations/endsWith-function.ts +22 -10
- package/src/operations/equal-operator.ts +18 -6
- package/src/operations/equivalent-operator.ts +15 -10
- package/src/operations/exclude-function.ts +12 -10
- package/src/operations/exists-function.ts +14 -7
- package/src/operations/first-function.ts +7 -4
- package/src/operations/floor-function.ts +9 -5
- package/src/operations/greater-operator.ts +9 -4
- package/src/operations/greater-or-equal-operator.ts +9 -4
- package/src/operations/iif-function.ts +18 -16
- package/src/operations/implies-operator.ts +22 -7
- package/src/operations/in-operator.ts +17 -7
- package/src/operations/index.ts +3 -0
- package/src/operations/indexOf-function.ts +22 -10
- package/src/operations/intersect-function.ts +17 -20
- package/src/operations/is-operator.ts +63 -14
- package/src/operations/isDistinct-function.ts +12 -6
- package/src/operations/join-function.ts +15 -4
- package/src/operations/last-function.ts +7 -4
- package/src/operations/length-function.ts +12 -5
- package/src/operations/less-operator.ts +9 -4
- package/src/operations/less-or-equal-operator.ts +9 -4
- package/src/operations/less-than.ts +25 -1
- package/src/operations/lower-function.ts +12 -5
- package/src/operations/minus-operator.ts +9 -4
- package/src/operations/mod-operator.ts +19 -2
- package/src/operations/multiply-operator.ts +11 -6
- package/src/operations/not-equal-operator.ts +15 -6
- package/src/operations/not-equivalent-operator.ts +15 -10
- package/src/operations/not-function.ts +12 -4
- package/src/operations/ofType-function.ts +135 -0
- package/src/operations/or-operator.ts +20 -11
- package/src/operations/plus-operator.ts +18 -6
- package/src/operations/power-function.ts +21 -9
- package/src/operations/replace-function.ts +26 -9
- package/src/operations/round-function.ts +23 -8
- package/src/operations/select-function.ts +12 -6
- package/src/operations/single-function.ts +5 -3
- package/src/operations/skip-function.ts +12 -5
- package/src/operations/split-function.ts +24 -9
- package/src/operations/sqrt-function.ts +9 -5
- package/src/operations/startsWith-function.ts +20 -8
- package/src/operations/subsetOf-function.ts +14 -11
- package/src/operations/substring-function.ts +36 -19
- package/src/operations/supersetOf-function.ts +14 -11
- package/src/operations/tail-function.ts +3 -1
- package/src/operations/take-function.ts +12 -5
- package/src/operations/toBoolean-function.ts +18 -11
- package/src/operations/toDecimal-function.ts +13 -6
- package/src/operations/toInteger-function.ts +13 -6
- package/src/operations/toString-function.ts +17 -10
- package/src/operations/trace-function.ts +12 -5
- package/src/operations/trim-function.ts +11 -4
- package/src/operations/truncate-function.ts +9 -5
- package/src/operations/unary-minus-operator.ts +22 -12
- package/src/operations/unary-plus-operator.ts +1 -0
- package/src/operations/union-function.ts +19 -24
- package/src/operations/union-operator.ts +1 -0
- package/src/operations/upper-function.ts +12 -5
- package/src/operations/where-function.ts +15 -8
- package/src/operations/xor-operator.ts +8 -3
- package/src/parser.ts +391 -8
- package/src/quantity-value.ts +4 -8
- package/src/registry.ts +3 -3
- package/src/types.ts +10 -6
- package/src/parser-base.ts +0 -400
package/src/interpreter.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { Registry } from './registry';
|
|
|
18
18
|
import * as operations from './operations';
|
|
19
19
|
import type { EvaluationResult, FunctionEvaluator, NodeEvaluator, OperationEvaluator, RuntimeContext } from './types';
|
|
20
20
|
import { createQuantity } from './quantity-value';
|
|
21
|
+
import { box, unbox, ensureBoxed, type FHIRPathValue } from './boxing';
|
|
22
|
+
import { Errors } from './errors';
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Runtime context manager that provides efficient prototype-based context operations
|
|
@@ -166,9 +168,11 @@ export class Interpreter {
|
|
|
166
168
|
private nodeEvaluators: Record<NodeType, NodeEvaluator>;
|
|
167
169
|
private operationEvaluators: Map<string, OperationEvaluator>;
|
|
168
170
|
private functionEvaluators: Map<string, FunctionEvaluator>;
|
|
171
|
+
private modelProvider?: import('./types').ModelProvider<any>;
|
|
169
172
|
|
|
170
|
-
constructor(registry?: Registry) {
|
|
173
|
+
constructor(registry?: Registry, modelProvider?: import('./types').ModelProvider<any>) {
|
|
171
174
|
this.registry = registry || new Registry();
|
|
175
|
+
this.modelProvider = modelProvider;
|
|
172
176
|
this.operationEvaluators = new Map();
|
|
173
177
|
this.functionEvaluators = new Map();
|
|
174
178
|
|
|
@@ -225,45 +229,171 @@ export class Interpreter {
|
|
|
225
229
|
input = input === null || input === undefined ? [] : [input];
|
|
226
230
|
}
|
|
227
231
|
|
|
232
|
+
// Box the initial input values
|
|
233
|
+
const boxedInput = input.map(value => ensureBoxed(value));
|
|
234
|
+
|
|
235
|
+
// Set current node in context
|
|
236
|
+
const contextWithNode = RuntimeContextManager.copy(context);
|
|
237
|
+
contextWithNode.currentNode = node;
|
|
238
|
+
|
|
228
239
|
// Dispatch to appropriate evaluator
|
|
229
240
|
const evaluator = this.nodeEvaluators[node.type];
|
|
230
241
|
if (!evaluator) {
|
|
231
|
-
throw
|
|
242
|
+
throw Errors.unknownNodeType(node.type);
|
|
232
243
|
}
|
|
233
244
|
|
|
234
|
-
return evaluator(node,
|
|
245
|
+
return evaluator(node, boxedInput, contextWithNode);
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
private createInitialContext(input: any[]): RuntimeContext {
|
|
238
249
|
const context = RuntimeContextManager.create(input);
|
|
239
250
|
// Set $this to initial input
|
|
240
251
|
context.variables['$this'] = input;
|
|
252
|
+
// Add model provider if available
|
|
253
|
+
if (this.modelProvider) {
|
|
254
|
+
context.modelProvider = this.modelProvider;
|
|
255
|
+
}
|
|
241
256
|
return context;
|
|
242
257
|
}
|
|
243
258
|
|
|
244
259
|
// Literal node evaluator
|
|
245
|
-
private evaluateLiteral(node: ASTNode, input:
|
|
260
|
+
private evaluateLiteral(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
246
261
|
const literal = node as LiteralNode;
|
|
262
|
+
|
|
263
|
+
// Box the literal value with appropriate type info
|
|
264
|
+
let typeInfo: import('./types').TypeInfo | undefined;
|
|
265
|
+
const value = literal.value;
|
|
266
|
+
|
|
267
|
+
if (typeof value === 'string') {
|
|
268
|
+
typeInfo = { type: 'String', singleton: true };
|
|
269
|
+
} else if (typeof value === 'number') {
|
|
270
|
+
typeInfo = Number.isInteger(value) ?
|
|
271
|
+
{ type: 'Integer', singleton: true } :
|
|
272
|
+
{ type: 'Decimal', singleton: true };
|
|
273
|
+
} else if (typeof value === 'boolean') {
|
|
274
|
+
typeInfo = { type: 'Boolean', singleton: true };
|
|
275
|
+
}
|
|
276
|
+
|
|
247
277
|
return {
|
|
248
|
-
value: [literal.value],
|
|
278
|
+
value: [box(literal.value, typeInfo)],
|
|
249
279
|
context
|
|
250
280
|
};
|
|
251
281
|
}
|
|
252
282
|
|
|
253
283
|
// Identifier node evaluator
|
|
254
|
-
private evaluateIdentifier(node: ASTNode, input:
|
|
284
|
+
private evaluateIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
255
285
|
const identifier = node as IdentifierNode;
|
|
256
286
|
const name = identifier.name;
|
|
257
287
|
|
|
258
|
-
// Navigate property on each item in input
|
|
259
|
-
const results:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
288
|
+
// Navigate property on each boxed item in input
|
|
289
|
+
const results: FHIRPathValue[] = [];
|
|
290
|
+
|
|
291
|
+
// Get the type info from the node (set by analyzer)
|
|
292
|
+
const nodeTypeInfo = node.typeInfo;
|
|
293
|
+
|
|
294
|
+
for (const boxedItem of input) {
|
|
295
|
+
const item = unbox(boxedItem);
|
|
296
|
+
|
|
297
|
+
// Special handling for primitive extension navigation
|
|
298
|
+
if (name === 'extension' && boxedItem.primitiveElement?.extension) {
|
|
299
|
+
// Navigation from a primitive value to its extensions
|
|
300
|
+
for (const ext of boxedItem.primitiveElement.extension) {
|
|
301
|
+
results.push(box(ext, nodeTypeInfo || { type: 'Any', singleton: false }));
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (item && typeof item === 'object') {
|
|
307
|
+
// Check if this is a choice type navigation
|
|
308
|
+
if (nodeTypeInfo?.modelContext && typeof nodeTypeInfo.modelContext === 'object' &&
|
|
309
|
+
'isUnion' in nodeTypeInfo.modelContext &&
|
|
310
|
+
nodeTypeInfo.modelContext.isUnion && 'choices' in nodeTypeInfo.modelContext &&
|
|
311
|
+
Array.isArray(nodeTypeInfo.modelContext.choices)) {
|
|
312
|
+
// For choice types, look for any of the choice properties
|
|
313
|
+
for (const choice of nodeTypeInfo.modelContext.choices) {
|
|
314
|
+
const choiceName = choice.choiceName;
|
|
315
|
+
if (choiceName && choiceName in item) {
|
|
316
|
+
const value = item[choiceName];
|
|
317
|
+
const primitiveElementName = `_${choiceName}`;
|
|
318
|
+
const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
|
|
319
|
+
|
|
320
|
+
// Box with the specific choice type
|
|
321
|
+
const choiceTypeInfo = {
|
|
322
|
+
type: choice.type,
|
|
323
|
+
singleton: !Array.isArray(value),
|
|
324
|
+
modelContext: choice
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
if (Array.isArray(value)) {
|
|
328
|
+
for (const v of value) {
|
|
329
|
+
results.push(box(v, { ...choiceTypeInfo, singleton: true }, primitiveElement));
|
|
330
|
+
}
|
|
331
|
+
} else if (value !== null && value !== undefined) {
|
|
332
|
+
results.push(box(value, choiceTypeInfo, primitiveElement));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else if (name in item) {
|
|
337
|
+
// Regular property navigation
|
|
338
|
+
const value = item[name];
|
|
339
|
+
const primitiveElementName = `_${name}`;
|
|
340
|
+
const primitiveElement = (primitiveElementName in item) ? item[primitiveElementName] : undefined;
|
|
341
|
+
|
|
342
|
+
if (Array.isArray(value)) {
|
|
343
|
+
// Box each array element with type info
|
|
344
|
+
// For arrays, make the type singleton since each element is a single value
|
|
345
|
+
const elementTypeInfo = nodeTypeInfo ? { ...nodeTypeInfo, singleton: true } : undefined;
|
|
346
|
+
for (const v of value) {
|
|
347
|
+
// Special handling for FHIR resources - use their resourceType
|
|
348
|
+
// Do this if the property navigation has type 'Any' (polymorphic reference) or no type info
|
|
349
|
+
if (v && typeof v === 'object' && 'resourceType' in v && typeof v.resourceType === 'string') {
|
|
350
|
+
// Get full type info from model provider if available
|
|
351
|
+
let resourceTypeInfo;
|
|
352
|
+
if (context.modelProvider) {
|
|
353
|
+
resourceTypeInfo = context.modelProvider.getType(v.resourceType);
|
|
354
|
+
if (resourceTypeInfo) {
|
|
355
|
+
// Make it singleton since it's a single element in the array
|
|
356
|
+
resourceTypeInfo = { ...resourceTypeInfo, singleton: true };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!resourceTypeInfo) {
|
|
360
|
+
// Fallback to basic type info
|
|
361
|
+
resourceTypeInfo = {
|
|
362
|
+
type: v.resourceType as import('./types').TypeName,
|
|
363
|
+
singleton: true
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
results.push(box(v, resourceTypeInfo, primitiveElement));
|
|
367
|
+
} else {
|
|
368
|
+
results.push(box(v, elementTypeInfo, primitiveElement));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} else if (value !== null && value !== undefined) {
|
|
372
|
+
// Special handling for FHIR resources - use their resourceType
|
|
373
|
+
// Do this if the property navigation has type 'Any' (polymorphic reference) or no type info
|
|
374
|
+
if (value && typeof value === 'object' && 'resourceType' in value && typeof value.resourceType === 'string') {
|
|
375
|
+
// Get full type info from model provider if available
|
|
376
|
+
let resourceTypeInfo;
|
|
377
|
+
if (context.modelProvider) {
|
|
378
|
+
resourceTypeInfo = context.modelProvider.getType(value.resourceType);
|
|
379
|
+
if (resourceTypeInfo) {
|
|
380
|
+
// Preserve singleton status
|
|
381
|
+
resourceTypeInfo = { ...resourceTypeInfo, singleton: !Array.isArray(value) };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!resourceTypeInfo) {
|
|
385
|
+
// Fallback to basic type info
|
|
386
|
+
resourceTypeInfo = {
|
|
387
|
+
type: value.resourceType as import('./types').TypeName,
|
|
388
|
+
singleton: !Array.isArray(value)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
results.push(box(value, resourceTypeInfo, primitiveElement));
|
|
392
|
+
} else {
|
|
393
|
+
// Box single value with primitive element if available
|
|
394
|
+
results.push(box(value, nodeTypeInfo, primitiveElement));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
267
397
|
}
|
|
268
398
|
}
|
|
269
399
|
}
|
|
@@ -275,14 +405,15 @@ export class Interpreter {
|
|
|
275
405
|
}
|
|
276
406
|
|
|
277
407
|
// TypeOrIdentifier node evaluator (handles Patient, Observation, etc.)
|
|
278
|
-
private evaluateTypeOrIdentifier(node: ASTNode, input:
|
|
408
|
+
private evaluateTypeOrIdentifier(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
279
409
|
const typeOrId = node as TypeOrIdentifierNode;
|
|
280
410
|
const name = typeOrId.name;
|
|
281
411
|
|
|
282
412
|
// First try as type filter
|
|
283
|
-
const filtered = input.filter(
|
|
284
|
-
|
|
285
|
-
|
|
413
|
+
const filtered = input.filter(boxedItem => {
|
|
414
|
+
const item = unbox(boxedItem);
|
|
415
|
+
return item && typeof item === 'object' && item.resourceType === name;
|
|
416
|
+
});
|
|
286
417
|
|
|
287
418
|
if (filtered.length > 0) {
|
|
288
419
|
return { value: filtered, context };
|
|
@@ -293,7 +424,7 @@ export class Interpreter {
|
|
|
293
424
|
}
|
|
294
425
|
|
|
295
426
|
// Binary operator evaluator
|
|
296
|
-
private evaluateBinary(node: ASTNode, input:
|
|
427
|
+
private evaluateBinary(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
297
428
|
const binary = node as BinaryNode;
|
|
298
429
|
const operator = binary.operator;
|
|
299
430
|
|
|
@@ -335,11 +466,11 @@ export class Interpreter {
|
|
|
335
466
|
}
|
|
336
467
|
|
|
337
468
|
// If no evaluator found, throw error
|
|
338
|
-
throw
|
|
469
|
+
throw Errors.noEvaluatorFound('binary operator', operator);
|
|
339
470
|
}
|
|
340
471
|
|
|
341
472
|
// Unary operator evaluator
|
|
342
|
-
private evaluateUnary(node: ASTNode, input:
|
|
473
|
+
private evaluateUnary(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
343
474
|
const unary = node as UnaryNode;
|
|
344
475
|
const operator = unary.operator;
|
|
345
476
|
|
|
@@ -358,11 +489,11 @@ export class Interpreter {
|
|
|
358
489
|
}
|
|
359
490
|
|
|
360
491
|
// If no evaluator found, throw error
|
|
361
|
-
throw
|
|
492
|
+
throw Errors.noEvaluatorFound('unary operator', operator);
|
|
362
493
|
}
|
|
363
494
|
|
|
364
495
|
// Variable evaluator
|
|
365
|
-
private evaluateVariable(node: ASTNode, input:
|
|
496
|
+
private evaluateVariable(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
366
497
|
const variable = node as VariableNode;
|
|
367
498
|
const name = variable.name;
|
|
368
499
|
|
|
@@ -371,17 +502,19 @@ export class Interpreter {
|
|
|
371
502
|
if (value !== undefined) {
|
|
372
503
|
// Ensure value is always an array
|
|
373
504
|
const arrayValue = Array.isArray(value) ? value : [value];
|
|
374
|
-
|
|
505
|
+
// Box each value in the array
|
|
506
|
+
const boxedValues = arrayValue.map(v => ensureBoxed(v));
|
|
507
|
+
return { value: boxedValues, context };
|
|
375
508
|
}
|
|
376
509
|
|
|
377
510
|
// According to FHIRPath spec: attempting to access an undefined environment variable will result in an error
|
|
378
|
-
throw
|
|
511
|
+
throw Errors.variableNotDefined(name);
|
|
379
512
|
}
|
|
380
513
|
|
|
381
514
|
// Collection evaluator
|
|
382
|
-
private evaluateCollection(node: ASTNode, input:
|
|
515
|
+
private evaluateCollection(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
383
516
|
const collection = node as CollectionNode;
|
|
384
|
-
const results:
|
|
517
|
+
const results: FHIRPathValue[] = [];
|
|
385
518
|
|
|
386
519
|
for (const element of collection.elements) {
|
|
387
520
|
const result = this.evaluate(element, input, context);
|
|
@@ -403,11 +536,11 @@ export class Interpreter {
|
|
|
403
536
|
}
|
|
404
537
|
|
|
405
538
|
// No function found in registry
|
|
406
|
-
throw
|
|
539
|
+
throw Errors.unknownFunction(funcName);
|
|
407
540
|
}
|
|
408
541
|
|
|
409
542
|
// Index evaluator
|
|
410
|
-
private evaluateIndex(node: ASTNode, input:
|
|
543
|
+
private evaluateIndex(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
411
544
|
const indexNode = node as IndexNode;
|
|
412
545
|
const exprResult = this.evaluate(indexNode.expression, input, context);
|
|
413
546
|
const indexResult = this.evaluate(indexNode.index, input, context);
|
|
@@ -416,48 +549,166 @@ export class Interpreter {
|
|
|
416
549
|
return { value: [], context };
|
|
417
550
|
}
|
|
418
551
|
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
421
|
-
|
|
552
|
+
const boxedIndex = indexResult.value[0];
|
|
553
|
+
if (boxedIndex) {
|
|
554
|
+
const index = unbox(boxedIndex);
|
|
555
|
+
if (typeof index === 'number' && index >= 0 && index < exprResult.value.length) {
|
|
556
|
+
const result = exprResult.value[index];
|
|
557
|
+
return { value: result ? [result] : [], context };
|
|
558
|
+
}
|
|
422
559
|
}
|
|
423
560
|
|
|
424
561
|
return { value: [], context };
|
|
425
562
|
}
|
|
426
563
|
|
|
427
564
|
// Type membership test (is operator)
|
|
428
|
-
private evaluateMembershipTest(node: ASTNode, input:
|
|
565
|
+
private evaluateMembershipTest(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
429
566
|
const test = node as MembershipTestNode;
|
|
430
567
|
const exprResult = this.evaluate(test.expression, input, context);
|
|
431
568
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
569
|
+
// If expression evaluates to empty, return empty
|
|
570
|
+
if (exprResult.value.length === 0) {
|
|
571
|
+
return { value: [], context };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// If we have type information from analyzer (with ModelProvider), use it
|
|
575
|
+
if (context.currentNode?.typeInfo?.modelContext) {
|
|
576
|
+
const modelContext = context.currentNode.typeInfo.modelContext as any;
|
|
577
|
+
|
|
578
|
+
// For union types, check if the target type is valid
|
|
579
|
+
if (modelContext.isUnion && modelContext.choices) {
|
|
580
|
+
const hasValidChoice = modelContext.choices.some((c: any) =>
|
|
581
|
+
c.type === test.targetType || c.elementType === test.targetType
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
if (!hasValidChoice) {
|
|
585
|
+
// Type system knows this will always be false
|
|
586
|
+
return {
|
|
587
|
+
value: exprResult.value.map(() => box(false, { type: 'Boolean', singleton: true })),
|
|
588
|
+
context
|
|
589
|
+
};
|
|
590
|
+
}
|
|
440
591
|
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Type checking with subtype support via ModelProvider
|
|
595
|
+
const results = exprResult.value.map(boxedItem => {
|
|
596
|
+
const item = unbox(boxedItem);
|
|
597
|
+
|
|
598
|
+
// If we have a ModelProvider and typeInfo, use it for accurate subtype checking
|
|
599
|
+
if (context.modelProvider && boxedItem.typeInfo) {
|
|
600
|
+
const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, test.targetType as import('./types').TypeName);
|
|
601
|
+
return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// For FHIR resources without typeInfo, try to get it from modelProvider
|
|
605
|
+
if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
|
|
606
|
+
const typeInfo = context.modelProvider.getType(item.resourceType);
|
|
607
|
+
if (typeInfo) {
|
|
608
|
+
const matchingType = context.modelProvider.ofType(typeInfo, test.targetType as import('./types').TypeName);
|
|
609
|
+
return box(matchingType !== undefined, { type: 'Boolean', singleton: true });
|
|
610
|
+
}
|
|
611
|
+
// Fall back to exact match
|
|
612
|
+
return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Check for FHIR resource types (no ModelProvider available)
|
|
616
|
+
if (item && typeof item === 'object' && 'resourceType' in item) {
|
|
617
|
+
return box(item.resourceType === test.targetType, { type: 'Boolean', singleton: true });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Check primitive types
|
|
621
|
+
const isMatch = (() => {
|
|
622
|
+
switch (test.targetType) {
|
|
623
|
+
case 'String': return typeof item === 'string';
|
|
624
|
+
case 'Boolean': return typeof item === 'boolean';
|
|
625
|
+
case 'Integer': return Number.isInteger(item);
|
|
626
|
+
case 'Decimal': return typeof item === 'number';
|
|
627
|
+
case 'Date':
|
|
628
|
+
case 'DateTime':
|
|
629
|
+
case 'Time':
|
|
630
|
+
// Simple check for date-like strings
|
|
631
|
+
return typeof item === 'string' && !isNaN(Date.parse(item));
|
|
632
|
+
default: return false;
|
|
633
|
+
}
|
|
634
|
+
})();
|
|
635
|
+
return box(isMatch, { type: 'Boolean', singleton: true });
|
|
441
636
|
});
|
|
442
637
|
|
|
443
638
|
return { value: results, context };
|
|
444
639
|
}
|
|
445
640
|
|
|
446
641
|
// Type cast (as operator)
|
|
447
|
-
private evaluateTypeCast(node: ASTNode, input:
|
|
642
|
+
private evaluateTypeCast(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
448
643
|
const cast = node as TypeCastNode;
|
|
449
644
|
const exprResult = this.evaluate(cast.expression, input, context);
|
|
450
645
|
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
646
|
+
// If we have type information from analyzer (with ModelProvider), use it
|
|
647
|
+
if (context.currentNode?.typeInfo?.modelContext) {
|
|
648
|
+
const modelContext = context.currentNode.typeInfo.modelContext as any;
|
|
649
|
+
|
|
650
|
+
// For union types, check if the cast is valid
|
|
651
|
+
if (modelContext.isUnion && modelContext.choices) {
|
|
652
|
+
const validChoice = modelContext.choices.find((c: any) =>
|
|
653
|
+
c.type === cast.targetType || c.elementType === cast.targetType
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
if (!validChoice) {
|
|
657
|
+
// Invalid cast - return empty
|
|
658
|
+
return { value: [], context };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Filter values that match the target type with subtype support
|
|
664
|
+
const filtered = exprResult.value.filter(boxedItem => {
|
|
665
|
+
const item = unbox(boxedItem);
|
|
666
|
+
|
|
667
|
+
// If we have a ModelProvider and typeInfo, use it for accurate subtype checking
|
|
668
|
+
if (context.modelProvider && boxedItem.typeInfo) {
|
|
669
|
+
const matchingType = context.modelProvider.ofType(boxedItem.typeInfo, cast.targetType as import('./types').TypeName);
|
|
670
|
+
return matchingType !== undefined;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// For FHIR resources without typeInfo, try to get it from modelProvider
|
|
674
|
+
if (context.modelProvider && item && typeof item === 'object' && 'resourceType' in item && typeof item.resourceType === 'string') {
|
|
675
|
+
const typeInfo = context.modelProvider.getType(item.resourceType);
|
|
676
|
+
if (typeInfo) {
|
|
677
|
+
const matchingType = context.modelProvider.ofType(typeInfo, cast.targetType as import('./types').TypeName);
|
|
678
|
+
return matchingType !== undefined;
|
|
679
|
+
}
|
|
680
|
+
// Fall back to exact match
|
|
681
|
+
return item.resourceType === cast.targetType;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Check for FHIR resource types (no ModelProvider available)
|
|
685
|
+
if (item && typeof item === 'object' && 'resourceType' in item) {
|
|
686
|
+
return item.resourceType === cast.targetType;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Check primitive types
|
|
690
|
+
switch (cast.targetType) {
|
|
691
|
+
case 'String': return typeof item === 'string';
|
|
692
|
+
case 'Boolean': return typeof item === 'boolean';
|
|
693
|
+
case 'Integer': return Number.isInteger(item);
|
|
694
|
+
case 'Decimal': return typeof item === 'number';
|
|
695
|
+
case 'Date':
|
|
696
|
+
case 'DateTime':
|
|
697
|
+
case 'Time':
|
|
698
|
+
// Simple check for date-like strings
|
|
699
|
+
return typeof item === 'string' && !isNaN(Date.parse(item));
|
|
700
|
+
default: return false;
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
return { value: filtered, context };
|
|
454
705
|
}
|
|
455
706
|
|
|
456
|
-
private evaluateQuantity(node: ASTNode, input:
|
|
707
|
+
private evaluateQuantity(node: ASTNode, input: FHIRPathValue[], context: RuntimeContext): EvaluationResult {
|
|
457
708
|
const quantity = node as QuantityNode;
|
|
458
709
|
const quantityValue = createQuantity(quantity.value, quantity.unit, quantity.isCalendarUnit);
|
|
459
710
|
return {
|
|
460
|
-
value: [quantityValue],
|
|
711
|
+
value: [box(quantityValue, { type: 'Quantity', singleton: true })],
|
|
461
712
|
context
|
|
462
713
|
};
|
|
463
714
|
}
|
package/src/model-provider.ts
CHANGED
|
@@ -76,14 +76,54 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
|
76
76
|
'Count': 'Quantity'
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
// Common FHIR types to preload
|
|
79
|
+
// Common FHIR types to preload - load all resource types and common data types
|
|
80
80
|
private readonly commonTypes = [
|
|
81
|
-
|
|
82
|
-
'
|
|
83
|
-
|
|
81
|
+
// Base resources
|
|
82
|
+
'Resource', 'DomainResource',
|
|
83
|
+
// All FHIR R4 resources (alphabetical)
|
|
84
|
+
'Account', 'ActivityDefinition', 'AdverseEvent', 'AllergyIntolerance', 'Appointment',
|
|
85
|
+
'AppointmentResponse', 'AuditEvent', 'Basic', 'Binary', 'BiologicallyDerivedProduct',
|
|
86
|
+
'BodyStructure', 'Bundle', 'CapabilityStatement', 'CarePlan', 'CareTeam',
|
|
87
|
+
'CatalogEntry', 'ChargeItem', 'ChargeItemDefinition', 'Claim', 'ClaimResponse',
|
|
88
|
+
'ClinicalImpression', 'CodeSystem', 'Communication', 'CommunicationRequest', 'CompartmentDefinition',
|
|
89
|
+
'Composition', 'ConceptMap', 'Condition', 'Consent', 'Contract',
|
|
90
|
+
'Coverage', 'CoverageEligibilityRequest', 'CoverageEligibilityResponse', 'DetectedIssue', 'Device',
|
|
91
|
+
'DeviceDefinition', 'DeviceMetric', 'DeviceRequest', 'DeviceUseStatement', 'DiagnosticReport',
|
|
92
|
+
'DocumentManifest', 'DocumentReference', 'EffectEvidenceSynthesis', 'Encounter', 'Endpoint',
|
|
93
|
+
'EnrollmentRequest', 'EnrollmentResponse', 'EpisodeOfCare', 'EventDefinition', 'Evidence',
|
|
94
|
+
'EvidenceVariable', 'ExampleScenario', 'ExplanationOfBenefit', 'FamilyMemberHistory', 'Flag',
|
|
95
|
+
'Goal', 'GraphDefinition', 'Group', 'GuidanceResponse', 'HealthcareService',
|
|
96
|
+
'ImagingStudy', 'Immunization', 'ImmunizationEvaluation', 'ImmunizationRecommendation', 'ImplementationGuide',
|
|
97
|
+
'InsurancePlan', 'Invoice', 'Library', 'Linkage', 'List',
|
|
98
|
+
'Location', 'Measure', 'MeasureReport', 'Media', 'Medication',
|
|
99
|
+
'MedicationAdministration', 'MedicationDispense', 'MedicationKnowledge', 'MedicationRequest', 'MedicationStatement',
|
|
100
|
+
'MedicinalProduct', 'MedicinalProductAuthorization', 'MedicinalProductContraindication', 'MedicinalProductIndication', 'MedicinalProductIngredient',
|
|
101
|
+
'MedicinalProductInteraction', 'MedicinalProductManufactured', 'MedicinalProductPackaged', 'MedicinalProductPharmaceutical', 'MedicinalProductUndesirableEffect',
|
|
102
|
+
'MessageDefinition', 'MessageHeader', 'MolecularSequence', 'NamingSystem', 'NutritionOrder',
|
|
103
|
+
'Observation', 'ObservationDefinition', 'OperationDefinition', 'OperationOutcome', 'Organization',
|
|
104
|
+
'OrganizationAffiliation', 'Parameters', 'Patient', 'PaymentNotice', 'PaymentReconciliation',
|
|
105
|
+
'Person', 'PlanDefinition', 'Practitioner', 'PractitionerRole', 'Procedure',
|
|
106
|
+
'Provenance', 'Questionnaire', 'QuestionnaireResponse', 'RelatedPerson', 'RequestGroup',
|
|
107
|
+
'ResearchDefinition', 'ResearchElementDefinition', 'ResearchStudy', 'ResearchSubject', 'RiskAssessment',
|
|
108
|
+
'RiskEvidenceSynthesis', 'Schedule', 'SearchParameter', 'ServiceRequest', 'Slot',
|
|
109
|
+
'Specimen', 'SpecimenDefinition', 'StructureDefinition', 'StructureMap', 'Subscription',
|
|
110
|
+
'Substance', 'SubstanceNucleicAcid', 'SubstancePolymer', 'SubstanceProtein', 'SubstanceReferenceInformation',
|
|
111
|
+
'SubstanceSourceMaterial', 'SubstanceSpecification', 'SupplyDelivery', 'SupplyRequest', 'Task',
|
|
112
|
+
'TerminologyCapabilities', 'TestReport', 'TestScript', 'ValueSet', 'VerificationResult',
|
|
113
|
+
'VisionPrescription',
|
|
114
|
+
// Common data types
|
|
115
|
+
'HumanName', 'CodeableConcept', 'Coding', 'Extension', 'Reference',
|
|
116
|
+
'Identifier', 'Period', 'ContactPoint', 'Address', 'Attachment',
|
|
117
|
+
'Meta', 'Narrative', 'Quantity', 'SimpleQuantity', 'Age', 'Distance',
|
|
118
|
+
'Duration', 'Count', 'Money', 'Range', 'Ratio', 'SampledData',
|
|
119
|
+
'Timing', 'Annotation', 'Signature', 'ContactDetail', 'Contributor',
|
|
120
|
+
'DataRequirement', 'ParameterDefinition', 'RelatedArtifact', 'TriggerDefinition',
|
|
121
|
+
'UsageContext', 'Dosage', 'ElementDefinition'
|
|
84
122
|
];
|
|
85
123
|
|
|
86
|
-
constructor(private config: FHIRModelProviderConfig
|
|
124
|
+
constructor(private config: FHIRModelProviderConfig = {
|
|
125
|
+
packages: [{ name: 'hl7.fhir.r4.core', version: '4.0.1' }]
|
|
126
|
+
}) {
|
|
87
127
|
const canonicalConfig: Config = {
|
|
88
128
|
packages: config.packages.map(p => `${p.name}@${p.version}`),
|
|
89
129
|
workingDir: config.cacheDir || './tmp/.fhir-cache'
|
|
@@ -203,7 +243,12 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
|
203
243
|
}
|
|
204
244
|
|
|
205
245
|
private mapToFHIRPathType(fhirType: string): TypeName {
|
|
206
|
-
|
|
246
|
+
// If it's a mapped type (primitive or special types), use the mapping
|
|
247
|
+
if (this.typeMapping[fhirType]) {
|
|
248
|
+
return this.typeMapping[fhirType];
|
|
249
|
+
}
|
|
250
|
+
// Otherwise, keep the FHIR type name (for complex types like CodeableConcept)
|
|
251
|
+
return fhirType as TypeName;
|
|
207
252
|
}
|
|
208
253
|
|
|
209
254
|
private isChoiceType(element: any): boolean {
|
|
@@ -373,8 +418,27 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
|
373
418
|
return undefined;
|
|
374
419
|
}
|
|
375
420
|
|
|
376
|
-
// For non-union types, check
|
|
377
|
-
|
|
421
|
+
// For non-union types, check if the type matches or is a subtype
|
|
422
|
+
// First check direct match on FHIRPath type
|
|
423
|
+
if (type.type === typeName) {
|
|
424
|
+
return type;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Check if the type name matches
|
|
428
|
+
if (type.name === typeName) {
|
|
429
|
+
return type;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Check if any of the schemas in the hierarchy match
|
|
433
|
+
if (context?.schemaHierarchy) {
|
|
434
|
+
for (const schema of context.schemaHierarchy) {
|
|
435
|
+
if (schema.type === typeName || schema.name === typeName) {
|
|
436
|
+
return type;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return undefined;
|
|
378
442
|
}
|
|
379
443
|
|
|
380
444
|
getElementNames(parentType: TypeInfo<FHIRModelContext>): string[] {
|
|
@@ -398,6 +462,44 @@ export class FHIRModelProvider implements ModelProvider<FHIRModelContext> {
|
|
|
398
462
|
|
|
399
463
|
return Array.from(names);
|
|
400
464
|
}
|
|
465
|
+
|
|
466
|
+
getChildrenType(parentType: TypeInfo<FHIRModelContext>): TypeInfo<FHIRModelContext> | undefined {
|
|
467
|
+
const elementNames = this.getElementNames(parentType);
|
|
468
|
+
if (elementNames.length === 0) return undefined;
|
|
469
|
+
|
|
470
|
+
// Collect all unique child types
|
|
471
|
+
const childTypes = new Map<string, TypeInfo<FHIRModelContext>>();
|
|
472
|
+
|
|
473
|
+
for (const elementName of elementNames) {
|
|
474
|
+
const elementType = this.getElementType(parentType, elementName);
|
|
475
|
+
if (elementType) {
|
|
476
|
+
// Use a combination of namespace and name as key to deduplicate
|
|
477
|
+
const key = `${elementType.namespace || ''}.${elementType.name || elementType.type}`;
|
|
478
|
+
childTypes.set(key, elementType);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (childTypes.size === 0) return undefined;
|
|
483
|
+
|
|
484
|
+
// Create a union type representing all possible children
|
|
485
|
+
return {
|
|
486
|
+
type: 'Any',
|
|
487
|
+
namespace: parentType.namespace,
|
|
488
|
+
name: 'ChildrenUnion',
|
|
489
|
+
singleton: false, // children() always returns a collection
|
|
490
|
+
modelContext: {
|
|
491
|
+
path: `${parentType.modelContext?.path || ''}.children()`,
|
|
492
|
+
schemaHierarchy: [],
|
|
493
|
+
isUnion: true,
|
|
494
|
+
choices: Array.from(childTypes.values()).map(type => ({
|
|
495
|
+
type: type.type as TypeName,
|
|
496
|
+
code: type.name || type.type,
|
|
497
|
+
namespace: type.namespace,
|
|
498
|
+
modelContext: type.modelContext
|
|
499
|
+
}))
|
|
500
|
+
} as FHIRModelContext
|
|
501
|
+
};
|
|
502
|
+
}
|
|
401
503
|
|
|
402
504
|
// Async helper methods for loading additional schemas
|
|
403
505
|
async loadType(typeName: string): Promise<TypeInfo<FHIRModelContext> | undefined> {
|