Skip to content

Commit

Permalink
Merge 6cbf3b4 into 9d26b82
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert-W committed Aug 24, 2018
2 parents 9d26b82 + 6cbf3b4 commit 7cb299a
Show file tree
Hide file tree
Showing 24 changed files with 921 additions and 259 deletions.
4 changes: 3 additions & 1 deletion .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
### Do you want to submit a request for change, improve documentation, or ask a question?

### Are you reporting a bug?
#### If asking a question, please attach the 'Question' label

#### If reporting a bug, please answer the following at minimum and add any additional information you think may be helpful

**What is the current behavior?**

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ node_modules
package-lock.json

# Generated files
coverage
npm-debug.log
.DS_Store
/coverage
.idea/
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_js:
- "lts/*"
- "8"
script: yarn test --coverage
after_success: node ./node_modules/.bin/coveralls < ./coverage/lcov.info
262 changes: 262 additions & 0 deletions FAQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
Frequently Asked Questions
==========================

## Commands
There are four commands defined in the package.json you can run via yarn or npm. They are as follows:

#### `start`
This starts the server with no special flags. This will eventually become the default way to start the server in production mode.

#### `nodemon`
This starts the server in a 'development' mode. It will watch all the files in the `src` directory and restart on any changes. You can see and tweak the nodemon configurations in `src/scripts/nodemon`.

