Skip to content

Commit

Permalink
Initial typescript setup (#260)
Browse files Browse the repository at this point in the history
This commit does the initial conversion of the library to TypeScript, without too many strict types and no runtime changes. This will significantly lower the burden of TypeScript developers wishing to use cql-execution, as they no longer have to define custom .d.ts files to make the TypeScript compiler happy. All types are now exported from cql-execution itself when publishing.

Basically all of these changes will not be seen by any non-TypeScript projects consuming the library, as most of the changes are development-focused, with a few new dependencies, and using tsc over babel

This commit was squashed from the following commits (see PR #260 for details):

* WIP: rename files for later additions

* Initial working ts commit

* Set up TS infrastructure
* Updated all files to proper TS
* Add low-hanging fruit types
* Used any for all complex types

* Fix luxon types to v1.x; add self to contributors

* Update examples and fix optional param

* remove babel config

* Update cql4Browsers

* Yarn audit fix

* Switch parseInt calls to Number.isInteger

* Yarn audit fix

* yarn --> npm

* Update grep of validate_cql4browsers to include file extension

* Update CI to npm over yarn

* Type and infra improvements

* Fix args that should have been optional
* Add stricter typing of execution parameters
* Switch to prepare over prepublish
* Export types directory from cql.ts

* Add default exports

* Fix overloaded import since Length class moved

* Simplify usage of accessing built in Math functions

* Apply suggestions from code review

* Fix easy type updates
* Properly mark optional params

Co-authored-by: Chris Moesel <cmoesel@users.noreply.github.com>

* Quick updates from code review

* Update PR template to use npm
* Remove prepend flag from npm scripts
* Properly mark patient attribute as potentially undefined

* Missed an optional version

* Add stricter type to datetime functions

* Unify luxon imports in datetime

* Add interfaces specifying CodeService and Data Provider structure

* Used access type modifiers in select constructors

* Regen cql4browsers and add npm build as prereq to browserify

* Better type support

* Rename types files
* Add strict typing to executor and context args
* Update example to use typescript import with comment

* Remove redundant constructor assignments

* move to inline exports

* Update findValueSet to use consistent type when not found

* Update structure of exposed API

* Rename runtime-types -> runtime.types

* update cql4browsers

* Add comment explaining overrides

* Added TODO for future refactors of datetime

* Reworked unit conversion to use clearer type checking

* removed patients from DataProvider

* added abstract base class for Date and DateTime

* Add more proper typing

* Require _is and _typeHierarchy
* Add interfaces for TypeSpecifier structures

* Make Concept default codes to [] when null

* Make ValueSet default codes to [] when null

* Properly mark code as type string in messageListeners.ts

* Update docs

* Add information about TypeScript conversion
* Update examples to include TypeScript and proper JavaScript
* Update CI bade to GH Actions over travis
* Update command to generate cql4browsers

* A fitting end to this work: a typo
  • Loading branch information
Matthew Gramigna committed Feb 21, 2022
1 parent b0f5307 commit 4e1c56a
Show file tree
Hide file tree
Showing 127 changed files with 18,235 additions and 22,063 deletions.
13 changes: 11 additions & 2 deletions .eslintrc.json
@@ -1,4 +1,5 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
Expand All @@ -7,6 +8,8 @@
}
},
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"indent": ["error", 2, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"no-console": ["warn", { "allow": ["warn", "error"] }],
Expand All @@ -15,10 +18,16 @@
"quotes": ["error", "single", { "allowTemplateLiterals": true, "avoidEscape": true }],
"semi": ["error", "always"]
},
"overrides": [{
"files": ["test/**/*.{js,ts}", "examples/**/*.js", "bin/**/*.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
}],
"env": {
"es6": true,
"node": true,
"mocha": true
},
"extends": ["eslint:recommended", "prettier"]
}
"extends": ["plugin:@typescript-eslint/recommended", "prettier"]
}
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Expand Up @@ -11,9 +11,9 @@ It is strongly recommended to include a person from each of those projects as a
- [ ] Tests are included and test edge cases
- [ ] Tests have been run locally and pass
- [ ] Code coverage has not gone down and all code touched or added is covered.
- [ ] Code passes lint and prettier (hint: use `yarn run test:plus` to run tests, lint, and prettier)
- [ ] Code passes lint and prettier (hint: use `npm run test:plus` to run tests, lint, and prettier)
- [ ] All dependent libraries are appropriately updated or have a corresponding PR related to this change
- [ ] `cql4browsers.js` built with `yarn run build:browserify` if source changed.
- [ ] `cql4browsers.js` built with `npm run build:browserify` if source changed.

