Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions cmd/cqlplay/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"time"

"flag"

log "github.com/golang/glog"
"github.com/google/cql"
"github.com/google/cql/retriever/local"
Expand Down Expand Up @@ -107,7 +108,13 @@ func handleEvalCQL(w http.ResponseWriter, req *http.Request) {
return
}

elm, err := cql.Parse(req.Context(), []string{evalCQLReq.CQL, fhirHelpers}, cql.ParseConfig{DataModels: [][]byte{fhirDM}})
// Combine main CQL with additional libraries and FHIRHelpers
cqlInputs := append([]string{evalCQLReq.CQL}, evalCQLReq.Libraries...)
// TODO: now that users can supply their own libraries, we may wish to only add FHIRHelpers if
// it's not already included. Though, our parser really only works with FHIR Helpers 4.0.1.
cqlInputs = append(cqlInputs, fhirHelpers)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may not want to always add fhirHelpers, and/or try to detect if the user has added a version of fhirHelpers already


elm, err := cql.Parse(req.Context(), cqlInputs, cql.ParseConfig{DataModels: [][]byte{fhirDM}})
if err != nil {
sendError(w, fmt.Errorf("failed to parse: %w", err), http.StatusInternalServerError)
return
Expand Down Expand Up @@ -147,8 +154,9 @@ func sendError(w http.ResponseWriter, err error, code int) {
}

type evalCQLRequest struct {
CQL string `json:"cql"`
Data string `json:"data"`
CQL string `json:"cql"`
Data string `json:"data"`
Libraries []string `json:"libraries"`
}

func getTerminologyProvider() (*terminology.LocalFHIRProvider, error) {
Expand Down
54 changes: 51 additions & 3 deletions cmd/cqlplay/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
Expand All @@ -32,6 +31,7 @@ func TestServerHandler(t *testing.T) {
name string
cql string
data string
libraries []string
bodyJSON string
wantOutput string
}{
Expand Down Expand Up @@ -86,6 +86,38 @@ func TestServerHandler(t *testing.T) {
}
]`,
},
{
name: "CQL with multiple libraries",
cql: dedent.Dedent(`
library Main version '1.0.0'
include HelperLib version '1.0.0' called Helper
define result: Helper.ConstantValue + 10`),
libraries: []string{dedent.Dedent(`
library HelperLib version '1.0.0'
define public ConstantValue: 42`)},
wantOutput: `[
{
"libName": "HelperLib",
"libVersion": "1.0.0",
"expressionDefinitions": {
"ConstantValue": {
"@type": "System.Integer",
"value": 42
}
}
},
{
"libName": "Main",
"libVersion": "1.0.0",
"expressionDefinitions": {
"result": {
"@type": "System.Integer",
"value": 52
}
}
}
]`,
},
}

for _, tc := range tests {
Expand All @@ -97,7 +129,23 @@ func TestServerHandler(t *testing.T) {
server := httptest.NewServer(h)
defer server.Close()

bodyJSON := fmt.Sprintf(`{"cql": %q, "data": %q}`, tc.cql, tc.data)
// Build the request body using anonymous struct
reqBody := struct {
CQL string `json:"cql"`
Data string `json:"data"`
Libraries []string `json:"libraries"`
}{
CQL: tc.cql,
Data: tc.data,
Libraries: tc.libraries,
}

bodyBytes, err := json.Marshal(reqBody)
if err != nil {
t.Fatalf("json.Marshal(%v) returned an unexpected error: %v", reqBody, err)
}
bodyJSON := string(bodyBytes)

resp, err := http.Post(server.URL+"/eval_cql", "application/json", strings.NewReader(bodyJSON))
if err != nil {
t.Fatalf("http.Post(%v) with body %v returned an unexpected error: %v", server.URL, bodyJSON, err)
Expand All @@ -109,7 +157,7 @@ func TestServerHandler(t *testing.T) {
}
got := string(body)
if !cmp.Equal(normalizeJSON(t, got), normalizeJSON(t, tc.wantOutput)) {
t.Errorf("POST to /eval_cql to CQL server with body %v returned %v, want %v", tc.bodyJSON, got, tc.wantOutput)
t.Errorf("POST to /eval_cql to CQL server with body %s returned %v, want %v", bodyJSON, got, tc.wantOutput)
}
})
}
Expand Down
190 changes: 186 additions & 4 deletions cmd/cqlplay/static/cqlPlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ context Patient`;

let data = syntheticPatient;

// Libraries array to store additional CQL libraries
let libraries = [];

let results = '';

// Helper functions:
Expand Down Expand Up @@ -81,6 +84,16 @@ function bindButtonActions() {
.addEventListener('click', function(e) {
showDataTab();
});
document.getElementById('librariesTabButton')
.addEventListener('click', function(e) {
showLibrariesTab();
});
document.getElementById('addLibrary')
.addEventListener('click', function(e) {
addLibrary();
});
document.getElementById('uploadLibrary')
.addEventListener('change', handleFileUpload);
}

/**
Expand All @@ -98,7 +111,15 @@ function runCQL() {
};
xhr.open('POST', '/eval_cql', true);
xhr.setRequestHeader('Content-Type', 'text/json');
xhr.send(JSON.stringify({'cql': code, 'data': data}));

// Collect library content to send with request
const libraryContents = libraries.map(lib => lib.content);

xhr.send(JSON.stringify({
'cql': code,
'data': data,
'libraries': libraryContents
}));
}

/**
Expand All @@ -107,9 +128,11 @@ function runCQL() {
function showDataTab() {
document.getElementById('cqlEntry').style.display = 'none';
document.getElementById('dataEntry').style.display = 'block';
document.getElementById('librariesEntry').style.display = 'none';

document.getElementById('dataTabButton').className += 'active';
document.getElementById('dataTabButton').className = 'active';
document.getElementById('cqlTabButton').className = '';
document.getElementById('librariesTabButton').className = '';
}

/**
Expand All @@ -118,9 +141,134 @@ function showDataTab() {
function showCQLTab() {
document.getElementById('cqlEntry').style.display = 'block';
document.getElementById('dataEntry').style.display = 'none';
document.getElementById('librariesEntry').style.display = 'none';

document.getElementById('cqlTabButton').className += 'active';
document.getElementById('cqlTabButton').className = 'active';
document.getElementById('dataTabButton').className = '';
document.getElementById('librariesTabButton').className = '';
}

/**
* showLibrariesTab shows the Libraries tab and hides other tabs.
*/
function showLibrariesTab() {
document.getElementById('cqlEntry').style.display = 'none';
document.getElementById('dataEntry').style.display = 'none';
document.getElementById('librariesEntry').style.display = 'block';

document.getElementById('librariesTabButton').className = 'active';
document.getElementById('cqlTabButton').className = '';
document.getElementById('dataTabButton').className = '';
}

/**
* addLibrary adds a new library to the libraries list and updates the UI.
*/
function addLibrary(name = '', content = '') {
const libraryId = Date.now(); // Unique ID for the library

// Add to libraries array
libraries.push({
id: libraryId,
name: name,
content: content
});

// Update the UI
renderLibraries();

// Save to localStorage
saveLibrariesToLocalStorage();
}

/**
* removeLibrary removes a library from the libraries list and updates the UI.
*/
function removeLibrary(libraryId) {
libraries = libraries.filter(lib => lib.id !== libraryId);
renderLibraries();
saveLibrariesToLocalStorage();
}

/**
* renderLibraries updates the libraries UI with the current libraries.
*/
function renderLibraries() {
const container = document.getElementById('librariesContainer');
container.innerHTML = '';

libraries.forEach(library => {
const libraryContainer = document.createElement('div');
libraryContainer.className = 'libraryContainer';

const headerDiv = document.createElement('div');
headerDiv.className = 'libraryHeader';

// Create label for the library name input
const nameLabel = document.createElement('div');
nameLabel.className = 'libraryNameLabel';

const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.value = library.name;
nameInput.placeholder = 'Library Name';
nameInput.className = 'libraryNameInput';
nameInput.oninput = function(e) {
library.name = e.target.value;
saveLibrariesToLocalStorage();
};

nameLabel.appendChild(document.createTextNode('Library Name:'));
nameLabel.appendChild(nameInput);

const removeButton = document.createElement('button');
removeButton.className = 'removeLibraryButton';
removeButton.textContent = 'Remove';
removeButton.onclick = function() {
removeLibrary(library.id);
};

headerDiv.appendChild(nameLabel);
headerDiv.appendChild(removeButton);

const editorDiv = document.createElement('div');
editorDiv.className = 'codeInputContainer';

const codeInput = document.createElement('code-input');
codeInput.setAttribute('lang', 'cql');
codeInput.setAttribute('placeholder', 'Type CQL Library Here');
codeInput.className = 'codeInput';
codeInput.value = library.content;
codeInput.onchange = function(e) {
library.content = e.target.value;
saveLibrariesToLocalStorage();
};

editorDiv.appendChild(codeInput);

libraryContainer.appendChild(headerDiv);
libraryContainer.appendChild(editorDiv);

container.appendChild(libraryContainer);
});
}

/**
* saveLibrariesToLocalStorage saves the libraries to localStorage.
*/
function saveLibrariesToLocalStorage() {
localStorage.setItem('cqlLibraries', JSON.stringify(libraries));
}

/**
* loadLibrariesFromLocalStorage loads the libraries from localStorage.
*/
function loadLibrariesFromLocalStorage() {
const storedLibraries = localStorage.getItem('cqlLibraries');
if (storedLibraries) {
libraries = JSON.parse(storedLibraries);
renderLibraries();
}
}

/**
Expand Down Expand Up @@ -150,6 +298,33 @@ function setupPrism() {
'syntax-highlighted', codeInput.templates.prism(Prism, []));
}

/**
* handleFileUpload processes uploaded CQL library files
*/
function handleFileUpload(event) {
const fileList = event.target.files;
if (fileList.length === 0) {
return; // No file selected
}

const file = fileList[0];
const reader = new FileReader();

reader.onload = function(e) {
const content = e.target.result;
// Extract library name from filename (remove .cql extension)
const fileName = file.name.replace(/\.cql$/i, '');

// Add the library with the file content
addLibrary(fileName, content);
};

reader.readAsText(file);

// Reset the file input so the same file can be selected again
event.target.value = '';
}

/**
* main is the entrypoint for the script.
*/
Expand All @@ -159,8 +334,15 @@ function main() {
bindInputsOnChange();
bindButtonActions();

// Initially hide dataEntry tab:
// Load libraries from localStorage
loadLibrariesFromLocalStorage();

// Initially hide non-CQL tabs
document.getElementById('dataEntry').style.display = 'none';
document.getElementById('librariesEntry').style.display = 'none';

// Set CQL tab as active
document.getElementById('cqlTabButton').className = 'active';
}

main(); // All code actually executed when the script is loaded by the HTML.
Loading