Skip to content

Commit b4cdc5d

Browse files
committed
Align $sqlquery-run server with SQLQueryRun OperationDefinition
Update the stored OperationDefinition and reference handler to match the spec defined in input/fsh/operations.fsh: - Make _format required (1..1); return 400 when absent instead of defaulting to JSON. - Constrain queryResource to the SQLQuery Library profile via the operationdefinition-allowed-type extension. - Declare the source parameter (0..1 string) and reject it with 422 not-supported, since this reference implementation has no external data source concept. - Rename the output parameter from result (0..* Binary) to return (1..1) and declare Binary and Parameters as allowed types, matching the two body shapes the handler already produces. Add tests covering the new validation rules and the updated shape of the published OperationDefinition.
1 parent 91c1130 commit b4cdc5d

3 files changed

Lines changed: 139 additions & 24 deletions

File tree

sof-js/metadata/OperationDefinition/$sqlquery-run.json

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,63 +7,87 @@
77
"code": "sqlquery-run",
88
"status": "active",
99
"kind": "operation",
10-
"description": "Execute a SQL query defined within a Library resource against materialised ViewDefinitions.",
10+
"description": "Execute a SQLQuery Library against ViewDefinition tables.",
1111
"system": true,
1212
"type": true,
1313
"instance": true,
1414
"resource": ["Library"],
1515
"parameter": [
1616
{
17-
"name": "queryResource",
17+
"name": "_format",
18+
"use": "in",
19+
"min": 1,
20+
"max": "1",
21+
"type": "code",
22+
"documentation": "Output format for the result (json, ndjson, csv, parquet, fhir). Use fhir to return results as a FHIR Parameters resource.",
23+
"binding": {
24+
"strength": "extensible",
25+
"valueSet": "http://sql-on-fhir.org/ValueSet/output-format"
26+
}
27+
},
28+
{
29+
"name": "header",
1830
"use": "in",
1931
"min": 0,
2032
"max": "1",
21-
"type": "Resource",
22-
"documentation": "The Library resource containing the SQL query and relatedArtifact dependencies inline."
33+
"type": "boolean",
34+
"documentation": "Include CSV headers (default true). Applies only when csv output is requested."
2335
},
2436
{
2537
"name": "queryReference",
2638
"use": "in",
2739
"min": 0,
2840
"max": "1",
2941
"type": "Reference",
30-
"documentation": "A reference to a stored Library resource that contains the SQL query."
42+
"documentation": "Reference to a SQLQuery Library stored on the server."
3143
},
3244
{
33-
"name": "parameters",
45+
"name": "queryResource",
3446
"use": "in",
3547
"min": 0,
3648
"max": "1",
37-
"type": "Parameters",
38-
"documentation": "Named parameter bindings to pass to the SQL query as a FHIR Parameters resource."
49+
"type": "Resource",
50+
"extension": [
51+
{
52+
"url": "http://hl7.org/fhir/StructureDefinition/operationdefinition-allowed-type",
53+
"valueUri": "https://sql-on-fhir.org/ig/StructureDefinition/SQLQuery"
54+
}
55+
],
56+
"documentation": "Inline SQLQuery Library resource to execute."
3957
},
4058
{
41-
"name": "_format",
59+
"name": "parameters",
4260
"use": "in",
4361
"min": 0,
4462
"max": "1",
45-
"type": "code",
46-
"documentation": "Output format - json, ndjson, csv, or fhir.",
47-
"binding": {
48-
"strength": "extensible",
49-
"valueSet": "http://sql-on-fhir.org/ValueSet/output-format"
50-
}
63+
"type": "Parameters",
64+
"documentation": "Input parameters for the query. Parameters are bound by name to parameters declared in the SQLQuery Library (Library.parameter.name). Parameter types are mapped using the appropriate value[x] type matching the declared parameter type."
5165
},
5266
{
53-
"name": "header",
67+
"name": "source",
5468
"use": "in",
5569
"min": 0,
5670
"max": "1",
57-
"type": "boolean",
58-
"documentation": "Whether to include the header row in CSV output. Defaults to true."
71+
"type": "string",
72+
"documentation": "External data source containing the ViewDefinition tables."
5973
},
6074
{
61-
"name": "result",
75+
"name": "return",
6276
"use": "out",
63-
"min": 0,
64-
"max": "*",
65-
"type": "Binary",
66-
"documentation": "The query result in the requested format."
77+
"min": 1,
78+
"max": "1",
79+
"type": "Resource",
80+
"extension": [
81+
{
82+
"url": "http://hl7.org/fhir/StructureDefinition/operationdefinition-allowed-type",
83+
"valueUri": "Binary"
84+
},
85+
{
86+
"url": "http://hl7.org/fhir/StructureDefinition/operationdefinition-allowed-type",
87+
"valueUri": "Parameters"
88+
}
89+
],
90+
"documentation": "Query results. Returns Binary for flat formats (csv, json, ndjson, parquet) or Parameters for _format=fhir."
6791
}
6892
]
6993
}