**Reviewer:**

Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/ci-workflow.yml
Expand Up @@ -4,14 +4,14 @@ on: [push, pull_request]

jobs:
audit:
name: Check yarn audit
name: Check npm audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- run: yarn audit
- run: npm audit
env:
CI: true
cql4browsers:
Expand All @@ -33,9 +33,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '12.x'
- run: yarn install
- run: yarn run lint
- run: yarn run prettier
- run: npm install
- run: npm run lint
- run: npm run prettier
env:
CI: true
test:
Expand All @@ -51,8 +51,8 @@ jobs:
with:
timezone: America/New_York
- run: date +"%Y-%m-%d %T"
- run: yarn install
- run: npm install
- run: ./bin/check_for_nonassertive_tests.sh
- run: npx nyc --reporter=lcov yarn test && npx codecov
- run: npx nyc --reporter=lcov npm test && npx codecov
env:
CI: true
4 changes: 4 additions & 0 deletions .mocharc.json
@@ -0,0 +1,4 @@
{
"extension": ["ts"],
"require": "ts-node/register"
}
24 changes: 14 additions & 10 deletions OVERVIEW.md
Expand Up @@ -9,6 +9,7 @@ Technologies
------------

The CQL execution framework was originally written in [CoffeeScript](http://coffeescript.org/). CoffeeScript is a scripting language that compiles down to JavaScript. In 2020, the CQL Execution framework source code was migrated from CoffeeScript to ES6 JavaScript. JavaScript execution code allows the reference implementation to be integrated into a variety of environments, including servers, other languages’ runtime environments, and standard web browsers.
In 2022, the CQL execution framework was converted to [TypeScript](https://www.typescriptlang.org/) to make use of static typing and support both TypeScript and JavaScript users of the framework.

The CQL execution framework tests and examples are configured to run using [Node.js](http://nodejs.org/), but can be easily integrated into other JavaScript runtime environments.

Expand Down Expand Up @@ -46,7 +47,7 @@ The expression 1 + 2 is represented in JSON ELM as follows:

### ELM Expression Classes

Each ELM expression has a corresponding class defined in the JavaScript CQL execution framework. These classes all extend a common `Expression` class and define, at a minimum, these components:
Each ELM expression has a corresponding class defined in the TypeScript CQL execution framework. These classes all extend a common `Expression` class and define, at a minimum, these components:

1. A `constructor` that takes a JSON ELM object as its argument
2. An `exec` function that takes a `Context` object as its argument
Expand All @@ -55,16 +56,19 @@ The `constructor` is responsible for setting class properties from the JSON ELM

The following is an example of the `Add` class that corresponds to the JSON ELM example in the previous section:

```js
```typescript
class Add extends Expression {
constructor(json) {
arg1: IntegerLiteral;
arg2: IntegerLiteral;

constructor(json: any) {
super(json);
this.arg1 = new IntegerLiteral(json.operand[0])
this.arg2 = new IntegerLiteral(json.operand[1])
this.arg1 = new IntegerLiteral(json.operand[0]);
this.arg2 = new IntegerLiteral(json.operand[1]);
}

exec(ctx) {
return this.arg1.exec(ctx) + this.arg2.exec(ctx)
exec(ctx: Context) {
return this.arg1.exec(ctx) + this.arg2.exec(ctx);
}
}
```
Expand Down Expand Up @@ -92,8 +96,8 @@ In order for the CQL execution framework to determine if a code is in a valueset
### MessageListener

The CQL specification defines a [Message](https://cql.hl7.org/09-b-cqlreference.html#message) operator that "provides a run-time mechanism for returning messages, warnings, traces, and errors to the calling environment." To support this, the CQL execution framework supports a "MessageListener" API. A MessageListener class must contain an `onMessage` function which will be called by the CQL execution framework if the `condition` passed to the `Message` operator is `true`:
```js
onMessage(source, code, severity, message) {
```typescript
onMessage(source: any, code: string, severity: string, message: string) {
// do something with the message
}
```
Expand Down Expand Up @@ -127,7 +131,7 @@ const parameters = {
new cql.DateTime(2014, 1, 1, 0, 0, 0, 0),
true,
false
);
)
};

const executor = new cql.Executor(lib, codeService, parameters, messageListener);
Expand Down
100 changes: 78 additions & 22 deletions README.md
@@ -1,9 +1,9 @@
[![Build Status](https://travis-ci.org/cqframework/cql-execution.svg?branch=master)](https://travis-ci.org/cqframework/cql-execution)
[![CI Checks](https://github.com/cqframework/cql-execution/actions/workflows/ci-workflow.yml/badge.svg)](https://github.com/cqframework/cql-execution/actions/workflows/ci-workflow.yml)
[![codecov](https://codecov.io/gh/cqframework/cql-execution/branch/master/graph/badge.svg)](https://codecov.io/gh/cqframework/cql-execution)

# CQL Execution Framework

The CQL Execution Framework provides a JavaScript library for executing CQL artifacts expressed as
The CQL Execution Framework provides a TypeScript/JavaScript library for executing CQL artifacts expressed as
JSON ELM.

For more information, see the [CQL Execution Framework Overview](OVERVIEW.md).
Expand Down Expand Up @@ -51,6 +51,9 @@ Implementors should be aware of the following limitations and gaps in `cql-execu
* Unfiltered context retrieves
* Unfiltered context references to other libraries
* External functions
* While the source code of `cql-execution` is in TypeScript, full-fledged typing of the library is not yet implemented
* Conversion from JavaScript to TypeScript was done in [this pull request](https://github.com/cqframework/cql-execution/pull/260),
with the intent on making incremental type improvements in subsequent pull requests.

The above is a partial list covering the most significant limitations. For more details, see the
[CQL_Execution_Features.xlsx](CQL_Execution_Features.xlsx) spreadsheet.
Expand All @@ -59,9 +62,8 @@ The above is a partial list covering the most significant limitations. For more

To use this project, you should perform the following steps:

1. Install [Node.js](http://nodejs.org/)
2. Install [Yarn](https://yarnpkg.com)
3. Execute the following from the root directory: `yarn install`
1. Install [Node.js](http://nodejs.org/) (Note: `npm` version `6.x.x` recommended)
2. Execute the following from the root directory: `npm install`

# To Execute Your CQL

Expand Down Expand Up @@ -103,14 +105,70 @@ define InDemographic:
AgeInYearsAt(start of MeasurementPeriod) >= 2 and AgeInYearsAt(start of MeasurementPeriod) < 18
```

## TypeScript Example

Next, we can create a TypeScript file to execute the above CQL. This file will need to contain (or
`import`) JSON patient representations for testing as well. Our example CQL uses a "Simple"
data model developed only for demonstration and testing purposes. In this model, each patient is
represented using a simple JSON object. For ease of use, let's put the file in the `customCQL`
directory:

``` typescript
import cql from '../../src/cql';
import * as measure from './age.json'; // Requires the "resolveJsonModule" compiler option to be "true"

const lib = new cql.Library(measure);
const executor = new cql.Executor(lib);
const psource = new cql.PatientSource([
{
id: '1',
recordType: 'Patient',
name: 'John Smith',
gender: 'M',
birthDate: '1980-02-17T06:15'
},
{
id: '2',
recordType: 'Patient',
name: 'Sally Smith',
gender: 'F',
birthDate: '2007-08-02T11:47'
}
]);

const result = executor.exec(psource);
console.log(JSON.stringify(result, undefined, 2));
```

In the above file, we've assumed the JSON ELM JSON file for the measure is called
`age.json` and is in the same directory as the file that requires is. We've
also assumed a couple of very simple patients. Let's call the file we just created
`exec-age.ts`.

Now we can execute the measure using [ts-node](https://www.npmjs.com/package/ts-node):

``` bash
npx ts-node -O '{ "resolveJsonModule": true }' --files ${path_to_cql-execution}/customCQL/exec-age.ts
```

If all is well, it should print the result object to standard out.

## JavaScript Example

For usage in regular JavaScript, we can refer to the compiled JavaScript in the `lib` directory.
Ensure that this JavaScript is present by running `npm run build` before continuing on to the example.
We will follow the same steps as the above TypeScript example, but our JavaScript code must use `require`
instead of `import`, and will load the `cql-execution` library from the `lib` directory. As before,
let's put the file in the `customCQL` directory:

Next, create a JavaScript file to execute the CQL above. This file will need to contain (or
`require`) JSON patient representations for testing as well. Our example CQL uses a "Simple"
data model developed only for demonstration and testing purposes. In this model, each patient is
represented using a simple JSON object. For ease of use, let's put the file in the `customCQL`
directory:

```js
const cql = require('../src/cql');
const cql = require('../lib/cql');
const measure = require('./age.json');

const lib = new cql.Library(measure);
Expand All @@ -134,9 +192,7 @@ console.log(JSON.stringify(result, undefined, 2));

```

In the above file, we've assumed the JSON ELM JSON file for the measure is called
`age.json` and is in the same directory as the file that requires is. We've
also assumed a couple of very simple patients. Let's call the file we just created
The above file has the same assumptions as the TypeScript example above. Let's call the file we just created
`exec-age.js`.

Now we can execute the measure using Node.js:
Expand All @@ -145,11 +201,11 @@ Now we can execute the measure using Node.js:
node ${path_to_cql-execution}/customCQL/exec-age.js
```

If all is well, it should print the result object to standard out.
If all is well, it should print the result object to standard out, and the output should be identical to that of the TypeScript example.

# To Run the CQL Execution Unit Tests

Execute `yarn test`.
Execute `npm test`.

# To Develop Tests

Expand All @@ -167,7 +223,7 @@ statements that follows the `# And` represents the CQL Library that will be supp
to the "And" test suite.

To convert the CQL to JavaScript containing the JSON ELM representation, execute
`yarn build:test-data`. This will use the java _cql-to-elm_ project to generate the
`npm run build:test-data`. This will use the java _cql-to-elm_ project to generate the
_test/elm/*/data.js_ file containing the following exported variable declaration
(NOTE: It's been slimmed down a bit here to make it easier to read, but nothing substantial
has been removed):
Expand Down Expand Up @@ -244,23 +300,23 @@ module.exports['And'] = {

Notice that since the CQL didn't declare a library name/version, a data model, or a context,
default values were inserted into the CQL at generation time. Now this CQL can be used in a test
defined in _test/elm/*/logical-test.js_. For example:
defined in _test/elm/*/logical-test.ts_. For example:

```js
describe('And', () => {
this.beforeEach(() => {
this.beforeEach(function () {
setup(this, data);
});

it('should execute allTrue as true', () => {
it('should execute allTrue as true', function () {
this.allTrue.exec(this.ctx).should.be.true();
});

it('should execute someTrue as false', () => {
it('should execute someTrue as false', function () {
this.someTrue.exec(this.ctx).should.be.false();
});

it('should execute allFalse as false', () => {
it('should execute allFalse as false', function () {
this.allFalse.exec(this.ctx).should.be.false();
});
});
Expand All @@ -275,16 +331,16 @@ use lowercase first letters even though the CQL expression name starts with an u

# Watching For Changes

Rather than continually having to run `yarn build:test-data` and `yarn:test` after every
Rather than continually having to run `npm run build:test-data` and `npm test` after every
modification to the test data text file, you can setup a process to _watch_ for changes and
regenerate the `data.js` files every time it detects changes in the source text file. Simply
execute `yarn watch:test-data`.
execute `npm run watch:test-data`.

# Pull Requests

If JavaScript source code is modified, `cql4browsers.js` needs to be included in the pull request,
otherwise Travis CI will fail. To generate this file, run:
If TypeScript source code is modified, `cql4browsers.js` needs to be included in the pull request,
otherwise GitHub Actions CI will fail. To generate this file, run:

```
yarn build:all
npm run build:browserify
```
10 changes: 0 additions & 10 deletions babel.config.json

This file was deleted.

6 changes: 3 additions & 3 deletions bin/validate_cql4browsers.sh
@@ -1,7 +1,7 @@
#!/bin/bash

mkdir -p tmp/dist/
git status --porcelain | grep cql4browsers
git status --porcelain | grep cql4browsers.js
if [ $? == 0 ]; then
echo "cql4browsers.js has uncommitted changes. Reset or commit them before continuing."
exit 1
Expand All @@ -13,7 +13,7 @@ if [ $? != 0 ]; then
echo "cql4browsers.js not found. Run this script from the base repository directory."
exit 1
fi
yarn install
npm install

# comm -3 only returns lines that differ between the two files. If none are different, diff will be empty
diff=`diff ./examples/browser/cql4browsers.js.original ./examples/browser/cql4browsers.js`
Expand All @@ -22,7 +22,7 @@ mv ./examples/browser/cql4browsers.js.original ./examples/browser/cql4browsers.j

# Exit with a non-zero code if the diff isn't empty
if [ "$diff" != "" ]; then
echo "cql4browsers.js is out of date. Please run 'yarn install' locally and commit/push the result"
echo "cql4browsers.js is out of date. Please run 'npm install' locally and commit/push the result"
exit 1
fi

Expand Down
9 changes: 4 additions & 5 deletions examples/browser/README.md
Expand Up @@ -5,8 +5,7 @@ does not support other data models or execution on patients.
The browserified code is checked into source control, but if you need to update it,
you can follow these steps:

1. Install [Node.js](http://nodejs.org/)
2. Install [Yarn](https://yarnpkg.com)
3. Execute the following from the _cql-execution_ directory:
1. `yarn install`
2. `cake build:all`
1. Install [Node.js](http://nodejs.org/) (Note: `npm` version `6.x.x` recommended)
2. Execute the following from the _cql-execution_ directory:
1. `npm install`
2. `npm run build:all`

0 comments on commit 4e1c56a

Please sign in to comment.