#### `test`
This runs `node src/scripts/test --env=jsdom`. We are using [jest](https://jestjs.io/) as our test framework. By default, it runs in watch mode unless you call it with the coverage flag (`yarn test --coverage`). Any valid jest argument can be passed through when invoking with yarn. You can configure the test script if you like at `src/scripts/test.js`.

#### `lint`
This runs eslint once on all files matching `src/**/*.js`.

## Server configuration
We expose a couple of different options for configuring the server. You can change what features are enabled by altering how the server is built in `src/index.js`, or you can modify the default server configurations in `src/config.js` which are outlined below.

### In `src/config.js`

#### `VERSION`
You can configure which version you want to support here. The values should never be too generic, like DSTU2 for example, rather they should be very specific, like 1_0_2. We used underscores instead of dots since we use this value for url's, directories, and JSON properties. Default supports DSTU2 (1_0_2) and STU3 (3_0_1).

#### `SERVER_CONFIG`
- **defaultVersion** - This is the default version of a resource to fall back on. This is mostly used for the case of an internal server error or a 404 error. Since we cannot reliably determine which version the request was intended for (without inspecting headers), we use this to know which version of an OperationOutcome to respond with.
- **port** - Port the server will listen on for incoming connections. Default is 3000.
- **logging** - Place to put logging configurations. Currently only level is allowed, but we intend to add more in the future. You can also customize the logger in `src/lib/logger` to add support for whatever you need.
- **auth** - Various configurations for authentication.
- **name** - Name of the strategy to use for passport
- **clientId** - Client Id needed for an introspection query. This will be populated by a CLIENT_ID environment variable.
- **clientSecret** - Client Secret needed for an introspection query. This will be populated by a CLIENT_SECRET environment variable.
- **introspectionUrl** - Introspection url
- **strategy** - Path to your strategy
- **passportOptions** - Options object passed directly into passport.

#### `DATE_CONFIG`
Be careful when modifying any of these, this is for formatting dates coming in and out of the server via GraphQL. Whatever format you change this to needs to be compatible with the structure definitions hosted on FHIR's website.

- **dateFormat** - Used in custom scalars for formatting date strings.
- **timeFormat** - Used in custom scalars for formatting time strings.
- **dateTimeFormat** - Used in custom scalars for formatting date time strings.

#### `RESOURCE_CONFIG`
This should not be modified, it is used by the server to discover profiles and their capabilities.

## Authentication
Authentication is implemented based on the [SMART App Authorization Guide](http://docs.smarthealthit.org/authorization/) and enabled by default. For your convenience, the graphiql endpoint does not use auth and is only enabled during development mode which will make it much easier for local testing.

The way it works is our server will parse the bearer token from headers and then send the token back to the introspection url along with the client id and client secret to validate the token. If the token is valid, the scopes will be used to validate access to all resources. For example, let's say Joe logs in to some app that uses this server as a backend. He authenticates with the application and then tries to load up a dashboard full of data. When the request comes to this server, we will take the token provided by the app, validate it, then check the scopes associated with it before returning any data. If Joe has enough scope to view the resources requested, they will be returned, otherwise, an insufficient scope error will be returned.

To set all this up in our server, you only need to define a few environment variables. However, you will need a valid auth server with an introspection endpoint available. This must be used to prevent someone from resigning a token after modifying it or some other man in the middle type attacks. We already wrote our own bearer strategy (`src/strategies/bearer.strategy`) that will utilize the introspection endpoint. You can use it, customize it, or add your own passport strategies (writing your own requires more customization). The three environment variables you need to define are `CLIENT_ID`, `CLIENT_SECRET`, and `INTROSPECTION_URL`.

That's it. Remember authentication is enabled by default and will throw an error if you attempt to query the graphql endpoint without setting those three environment variables. If you want to disable auth completely, just remove the name or strategy from `src/config.js` under the `SERVER_CONFIG.auth` property or remove the `configurePassport` call in `src/index.js`.

## Resolvers
You can read a little bit about GraphQL resolvers on [graphql.org](https://graphql.org/learn/execution/#root-fields-resolvers). Resolvers are where you return data back for the API and it needs to be in the correct format. GraphQL will attempt to coerce data types being resolved when possible, but if you return an invalid property, it will throw an error. You can return an object, array of objects, promise, or an array or promises and GraphQL will just handle it. All of the resolvers are located in `src/resources/{version}/profiles/{profile_name}/resolver.js`.

All of the resolver's contain stubs that just need to be filled in, so all you need to do is start writing queries and returning FHIR resources. Here is a very trivial example of reading a patient from a local filesystem and returning it via a graphql resolver.

```javascript
// In src/resources/3_0_1/profiles/patient/resolver.js
module.exports.patientResolver = function patientResolver (root, args, ctx, info) {
// This is not very realistic, but just giving you a simple idea of how
// this works
let id = args._id;
let path = `patients/${id}.json`;
// If we did this synchronously
return require(path);
// If we wanted to do this asynchronously
return new Promise((resolve, reject) {
let patient;
try {
patient = require(path);
} catch (err) {
let error = errorUtils.internal(ctx.version, 'Patient not found');
return reject(errorUtils.formatErrorForGraphQL(error));
}
return resolve(patient);
});
}
```

## Connecting to a Database
Theoretically we support any database that is supported via node.js. If there is a node module that can connect to it, then we can support it. Connecting to a database will require a few steps to get up and running (don't worry, there is a description of the steps below with some example code connecting to MongoDB).

First, you will need to implement the `initializeDatabaseConnection` method in `src/lib/server`. This needs to establish a connection and store the client somewhere. This can be asynchronous or synchronous, it doesn't really matter. You just need to use it correctly in the next step.

```javascript
const { MongoClient } = require('mongodb');

class Server {
initializeDatabaseConnection(options ={}) {
// Connecting to Mongo is async
// Let's handle this with a promise
return new Promise((resolve, reject) => {
let {
url,
db_name,
mongo_options
} = options;

MongoClient.connect(url, mongo_options, (err, client) => {
if (err) return reject(err);
// Store the client and db on the server instance,
// the server instance is passed into the resolvers
// so it will make accessing this later a breeze
this.client = client;
this.db = client.db(db_name);
return resolve();
});
});
}
}
```

Next, in `src/index.js`, initialize the connection before starting your server. Once you have connected, you can call the `server.listen` method to start listening for requests.

```javascript
const { SERVER_CONFIG } = require('./config');
const FHIRServer = require('./lib/server');

// Start buliding our server
let server = new FHIRServer(SERVER_CONFIG)
.configureMiddleware()
.configurePassport()
.configureHelmet()
.setProfileRoutes()
.setErrorRoutes();

server.initializeDatabaseConnection({
url: 'mongodb://' + mongo_hostname,
db_name: 'my_mongo_database_name',
mongo_options: { auto_reconnect: true }
}).then(() => {
server.listen(SERVER_CONFIG.port);
server.logger.info('FHIR Server listening on localhost:' + SERVER_CONFIG.port);
}).catch(err => {
server.logger.error('Fatal Error connecting to Mongo.');
});
```

Finally, grab the connnection or client in your resolvers so you can start working with your database. Depending how you stored the connection/client in the first step, the implementation here may change slightly. For example, here is what it might look like in a 3.0.1 patient resolver.

```javascript
// In src/resources/3_0_1/profiles/patient/resolver.js
const errorUtils = require('../../../../utils/error.utils');

module.exports.patientResolver = function patientResolver (root, args, ctx, info) {
let db = ctx.server.db;
let version = ctx.version;
let logger = ctx.server.logger;

return new Promise((resolve, reject) => {
// Query MongoDB
db.findOne({ _id: args._id }, (err, patient) => {
// Errors need to be formatted in a specific way, you can see more
// of en explanation on this in the Error handling section below
if (err) {
let error = errorUtils.internal(version, err.message);
logger.error(error);
reject(errorUtils.formatErrorForGraphQL(error));
} else {
resolve(patient);
}
});
});
}
```

That's it! Now you can start writing queries for all your profiles. If you have any more questions feel free to open an issue and tag it with the 'Question' label.

## Error handling
In general, errors should be handled as described in the [Using GraphQL with FHIR](https://build.fhir.org/graphql.html#errors) documentation. In `graphql-fhir` we have already integrated this into our `express` server and the `express-graphql` middleware.

If an unexpected server error or a 404 error happens, the logger logs the event, and the appropriate header(`application/fhir+json`) and response code are sent along with an OperationOutcome. These errors happen outside of the GraphQL middleware which is why we return them as standard OperationOutcome's with the normal headers and response codes.

If an unexpected error happens within the GraphQL middleware, they are logged and wrapped in an OperationOutcome and eventually passed back via GraphQL's errors property, with the OperationOutcome as a resource in the response. If you need to throw an error for any reason, we provide error utilities to help generate an operation outcome and format it for GraphQL. For example, let's say your database connection is temporarily down, in your resolver, you could do something like the following:

```javascript
// Error utils are located in src/utils/error.utils. Depending where
// you are requiring this from, you may need to change the path
const errorUtils = require('../../../../utils/error.utils');

// Provide the version in form 3_0_1 or 1_0_2 so we can create
// the OperationOutcome for the specific version.
let error = errorUtils.internal(version, 'Database connection down');
// Return this or resolve this in your resolvers and GraphQL
// will display it just as you see in the FHIR documentation
let graphqlError = errorUtils.formatErrorForGraphQL(error);
```

We also allow for partial error handling, so you can reject things in one resolver and pass back data in another. For example, let's say a user asks for a patient and an observation, but their account scopes only allow them to read patient records. This means you do not need to reject the entire request, just the parts they are not allowed to access (NOTE: the scope scenario just described is already handled for you automatically if authentication is enabled). In this case, they would see the following response:

```javascript
{
"errors": [{
"path": [ "Observation" ],
"message": "You do not have sufficient scope for read access on this Observation resource.",
"locations": [{
"line": 1,
"column": 32
}],
"extensions": {
"resource": {
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "forbidden",
"diagnostics": "You do not have sufficient scope for read access on this Observation resource."
}]
}
}
}],
"data": {
"Patient": {
"id": "123456789"
},
"Observation": null
}
}
```

## Resource configuration
By default, we support almost all the resources from STU3(3.0.1) and DSTU2(1.0.2). We allow you to turn on or off entire versions very easily. We also allow you to turn off individual profiles or even capabilities within a profile if you desire.

To configure which version you want to support, add/remove a value to the `VERSION` constant in `src/config.js`. You just need to make sure you have a corresponding set of resources in `src/resources`.

In order to explain how to configure a specific profile, let's walk through an example of configuring the patient profile and completely disabling the observation profile for version 1.0.2.

First, let's disable the observation profile. There is two ways you can do this. The quickest is just delete the directory at `src/resources/1_0_2/profiles/observation`. We use globs to discover and enable profiles so by removing this, our server will not be able to discover it and it will not be available in the server. Option number two does not involve deleting it, which may be better if you just need to disable it temporarily. Go to `src/resources/1_0_2/profiles/observation/index.js` and comment out the entire file. This way you can re-enable it later by simply uncommenting it.

Now let's customize the patient profile. Go to `src/resources/1_0_2/profiles/patient/index.js`. There are three top level properties in the exports of these files, `query`, `mutation`, `instance_query`. You can comment out individual properties to disable support for those capabilities. For example:

```javascript
module.exports = {
query: {
// Allow individual patient query
Patient: PatientQuery,
// Don't allow multiple patient query
// PatientList: PatientListQuery
},
mutation: {
// Allow create and update
PatientCreate: PatientCreateMutation,
PatientUpdate: PatientUpdateMutation,
// Don't allow delete
// PatientDelete: PatientDeleteMutation
},
// We don't want to support this at all, so we'll comment
// out the whole thing, this is for /1_0_2/Patient/:id/$graphql
// instance_query: {
// name: 'Patient',
// path: '/1_0_2/Patient/:id',
// query: PatientInstanceQuery
// }
};
```

All profiles are the same and can be customized in the same manner.
Loading

0 comments on commit 7cb299a

Please sign in to comment.