sof-js/src/server/sqlquery-run.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,14 @@ async function handleSqlQueryRun(req, res, options = {}) {
620620
const createdTables = []
621621
try {
622622
const params = req.body
623-
const format = getParameterValue(params, '_format', 'Code') || 'json'
623+
const format = getParameterValue(params, '_format', 'Code')
624+
if (!format) {
625+
throw badRequestError('_format is required')
626+
}
627+
const source = getParameterValue(params, 'source', 'String')
628+
if (source) {
629+
throw notSupportedError('The source parameter is not supported by this server')
630+
}
624631
const headerParam = getParameterValue(params, 'header', 'Boolean')
625632
const includeHeader = headerParam !== false && headerParam !== 'false'
626633

@@ -751,6 +758,10 @@ function buildParametersFromBody(body) {
751758
parameter.push({ name: 'header', valueBoolean: false })
752759
}
753760

761+
if (body.source) {
762+
parameter.push({ name: 'source', valueString: body.source })
763+
}
764+
754765
return { resourceType: 'Parameters', parameter }
755766
}
756767

sof-js/tests/server/sqlquery-run.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,86 @@ describe('$sqlquery-run operation', () => {
451451
expect(body.resourceType).toBe('OperationOutcome')
452452
})
453453

454+
test('missing _format returns 400', async () => {
455+
// The spec declares _format as 1..1, so a request without it must be
456+
// rejected rather than silently defaulting to JSON.
457+
const response = await fetch('http://localhost:3004/Library/$sqlquery-run', {
458+
method: 'POST',
459+
headers: { 'Content-Type': 'application/fhir+json' },
460+
body: JSON.stringify({
461+
resourceType: 'Parameters',
462+
parameter: [{ name: 'queryReference', valueReference: { reference: 'Library/patient-bp-query' } }],
463+
}),
464+
})
465+
466+
expect(response.status).toBe(400)
467+
const body = await response.json()
468+
expect(body.resourceType).toBe('OperationOutcome')
469+
expect(body.issue[0].code).toBe('invalid')
470+
})
471+
472+
test('source parameter returns 422 not-supported', async () => {
473+
// This reference implementation has no external data source concept, so
474+
// supplying `source` should be rejected explicitly rather than ignored.
475+
const response = await fetch('http://localhost:3004/Library/$sqlquery-run', {
476+
method: 'POST',
477+
headers: { 'Content-Type': 'application/fhir+json' },
478+
body: JSON.stringify({
479+
resourceType: 'Parameters',
480+
parameter: [
481+
{ name: '_format', valueCode: 'json' },
482+
{ name: 'source', valueString: 'http://example.com/data' },
483+
{ name: 'queryReference', valueReference: { reference: 'Library/patient-bp-query' } },
484+
],
485+
}),
486+
})
487+
488+
expect(response.status).toBe(422)
489+
const body = await response.json()
490+
expect(body.resourceType).toBe('OperationOutcome')
491+
expect(body.issue[0].code).toBe('not-supported')
492+
})
493+
494+
test('OperationDefinition exposes spec-aligned parameters', async () => {
495+
// The stored OperationDefinition must reflect the FSH source: _format is
496+
// required, queryResource carries an allowed-type extension for the
497+
// SQLQuery profile, source is declared, and the output is `return` with
498+
// Binary or Parameters allowed types.
499+
const response = await fetch('http://localhost:3004/OperationDefinition/$sqlquery-run')
500+
expect(response.status).toBe(200)
501+
502+
const body = await response.json()
503+
const byName = Object.fromEntries(body.parameter.map((p) => [p.name, p]))
504+
505+
expect(byName._format).toBeDefined()
506+
expect(byName._format.min).toBe(1)
507+
expect(byName._format.max).toBe('1')
508+
509+
expect(byName.queryResource).toBeDefined()
510+
const allowedTypeUrl = 'http://hl7.org/fhir/StructureDefinition/operationdefinition-allowed-type'
511+
const queryResourceAllowed = (byName.queryResource.extension || [])
512+
.filter((e) => e.url === allowedTypeUrl)
513+
.map((e) => e.valueUri)
514+
expect(queryResourceAllowed).toContain('https://sql-on-fhir.org/ig/StructureDefinition/SQLQuery')
515+
516+
expect(byName.source).toBeDefined()
517+
expect(byName.source.type).toBe('string')
518+
expect(byName.source.min).toBe(0)
519+
expect(byName.source.max).toBe('1')
520+
521+
expect(byName.return).toBeDefined()
522+
expect(byName.return.use).toBe('out')
523+
expect(byName.return.min).toBe(1)
524+
expect(byName.return.max).toBe('1')
525+
const returnAllowed = (byName.return.extension || [])
526+
.filter((e) => e.url === allowedTypeUrl)
527+
.map((e) => e.valueUri)
528+
expect(returnAllowed).toContain('Binary')
529+
expect(returnAllowed).toContain('Parameters')
530+
531+
expect(byName.result).toBeUndefined()
532+
})
533+
454534
test('invalid SQL returns 422', async () => {
455535
const response = await fetch('http://localhost:3004/$sqlquery-run', {
456536
method: 'POST',

0 commit comments

Comments
 (0)