sof-mssql 1.0.1 → 2.0.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/README.md +175 -35
- package/dist/fhirpath/transpiler.d.ts +40 -2
- package/dist/fhirpath/transpiler.d.ts.map +1 -1
- package/dist/fhirpath/transpiler.js +85 -15
- package/dist/fhirpath/transpiler.js.map +1 -1
- package/dist/loader/tables.d.ts.map +1 -1
- package/dist/loader/tables.js +7 -0
- package/dist/loader/tables.js.map +1 -1
- package/dist/parser.d.ts +12 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +66 -9
- package/dist/parser.js.map +1 -1
- package/dist/queryGenerator/WhereClauseBuilder.d.ts +1 -0
- package/dist/queryGenerator/WhereClauseBuilder.d.ts.map +1 -1
- package/dist/queryGenerator/WhereClauseBuilder.js +6 -2
- package/dist/queryGenerator/WhereClauseBuilder.js.map +1 -1
- package/dist/queryGenerator.d.ts.map +1 -1
- package/dist/queryGenerator.js +13 -8
- package/dist/queryGenerator.js.map +1 -1
- package/dist/tests/utils/generator.js +11 -11
- package/dist/tests/utils/generator.js.map +1 -1
- package/dist/tests/utils/sqlOnFhir.d.ts.map +1 -1
- package/dist/tests/utils/sqlOnFhir.js +39 -4
- package/dist/tests/utils/sqlOnFhir.js.map +1 -1
- package/dist/tests/utils/testContext.d.ts +3 -6
- package/dist/tests/utils/testContext.d.ts.map +1 -1
- package/dist/tests/utils/testContext.js +5 -9
- package/dist/tests/utils/testContext.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +66 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +449 -0
- package/dist/validation.js.map +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🔥 MSSQL on FHIR 🔥
|
|
2
2
|
|
|
3
3
|
A TypeScript library and CLI tool for loading FHIR into Microsoft SQL Server,
|
|
4
4
|
and transpiling [SQL on FHIR](https://sql-on-fhir.org/) view definitions into
|
|
@@ -209,59 +209,76 @@ WHERE [r].[resource_type] = 'Patient'
|
|
|
209
209
|
|
|
210
210
|
### Basic usage
|
|
211
211
|
|
|
212
|
-
```
|
|
212
|
+
```typescript
|
|
213
213
|
import {SqlOnFhir} from 'sof-mssql';
|
|
214
214
|
|
|
215
|
-
// Create an instance with default configuration
|
|
216
215
|
const sqlOnFhir = new SqlOnFhir();
|
|
216
|
+
```
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
Create a ViewDefinition to transpile:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"resourceType": "ViewDefinition",
|
|
223
|
+
"resource": "Patient",
|
|
224
|
+
"name": "patient_demographics",
|
|
225
|
+
"select": [
|
|
224
226
|
{
|
|
225
|
-
column: [
|
|
227
|
+
"column": [
|
|
226
228
|
{
|
|
227
|
-
name:
|
|
228
|
-
path:
|
|
229
|
-
type:
|
|
229
|
+
"name": "id",
|
|
230
|
+
"path": "id",
|
|
231
|
+
"type": "id"
|
|
230
232
|
},
|
|
231
233
|
{
|
|
232
|
-
name:
|
|
233
|
-
path:
|
|
234
|
-
type:
|
|
234
|
+
"name": "family_name",
|
|
235
|
+
"path": "name.family",
|
|
236
|
+
"type": "string"
|
|
235
237
|
},
|
|
236
238
|
{
|
|
237
|
-
name:
|
|
238
|
-
path:
|
|
239
|
-
type:
|
|
239
|
+
"name": "birth_date",
|
|
240
|
+
"path": "birthDate",
|
|
241
|
+
"type": "date"
|
|
240
242
|
}
|
|
241
243
|
]
|
|
242
244
|
}
|
|
243
245
|
]
|
|
244
|
-
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Transpile the ViewDefinition to T-SQL:
|
|
245
250
|
|
|
251
|
+
```typescript
|
|
246
252
|
const result = sqlOnFhir.transpile(viewDefinition);
|
|
247
253
|
|
|
248
254
|
console.log(result.sql);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Generated SQL output:
|
|
258
|
+
|
|
259
|
+
```sql
|
|
260
|
+
SELECT
|
|
261
|
+
r.id AS [id],
|
|
262
|
+
JSON_VALUE(r.json, '$.name[0].family') AS [family_name],
|
|
263
|
+
CAST(JSON_VALUE(r.json, '$.birthDate') AS DATETIME2) AS [birth_date]
|
|
264
|
+
FROM [dbo].[fhir_resources] AS [r]
|
|
265
|
+
WHERE [r].[resource_type] = 'Patient'
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Access column metadata:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
258
271
|
console.log(result.columns);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Column metadata output:
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
[
|
|
278
|
+
{ "name": "id", "type": "VARCHAR(64)", "nullable": true },
|
|
279
|
+
{ "name": "family_name", "type": "NVARCHAR(MAX)", "nullable": true },
|
|
280
|
+
{ "name": "birth_date", "type": "VARCHAR(10)", "nullable": true }
|
|
281
|
+
]
|
|
265
282
|
```
|
|
266
283
|
|
|
267
284
|
### Custom table configuration
|
|
@@ -292,8 +309,131 @@ const fhirResource = {
|
|
|
292
309
|
// ... rest of ViewDefinition
|
|
293
310
|
};
|
|
294
311
|
const result2 = sqlOnFhir.transpile(fhirResource);
|
|
312
|
+
````
|
|
313
|
+
|
|
314
|
+
### Type mappings and type hints
|
|
315
|
+
|
|
316
|
+
#### Default type mappings
|
|
317
|
+
|
|
318
|
+
By default, FHIR primitive types are mapped to the following T-SQL types:
|
|
319
|
+
|
|
320
|
+
| FHIR Type | Default T-SQL Type | Rationale |
|
|
321
|
+
|-----------------------------------------|--------------------|-------------------------------------------|
|
|
322
|
+
| `id` | `VARCHAR(64)` | ASCII-only, fixed max length |
|
|
323
|
+
| `boolean` | `BIT` | Native boolean |
|
|
324
|
+
| `integer`, `positiveint`, `unsignedint` | `INT` | 32-bit integer |
|
|
325
|
+
| `integer64` | `BIGINT` | 64-bit integer |
|
|
326
|
+
| `decimal` | `VARCHAR(MAX)` | Preserves arbitrary precision |
|
|
327
|
+
| `date` | `VARCHAR(10)` | Preserves partial dates (e.g., "2024-01") |
|
|
328
|
+
| `datetime` | `VARCHAR(50)` | Preserves partial datetimes and timezones |
|
|
329
|
+
| `instant` | `VARCHAR(50)` | Preserves full ISO 8601 format |
|
|
330
|
+
| `time` | `VARCHAR(20)` | Preserves partial times |
|
|
331
|
+
| `string`, `markdown`, `code` | `NVARCHAR(MAX)` | Unicode-capable text |
|
|
332
|
+
| `uri`, `url`, `canonical` | `NVARCHAR(MAX)` | Can contain Unicode (IRIs) |
|
|
333
|
+
| `uuid` | `VARCHAR(100)` | ASCII UUID format |
|
|
334
|
+
| `oid` | `VARCHAR(255)` | ASCII OID format |
|
|
335
|
+
| `base64binary` | `VARBINARY(MAX)` | Binary data |
|
|
336
|
+
|
|
337
|
+
**Design principle:** Default mappings use `VARCHAR` for temporal and numeric types to preserve FHIR semantics (partial dates, arbitrary precision decimals) rather than forcing conversion to SQL native types.
|
|
338
|
+
|
|
339
|
+
#### Using type hints
|
|
340
|
+
|
|
341
|
+
You can override default type mappings using the `tag` array on column definitions. Two tag types are supported:
|
|
342
|
+
|
|
343
|
+
**`tsql/type` - Direct T-SQL type specification:**
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"name": "birth_date",
|
|
348
|
+
"path": "birthDate",
|
|
349
|
+
"type": "date",
|
|
350
|
+
"tag": [
|
|
351
|
+
{ "name": "tsql/type", "value": "DATE" }
|
|
352
|
+
]
|
|
353
|
+
}
|
|
295
354
|
```
|
|
296
355
|
|
|
356
|
+
This generates a CAST expression: `CAST(JSON_VALUE(r.json, '$.birthDate') AS DATE) AS [birth_date]`
|
|
357
|
+
|
|
358
|
+
**`ansi/type` - ANSI/ISO SQL standard types (automatically converted to T-SQL):**
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
{
|
|
362
|
+
"name": "age",
|
|
363
|
+
"path": "age",
|
|
364
|
+
"type": "integer",
|
|
365
|
+
"tag": [
|
|
366
|
+
{ "name": "ansi/type", "value": "INTEGER" }
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The ANSI type `INTEGER` is automatically converted to T-SQL `INT`.
|
|
372
|
+
|
|
373
|
+
```json
|
|
374
|
+
{
|
|
375
|
+
"name": "active",
|
|
376
|
+
"path": "active",
|
|
377
|
+
"type": "boolean",
|
|
378
|
+
"tag": [
|
|
379
|
+
{ "name": "ansi/type", "value": "BOOLEAN" }
|
|
380
|
+
]
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The ANSI type `BOOLEAN` is automatically converted to T-SQL `BIT`.
|
|
385
|
+
|
|
386
|
+
**Type precedence:** `tsql/type` > `ansi/type` > FHIR type defaults
|
|
387
|
+
|
|
388
|
+
**Supported ANSI types:**
|
|
389
|
+
- Character: `CHARACTER`, `CHARACTER VARYING`, `NATIONAL CHARACTER VARYING`
|
|
390
|
+
- Numeric: `INTEGER`, `SMALLINT`, `BIGINT`, `DECIMAL`, `NUMERIC`, `FLOAT`, `REAL`, `DOUBLE PRECISION`
|
|
391
|
+
- Temporal: `DATE`, `TIME`, `TIMESTAMP` (converted to `DATETIME2`)
|
|
392
|
+
- Boolean: `BOOLEAN` (converted to `BIT`)
|
|
393
|
+
|
|
394
|
+
**Example with multiple columns:**
|
|
395
|
+
|
|
396
|
+
This example demonstrates how different type hints affect the resulting SQL types:
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"resourceType": "ViewDefinition",
|
|
401
|
+
"resource": "Patient",
|
|
402
|
+
"select": [
|
|
403
|
+
{
|
|
404
|
+
"column": [
|
|
405
|
+
{
|
|
406
|
+
"name": "id",
|
|
407
|
+
"path": "id",
|
|
408
|
+
"type": "id"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"name": "birth_date",
|
|
412
|
+
"path": "birthDate",
|
|
413
|
+
"type": "date",
|
|
414
|
+
"tag": [
|
|
415
|
+
{ "name": "tsql/type", "value": "DATE" }
|
|
416
|
+
]
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"name": "deceased",
|
|
420
|
+
"path": "deceasedBoolean",
|
|
421
|
+
"type": "boolean",
|
|
422
|
+
"tag": [
|
|
423
|
+
{ "name": "ansi/type", "value": "BOOLEAN" }
|
|
424
|
+
]
|
|
425
|
+
}
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Type behaviour for each column:
|
|
433
|
+
- `id` - Uses default FHIR type mapping: `VARCHAR(64)`
|
|
434
|
+
- `birth_date` - Overrides default `VARCHAR(10)` with T-SQL `DATE` type
|
|
435
|
+
- `deceased` - Uses ANSI `BOOLEAN` type, automatically converted to T-SQL `BIT`
|
|
436
|
+
|
|
297
437
|
## Database setup
|
|
298
438
|
|
|
299
439
|
### Table structure
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* FHIRPath expression transpiler to T-SQL.
|
|
3
3
|
* Converts FHIRPath expressions to equivalent T-SQL expressions for MS SQL Server.
|
|
4
4
|
*/
|
|
5
|
+
import type { ViewDefinitionColumnTag } from "../types.js";
|
|
5
6
|
import { TranspilerContext } from "./visitor";
|
|
6
7
|
export { TranspilerContext } from "./visitor";
|
|
7
8
|
export declare class Transpiler {
|
|
@@ -11,8 +12,45 @@ export declare class Transpiler {
|
|
|
11
12
|
static transpile(expression: string, context: TranspilerContext): string;
|
|
12
13
|
private static parseExpression;
|
|
13
14
|
/**
|
|
14
|
-
* Get the SQL data type for a
|
|
15
|
+
* Get the SQL data type for a FHIR type, with optional tag-based override.
|
|
16
|
+
*
|
|
17
|
+
* Default mappings are conservative to accommodate ALL valid FHIR data,
|
|
18
|
+
* using MAX sizes where needed and NVARCHAR for any fields that could
|
|
19
|
+
* potentially contain Unicode characters.
|
|
20
|
+
*
|
|
21
|
+
* Design principles:
|
|
22
|
+
* - Use NVARCHAR for fields that may contain Unicode (string, code, uri/url/canonical)
|
|
23
|
+
* - Use VARCHAR for ASCII-only fields (id, uuid, oid, decimal, dates/times)
|
|
24
|
+
* - Use MAX or generous fixed sizes to prevent truncation
|
|
25
|
+
* - Preserve FHIR semantics (partial dates, arbitrary precision decimals)
|
|
26
|
+
*
|
|
27
|
+
* Users can optimise storage using type tags:
|
|
28
|
+
* - 'tsql/type' - Direct T-SQL type (e.g., 'DATE', 'VARCHAR(50)')
|
|
29
|
+
* - 'ansi/type' - ANSI/ISO SQL standard type (e.g., 'INTEGER', 'CHARACTER(50)', 'BOOLEAN')
|
|
30
|
+
*
|
|
31
|
+
* Type precedence: tsql/type > ansi/type > FHIR type defaults
|
|
32
|
+
*
|
|
33
|
+
* Example tag usage:
|
|
34
|
+
* - { "name": "tsql/type", "value": "DATE" } - Use T-SQL DATE type
|
|
35
|
+
* - { "name": "ansi/type", "value": "INTEGER" } - Use ANSI INTEGER (converted to T-SQL INT)
|
|
36
|
+
* - { "name": "ansi/type", "value": "BOOLEAN" } - Use ANSI BOOLEAN (converted to T-SQL BIT)
|
|
37
|
+
*
|
|
38
|
+
* @param fhirType - FHIR primitive type name (e.g., 'string', 'integer')
|
|
39
|
+
* @param tags - Optional array of column tags for type hints
|
|
40
|
+
* @returns MS SQL Server type specification
|
|
15
41
|
*/
|
|
16
|
-
static inferSqlType(fhirType?: string): string;
|
|
42
|
+
static inferSqlType(fhirType?: string, tags?: ViewDefinitionColumnTag[]): string;
|
|
43
|
+
/**
|
|
44
|
+
* Get type override from tsql/type or ansi/type tag if present.
|
|
45
|
+
*
|
|
46
|
+
* Precedence order:
|
|
47
|
+
* 1. tsql/type - Direct T-SQL type specification
|
|
48
|
+
* 2. ansi/type - ANSI/ISO SQL standard type (converted to T-SQL equivalent)
|
|
49
|
+
*/
|
|
50
|
+
private static getTagTypeOverride;
|
|
51
|
+
/**
|
|
52
|
+
* Get default MS SQL Server type mapping for a FHIR primitive type.
|
|
53
|
+
*/
|
|
54
|
+
private static getDefaultFhirTypeMapping;
|
|
17
55
|
}
|
|
18
56
|
//# sourceMappingURL=transpiler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transpiler.d.ts","sourceRoot":"","sources":["../../src/fhirpath/transpiler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,OAAO,EAAyB,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE9C,qBAAa,UAAU;IACrB;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM;IAkBxE,OAAO,CAAC,MAAM,CAAC,eAAe;IA4B9B
|
|
1
|
+
{"version":3,"file":"transpiler.d.ts","sourceRoot":"","sources":["../../src/fhirpath/transpiler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAyB,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGrE,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE9C,qBAAa,UAAU;IACrB;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM;IAkBxE,OAAO,CAAC,MAAM,CAAC,eAAe;IA4B9B;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,MAAM,CAAC,YAAY,CACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,uBAAuB,EAAE,GAC/B,MAAM;IAWT;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAuBjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,yBAAyB;CA0CzC"}
|
|
@@ -8,6 +8,7 @@ exports.Transpiler = void 0;
|
|
|
8
8
|
const antlr4ts_1 = require("antlr4ts");
|
|
9
9
|
const fhirpathLexer_1 = require("../generated/grammar/fhirpathLexer");
|
|
10
10
|
const fhirpathParser_1 = require("../generated/grammar/fhirpathParser");
|
|
11
|
+
const validation_js_1 = require("../validation.js");
|
|
11
12
|
const visitor_1 = require("./visitor");
|
|
12
13
|
class Transpiler {
|
|
13
14
|
/**
|
|
@@ -47,29 +48,98 @@ class Transpiler {
|
|
|
47
48
|
return { success: true, tree };
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
50
|
-
* Get the SQL data type for a
|
|
51
|
+
* Get the SQL data type for a FHIR type, with optional tag-based override.
|
|
52
|
+
*
|
|
53
|
+
* Default mappings are conservative to accommodate ALL valid FHIR data,
|
|
54
|
+
* using MAX sizes where needed and NVARCHAR for any fields that could
|
|
55
|
+
* potentially contain Unicode characters.
|
|
56
|
+
*
|
|
57
|
+
* Design principles:
|
|
58
|
+
* - Use NVARCHAR for fields that may contain Unicode (string, code, uri/url/canonical)
|
|
59
|
+
* - Use VARCHAR for ASCII-only fields (id, uuid, oid, decimal, dates/times)
|
|
60
|
+
* - Use MAX or generous fixed sizes to prevent truncation
|
|
61
|
+
* - Preserve FHIR semantics (partial dates, arbitrary precision decimals)
|
|
62
|
+
*
|
|
63
|
+
* Users can optimise storage using type tags:
|
|
64
|
+
* - 'tsql/type' - Direct T-SQL type (e.g., 'DATE', 'VARCHAR(50)')
|
|
65
|
+
* - 'ansi/type' - ANSI/ISO SQL standard type (e.g., 'INTEGER', 'CHARACTER(50)', 'BOOLEAN')
|
|
66
|
+
*
|
|
67
|
+
* Type precedence: tsql/type > ansi/type > FHIR type defaults
|
|
68
|
+
*
|
|
69
|
+
* Example tag usage:
|
|
70
|
+
* - { "name": "tsql/type", "value": "DATE" } - Use T-SQL DATE type
|
|
71
|
+
* - { "name": "ansi/type", "value": "INTEGER" } - Use ANSI INTEGER (converted to T-SQL INT)
|
|
72
|
+
* - { "name": "ansi/type", "value": "BOOLEAN" } - Use ANSI BOOLEAN (converted to T-SQL BIT)
|
|
73
|
+
*
|
|
74
|
+
* @param fhirType - FHIR primitive type name (e.g., 'string', 'integer')
|
|
75
|
+
* @param tags - Optional array of column tags for type hints
|
|
76
|
+
* @returns MS SQL Server type specification
|
|
51
77
|
*/
|
|
52
|
-
static inferSqlType(fhirType) {
|
|
78
|
+
static inferSqlType(fhirType, tags) {
|
|
79
|
+
// Check for tsql/type tag override.
|
|
80
|
+
const tagOverride = this.getTagTypeOverride(tags);
|
|
81
|
+
if (tagOverride) {
|
|
82
|
+
return tagOverride;
|
|
83
|
+
}
|
|
84
|
+
// Use default FHIR type mapping.
|
|
85
|
+
return this.getDefaultFhirTypeMapping(fhirType);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get type override from tsql/type or ansi/type tag if present.
|
|
89
|
+
*
|
|
90
|
+
* Precedence order:
|
|
91
|
+
* 1. tsql/type - Direct T-SQL type specification
|
|
92
|
+
* 2. ansi/type - ANSI/ISO SQL standard type (converted to T-SQL equivalent)
|
|
93
|
+
*/
|
|
94
|
+
static getTagTypeOverride(tags) {
|
|
95
|
+
if (!tags) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
// Check for tsql/type tag first (highest precedence)
|
|
99
|
+
const tsqlTypeTag = tags.find((tag) => tag.name === "tsql/type");
|
|
100
|
+
if (tsqlTypeTag) {
|
|
101
|
+
(0, validation_js_1.validateMsSqlType)(tsqlTypeTag.value);
|
|
102
|
+
return tsqlTypeTag.value;
|
|
103
|
+
}
|
|
104
|
+
// Check for ansi/type tag (lower precedence)
|
|
105
|
+
const ansiTypeTag = tags.find((tag) => tag.name === "ansi/type");
|
|
106
|
+
if (ansiTypeTag) {
|
|
107
|
+
return (0, validation_js_1.validateAnsiSqlType)(ansiTypeTag.value);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get default MS SQL Server type mapping for a FHIR primitive type.
|
|
113
|
+
*/
|
|
114
|
+
static getDefaultFhirTypeMapping(fhirType) {
|
|
115
|
+
// Conservative default type mappings based on FHIR R4 constraints.
|
|
116
|
+
// Sized to accommodate ALL valid FHIR data.
|
|
117
|
+
// Uses NVARCHAR for potential Unicode, VARCHAR for ASCII-only.
|
|
53
118
|
const typeMap = {
|
|
54
|
-
|
|
119
|
+
// ASCII-only types with fixed constraints
|
|
120
|
+
id: "VARCHAR(64)",
|
|
121
|
+
boolean: "BIT",
|
|
122
|
+
integer: "INT",
|
|
123
|
+
positiveint: "INT",
|
|
124
|
+
unsignedint: "INT",
|
|
125
|
+
integer64: "BIGINT",
|
|
126
|
+
// ASCII-only structured formats
|
|
127
|
+
uuid: "VARCHAR(100)",
|
|
128
|
+
oid: "VARCHAR(255)",
|
|
129
|
+
decimal: "VARCHAR(MAX)",
|
|
130
|
+
date: "VARCHAR(10)",
|
|
131
|
+
datetime: "VARCHAR(50)",
|
|
132
|
+
instant: "VARCHAR(50)",
|
|
133
|
+
time: "VARCHAR(20)",
|
|
134
|
+
// Unicode-capable text types
|
|
55
135
|
string: "NVARCHAR(MAX)",
|
|
56
136
|
markdown: "NVARCHAR(MAX)",
|
|
57
137
|
code: "NVARCHAR(MAX)",
|
|
138
|
+
// URIs (can be IRIs with Unicode)
|
|
58
139
|
uri: "NVARCHAR(MAX)",
|
|
59
140
|
url: "NVARCHAR(MAX)",
|
|
60
141
|
canonical: "NVARCHAR(MAX)",
|
|
61
|
-
|
|
62
|
-
oid: "NVARCHAR(MAX)",
|
|
63
|
-
boolean: "BIT",
|
|
64
|
-
integer: "INT",
|
|
65
|
-
positiveint: "INT",
|
|
66
|
-
unsignedint: "INT",
|
|
67
|
-
integer64: "BIGINT",
|
|
68
|
-
decimal: "DECIMAL(18,6)",
|
|
69
|
-
date: "DATETIME2",
|
|
70
|
-
datetime: "DATETIME2",
|
|
71
|
-
instant: "DATETIME2",
|
|
72
|
-
time: "TIME",
|
|
142
|
+
// Binary data
|
|
73
143
|
base64binary: "VARBINARY(MAX)",
|
|
74
144
|
};
|
|
75
145
|
if (!fhirType) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transpiler.js","sourceRoot":"","sources":["../../src/fhirpath/transpiler.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,uCAA0D;AAC1D,sEAAmE;AACnE,wEAG6C;
|
|
1
|
+
{"version":3,"file":"transpiler.js","sourceRoot":"","sources":["../../src/fhirpath/transpiler.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/loader/tables.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAY,EAAE,KAAK,cAAc,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/loader/tables.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAY,EAAE,KAAK,cAAc,EAAE,MAAM,OAAO,CAAC;AAGjD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAWlB;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,GAAE,OAAe,GACxB,OAAO,CAAC,IAAI,CAAC,CAUf"}
|
package/dist/loader/tables.js
CHANGED
|
@@ -14,6 +14,7 @@ exports.createTable = createTable;
|
|
|
14
14
|
exports.truncateTable = truncateTable;
|
|
15
15
|
exports.ensureTable = ensureTable;
|
|
16
16
|
const mssql_1 = __importDefault(require("mssql"));
|
|
17
|
+
const validation_js_1 = require("../validation.js");
|
|
17
18
|
/**
|
|
18
19
|
* Check if a table exists in the database.
|
|
19
20
|
*
|
|
@@ -42,6 +43,9 @@ async function tableExists(pool, schemaName, tableName) {
|
|
|
42
43
|
* @param tableName - Name of the table to create.
|
|
43
44
|
*/
|
|
44
45
|
async function createTable(pool, schemaName, tableName) {
|
|
46
|
+
// Validate identifiers to prevent SQL injection
|
|
47
|
+
(0, validation_js_1.validateSqlServerIdentifier)(schemaName, "Schema name");
|
|
48
|
+
(0, validation_js_1.validateSqlServerIdentifier)(tableName, "Table name");
|
|
45
49
|
// Create the table.
|
|
46
50
|
await pool.request().query(`
|
|
47
51
|
CREATE TABLE [${schemaName}].[${tableName}] (
|
|
@@ -64,6 +68,9 @@ async function createTable(pool, schemaName, tableName) {
|
|
|
64
68
|
* @param tableName - Name of the table to truncate.
|
|
65
69
|
*/
|
|
66
70
|
async function truncateTable(pool, schemaName, tableName) {
|
|
71
|
+
// Validate identifiers to prevent SQL injection
|
|
72
|
+
(0, validation_js_1.validateSqlServerIdentifier)(schemaName, "Schema name");
|
|
73
|
+
(0, validation_js_1.validateSqlServerIdentifier)(tableName, "Table name");
|
|
67
74
|
await pool.request().query(`TRUNCATE TABLE [${schemaName}].[${tableName}]`);
|
|
68
75
|
}
|
|
69
76
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../../src/loader/tables.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;
|
|
1
|
+
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../../src/loader/tables.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;AAaH,kCAeC;AAUD,kCAuBC;AASD,sCAUC;AAUD,kCAeC;AAvGD,kDAAiD;AACjD,oDAA+D;AAE/D;;;;;;;GAOG;AACI,KAAK,UAAU,WAAW,CAC/B,IAAoB,EACpB,UAAkB,EAClB,SAAiB;IAEjB,MAAM,MAAM,GAAG,MAAM,IAAI;SACtB,OAAO,EAAE;SACT,KAAK,CAAC,YAAY,EAAE,eAAG,CAAC,QAAQ,EAAE,UAAU,CAAC;SAC7C,KAAK,CAAC,WAAW,EAAE,eAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,KAAK,CAAC;;;;KAIlD,CAAC,CAAC;IAEL,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,WAAW,CAC/B,IAAoB,EACpB,UAAkB,EAClB,SAAiB;IAEjB,gDAAgD;IAChD,IAAA,2CAA2B,EAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IACvD,IAAA,2CAA2B,EAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAErD,oBAAoB;IACpB,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC;oBACT,UAAU,MAAM,SAAS;;;;;GAK1C,CAAC,CAAC;IAEH,6EAA6E;IAC7E,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC;uBACN,SAAS;UACtB,UAAU,MAAM,SAAS;GAChC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,aAAa,CACjC,IAAoB,EACpB,UAAkB,EAClB,SAAiB;IAEjB,gDAAgD;IAChD,IAAA,2CAA2B,EAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IACvD,IAAA,2CAA2B,EAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAErD,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,mBAAmB,UAAU,MAAM,SAAS,GAAG,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,WAAW,CAC/B,IAAoB,EACpB,UAAkB,EAClB,SAAiB,EACjB,WAAoB,KAAK;IAEzB,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;IAE9D,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;IACjD,CAAC;AACH,CAAC"}
|
package/dist/parser.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export declare class ViewDefinitionParser {
|
|
|
15
15
|
* Validate and narrow a ViewDefinition structure using type predicate.
|
|
16
16
|
*/
|
|
17
17
|
private static isValidViewDefinition;
|
|
18
|
+
/**
|
|
19
|
+
* Validate constant names match SQL on FHIR specification.
|
|
20
|
+
*/
|
|
21
|
+
private static validateConstants;
|
|
18
22
|
/**
|
|
19
23
|
* Validate select element using type predicate.
|
|
20
24
|
*/
|
|
@@ -43,6 +47,14 @@ export declare class ViewDefinitionParser {
|
|
|
43
47
|
* Validate column using type predicate.
|
|
44
48
|
*/
|
|
45
49
|
private static isValidColumn;
|
|
50
|
+
/**
|
|
51
|
+
* Validate column tag structure.
|
|
52
|
+
*/
|
|
53
|
+
private static validateColumnTags;
|
|
54
|
+
/**
|
|
55
|
+
* Validate a single tag object.
|
|
56
|
+
*/
|
|
57
|
+
private static validateSingleTag;
|
|
46
58
|
/**
|
|
47
59
|
* Validate collection property constraints.
|
|
48
60
|
*/
|
package/dist/parser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,SAAS,EAIT,cAAc,EAGf,MAAM,YAAY,CAAC;AAEpB,qBAAa,oBAAoB;IAC/B;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,cAAc;IAWjE;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUvD;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,SAAS,EAIT,cAAc,EAGf,MAAM,YAAY,CAAC;AAEpB,qBAAa,oBAAoB;IAC/B;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,cAAc;IAWjE;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUvD;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAqCpC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAqBhC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAa5B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAQtC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,yBAAyB;IAYxC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAWpC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAWpC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAY/B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IA6B5B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAcjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,iBAAiB;IA0BhC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,6BAA6B;IAiC5C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IA0CtC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,wBAAwB;CA+BxC"}
|
package/dist/parser.js
CHANGED
|
@@ -30,12 +30,12 @@ class ViewDefinitionParser {
|
|
|
30
30
|
*/
|
|
31
31
|
static isValidViewDefinition(data) {
|
|
32
32
|
if (!data.resource || typeof data.resource !== "string") {
|
|
33
|
-
throw new
|
|
33
|
+
throw new TypeError("ViewDefinition must specify a resource type.");
|
|
34
34
|
}
|
|
35
35
|
if (!data.select ||
|
|
36
36
|
!Array.isArray(data.select) ||
|
|
37
37
|
data.select.length === 0) {
|
|
38
|
-
throw new
|
|
38
|
+
throw new TypeError("ViewDefinition must have at least one select element.");
|
|
39
39
|
}
|
|
40
40
|
// Status is optional for test cases, but recommended for production use
|
|
41
41
|
// The SQL-on-FHIR spec requires status, but test cases may omit it
|
|
@@ -47,8 +47,30 @@ class ViewDefinitionParser {
|
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
// Validate constants if present
|
|
51
|
+
if (data.constant) {
|
|
52
|
+
this.validateConstants(data.constant);
|
|
53
|
+
}
|
|
50
54
|
return true;
|
|
51
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate constant names match SQL on FHIR specification.
|
|
58
|
+
*/
|
|
59
|
+
static validateConstants(constants) {
|
|
60
|
+
for (const constant of constants) {
|
|
61
|
+
if (!constant ||
|
|
62
|
+
typeof constant !== "object" ||
|
|
63
|
+
!("name" in constant) ||
|
|
64
|
+
typeof constant.name !== "string") {
|
|
65
|
+
throw new TypeError("Constant must have a valid name.");
|
|
66
|
+
}
|
|
67
|
+
// Validate constant name matches SQL on FHIR specification pattern
|
|
68
|
+
// Pattern: must start with a letter, followed by letters, digits, or underscores
|
|
69
|
+
if (!/^[A-Za-z]\w*$/.test(constant.name)) {
|
|
70
|
+
throw new Error(`Constant name '${constant.name}' does not match SQL on FHIR specification. Must start with a letter, followed by letters, digits, or underscores.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
52
74
|
/**
|
|
53
75
|
* Validate select element using type predicate.
|
|
54
76
|
*/
|
|
@@ -72,10 +94,10 @@ class ViewDefinitionParser {
|
|
|
72
94
|
*/
|
|
73
95
|
static validateSelectExpressions(select) {
|
|
74
96
|
if (select.forEach && typeof select.forEach !== "string") {
|
|
75
|
-
throw new
|
|
97
|
+
throw new TypeError("forEach must be a string FHIRPath expression.");
|
|
76
98
|
}
|
|
77
99
|
if (select.forEachOrNull && typeof select.forEachOrNull !== "string") {
|
|
78
|
-
throw new
|
|
100
|
+
throw new TypeError("forEachOrNull must be a string FHIRPath expression.");
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
103
|
/**
|
|
@@ -123,19 +145,54 @@ class ViewDefinitionParser {
|
|
|
123
145
|
*/
|
|
124
146
|
static isValidColumn(column, selectContext) {
|
|
125
147
|
if (!column.name || typeof column.name !== "string") {
|
|
126
|
-
throw new
|
|
148
|
+
throw new TypeError("Column must have a valid name.");
|
|
127
149
|
}
|
|
128
150
|
if (!column.path || typeof column.path !== "string") {
|
|
129
|
-
throw new
|
|
151
|
+
throw new TypeError("Column must have a valid FHIRPath expression.");
|
|
130
152
|
}
|
|
131
|
-
// Validate column name
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
// Validate column name matches SQL on FHIR specification pattern
|
|
154
|
+
// Pattern: must start with a letter, followed by letters, digits, or underscores
|
|
155
|
+
if (!/^[A-Za-z]\w*$/.test(column.name)) {
|
|
156
|
+
throw new Error(`Column name '${column.name}' does not match SQL on FHIR specification. Must start with a letter, followed by letters, digits, or underscores.`);
|
|
134
157
|
}
|
|
135
158
|
// Validate collection constraints
|
|
136
159
|
this.validateCollectionConstraints(column, selectContext);
|
|
160
|
+
// Validate tag structure if present
|
|
161
|
+
this.validateColumnTags(column);
|
|
137
162
|
return true;
|
|
138
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Validate column tag structure.
|
|
166
|
+
*/
|
|
167
|
+
static validateColumnTags(column) {
|
|
168
|
+
if (column.tag === undefined) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(column.tag)) {
|
|
172
|
+
throw new TypeError(`Column '${column.name}' tag must be an array.`);
|
|
173
|
+
}
|
|
174
|
+
for (const tag of column.tag) {
|
|
175
|
+
this.validateSingleTag(column.name, tag);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Validate a single tag object.
|
|
180
|
+
*/
|
|
181
|
+
static validateSingleTag(columnName, tag) {
|
|
182
|
+
if (typeof tag !== "object" || tag === null) {
|
|
183
|
+
throw new TypeError(`Column '${columnName}' tag entry must be an object.`);
|
|
184
|
+
}
|
|
185
|
+
if (!("name" in tag) ||
|
|
186
|
+
typeof tag.name !== "string" ||
|
|
187
|
+
tag.name.trim().length === 0) {
|
|
188
|
+
throw new TypeError(`Column '${columnName}' tag must have a non-empty 'name' string.`);
|
|
189
|
+
}
|
|
190
|
+
if (!("value" in tag) ||
|
|
191
|
+
typeof tag.value !== "string" ||
|
|
192
|
+
tag.value.trim().length === 0) {
|
|
193
|
+
throw new TypeError(`Column '${columnName}' tag must have a non-empty 'value' string.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
139
196
|
/**
|
|
140
197
|
* Validate collection property constraints.
|
|
141
198
|
*/
|