22 * Parser for ViewDefinition JSON structures.
33 */
44
5- import { JsonValue , TestSuite , ViewDefinition } from "./types.js" ;
5+ import {
6+ TestSuite ,
7+ UnvalidatedColumn ,
8+ UnvalidatedSelect ,
9+ UnvalidatedViewDefinition ,
10+ ViewDefinition ,
11+ ViewDefinitionColumn ,
12+ ViewDefinitionSelect ,
13+ } from "./types.js" ;
614
715export class ViewDefinitionParser {
816 /**
917 * Parse a ViewDefinition from JSON.
1018 */
1119 static parseViewDefinition ( json : string | object ) : ViewDefinition {
12- const data = typeof json === "string" ? JSON . parse ( json ) : json ;
20+ const data : UnvalidatedViewDefinition =
21+ typeof json === "string" ? JSON . parse ( json ) : json ;
1322
1423 // Only check resourceType if it's present (for backwards compatibility with test cases)
1524 if ( data . resourceType && data . resourceType !== "ViewDefinition" ) {
1625 throw new Error ( "Invalid resource type. Expected ViewDefinition." ) ;
1726 }
1827
19- return this . validateViewDefinition ( data ) ;
28+ if ( this . isValidViewDefinition ( data ) ) {
29+ return data ;
30+ }
31+
32+ throw new Error ( "Invalid ViewDefinition structure." ) ;
2033 }
2134
2235 /**
@@ -33,10 +46,12 @@ export class ViewDefinitionParser {
3346 }
3447
3548 /**
36- * Validate a ViewDefinition structure.
49+ * Validate and narrow a ViewDefinition structure using type predicate .
3750 */
38- private static validateViewDefinition ( data : JsonValue ) : ViewDefinition {
39- if ( ! data . resource ) {
51+ private static isValidViewDefinition (
52+ data : UnvalidatedViewDefinition ,
53+ ) : data is ViewDefinition {
54+ if ( ! data . resource || typeof data . resource !== "string" ) {
4055 throw new Error ( "ViewDefinition must specify a resource type." ) ;
4156 }
4257
@@ -54,62 +69,46 @@ export class ViewDefinitionParser {
5469 data . status ??= "active" ;
5570
5671 // Validate select elements
57- this . validateSelectElements ( data . select ) ;
58-
59- return data as ViewDefinition ;
60- }
61-
62- /**
63- * Validate select elements recursively.
64- */
65- private static validateSelectElements ( selects : JsonValue [ ] ) : void {
66- for ( const select of selects ) {
67- this . validateSelectElement ( select ) ;
72+ for ( const select of data . select ) {
73+ if ( ! this . isValidSelect ( select ) ) {
74+ return false ;
75+ }
6876 }
77+
78+ return true ;
6979 }
7080
7181 /**
72- * Validate a single select element.
82+ * Validate select element using type predicate .
7383 */
74- private static validateSelectElement ( select : JsonValue ) : void {
75- this . validateSelectElementStructure ( select ) ;
76- this . validateSelectElementContent ( select ) ;
77- this . validateSelectElementExpressions ( select ) ;
84+ private static isValidSelect (
85+ select : UnvalidatedSelect ,
86+ ) : select is ViewDefinitionSelect {
87+ this . validateSelectStructure ( select ) ;
88+ this . validateSelectExpressions ( select ) ;
89+
90+ return (
91+ this . validateSelectColumns ( select ) &&
92+ this . validateNestedSelects ( select ) &&
93+ this . validateUnionAll ( select )
94+ ) ;
7895 }
7996
8097 /**
8198 * Validate select element has required structure.
8299 */
83- private static validateSelectElementStructure ( select : JsonValue ) : void {
100+ private static validateSelectStructure ( select : UnvalidatedSelect ) : void {
84101 if ( ! select . column && ! select . select && ! select . unionAll ) {
85102 throw new Error (
86103 "Select element must have columns, nested selects, or unionAll." ,
87104 ) ;
88105 }
89106 }
90107
91- /**
92- * Validate select element content (columns and nested elements).
93- */
94- private static validateSelectElementContent ( select : JsonValue ) : void {
95- if ( select . column ) {
96- this . validateColumns ( select . column , select ) ;
97- }
98-
99- if ( select . select ) {
100- this . validateSelectElements ( select . select ) ;
101- }
102-
103- if ( select . unionAll ) {
104- this . validateSelectElements ( select . unionAll ) ;
105- this . validateUnionAllColumns ( select . unionAll ) ;
106- }
107- }
108-
109108 /**
110109 * Validate forEach and forEachOrNull expressions.
111110 */
112- private static validateSelectElementExpressions ( select : JsonValue ) : void {
111+ private static validateSelectExpressions ( select : UnvalidatedSelect ) : void {
113112 if ( select . forEach && typeof select . forEach !== "string" ) {
114113 throw new Error ( "forEach must be a string FHIRPath expression." ) ;
115114 }
@@ -120,43 +119,86 @@ export class ViewDefinitionParser {
120119 }
121120
122121 /**
123- * Validate column definitions .
122+ * Validate columns in a select element .
124123 */
125- private static validateColumns (
126- columns : JsonValue [ ] ,
127- selectContext ?: JsonValue ,
128- ) : void {
129- for ( const column of columns ) {
130- if ( ! column . name || typeof column . name !== "string" ) {
131- throw new Error ( "Column must have a valid name." ) ;
124+ private static validateSelectColumns ( select : UnvalidatedSelect ) : boolean {
125+ if ( select . column ) {
126+ for ( const column of select . column ) {
127+ if ( ! this . isValidColumn ( column , select ) ) {
128+ return false ;
129+ }
132130 }
131+ }
132+ return true ;
133+ }
133134
134- if ( ! column . path || typeof column . path !== "string" ) {
135- throw new Error ( "Column must have a valid FHIRPath expression." ) ;
135+ /**
136+ * Validate nested select elements.
137+ */
138+ private static validateNestedSelects ( select : UnvalidatedSelect ) : boolean {
139+ if ( select . select ) {
140+ for ( const nestedSelect of select . select ) {
141+ if ( ! this . isValidSelect ( nestedSelect ) ) {
142+ return false ;
143+ }
136144 }
145+ }
146+ return true ;
147+ }
137148
138- // Validate column name is database-friendly
139- if ( ! / ^ [ a - z A - Z _ ] \w * $ / . test ( column . name ) ) {
140- throw new Error (
141- `Column name '${ column . name } ' is not database-friendly. Use alphanumeric and underscores only.` ,
142- ) ;
149+ /**
150+ * Validate unionAll branches.
151+ */
152+ private static validateUnionAll ( select : UnvalidatedSelect ) : boolean {
153+ if ( select . unionAll ) {
154+ for ( const unionSelect of select . unionAll ) {
155+ if ( ! this . isValidSelect ( unionSelect ) ) {
156+ return false ;
157+ }
143158 }
159+ this . validateUnionAllColumns ( select . unionAll ) ;
160+ }
161+ return true ;
162+ }
163+
164+ /**
165+ * Validate column using type predicate.
166+ */
167+ private static isValidColumn (
168+ column : UnvalidatedColumn ,
169+ selectContext ?: UnvalidatedSelect ,
170+ ) : column is ViewDefinitionColumn {
171+ if ( ! column . name || typeof column . name !== "string" ) {
172+ throw new Error ( "Column must have a valid name." ) ;
173+ }
144174
145- // Validate collection constraints
146- this . validateCollectionConstraints ( column , selectContext ) ;
175+ if ( ! column . path || typeof column . path !== "string" ) {
176+ throw new Error ( "Column must have a valid FHIRPath expression." ) ;
147177 }
178+
179+ // Validate column name is database-friendly
180+ if ( ! / ^ [ a - z A - Z _ ] \w * $ / . test ( column . name ) ) {
181+ throw new Error (
182+ `Column name '${ column . name } ' is not database-friendly. Use alphanumeric and underscores only.` ,
183+ ) ;
184+ }
185+
186+ // Validate collection constraints
187+ this . validateCollectionConstraints ( column , selectContext ) ;
188+
189+ return true ;
148190 }
149191
150192 /**
151193 * Validate collection property constraints.
152194 */
153195 private static validateCollectionConstraints (
154- column : JsonValue ,
155- selectContext ?: JsonValue ,
196+ column : UnvalidatedColumn ,
197+ selectContext ?: UnvalidatedSelect ,
156198 ) : void {
157199 if ( column . collection === false ) {
158- // Check if the path could return multiple values
159- const path = column . path ;
200+ // At this point, path has been validated to be a string in isValidColumn
201+ const path = column . path as string ;
160202
161203 // Known array fields in FHIR Patient that could return multiple values
162204 const multiValuedPaths = [
@@ -186,7 +228,9 @@ export class ViewDefinitionParser {
186228 /**
187229 * Validate that all branches of a unionAll have the same columns in the same order.
188230 */
189- private static validateUnionAllColumns ( unionAllBranches : JsonValue [ ] ) : void {
231+ private static validateUnionAllColumns (
232+ unionAllBranches : UnvalidatedSelect [ ] ,
233+ ) : void {
190234 if ( unionAllBranches . length < 2 ) {
191235 return ; // Nothing to validate
192236 }
@@ -231,14 +275,17 @@ export class ViewDefinitionParser {
231275 * Handles direct columns, forEach columns, and nested select columns.
232276 */
233277 private static extractColumnsFromSelect (
234- select : JsonValue ,
278+ select : UnvalidatedSelect ,
235279 ) : Array < { name : string ; type ?: string } > {
236280 const columns : Array < { name : string ; type ?: string } > = [ ] ;
237281
238282 // Direct columns
239283 if ( select . column ) {
240284 for ( const column of select . column ) {
241- columns . push ( { name : column . name , type : column . type } ) ;
285+ columns . push ( {
286+ name : column . name as string ,
287+ type : column . type as string | undefined ,
288+ } ) ;
242289 }
243290 }
244291
@@ -272,7 +319,7 @@ export class ViewDefinitionParser {
272319 * Recursively collect column names from select elements.
273320 */
274321 private static collectColumnNames (
275- selects : JsonValue [ ] ,
322+ selects : ViewDefinitionSelect [ ] ,
276323 columns : string [ ] ,
277324 ) : void {
278325 for ( const select of selects ) {
0 commit comments