We show below how to configure Cordra to behave like a sample Medical Records Application that could act as a starting point to create a comprehensive system for managing patient medical records.
This sample application supports the following narrative:
Patients
visit Providers
. Information is created during those visits, i.e., Encounters
.
Providers have areas of expertise that determine their speciality: General Physician
and Endocrinologist
are supported here.
Patients encounters begin with a General Physician (GP) and the GP may order tests at the Labs
.
Lab tests produce additional information.
GP may recommend patients to specialists.
Patients encounter with specialists produces more information.
Also,
Patients have read access to their medical information.
Providers who see patients have read/write access to the medical information they create.
In one possible scenario, patients must specifically give providers’ access to their information when they visit new providers.
In another possible scenario, the referring provider may give the referred provider access to the patient information.
Labs do not have access to any medical information, by default. Patients must give explicit access.
We describe below how to support the aforementioned narrative, beginning with the design.
We will create four different types of digital objects:
Patient
type to capture information about patients. Patient Id, Name, Age, and Sex are captured.
Provider
type to capture information about providers. Provider Id, Name, and Speciality are captured.
Lab
type to capture information about labs. Lab Id and Name are captured.
Encounter
type to capture medical information created by providers or generated due to lab tests. Specifically:
Timestamp,
Patient Id,
Producer (to reflect the Id of the provider and/or the lab that produced the information)
Notes (to capture provider observations or lab results)
Referred (to capture the Id of the provider or the lab to whom the patient is referred)
Referral Details (to capture lab prescriptions or details why the patient is referred to a specialist).
Four types of digital objects are show below: Patient, Provider, Lab, and Encounter. Notice that identifiers of digital objects are flagged to be auto-generated. The timestamp attribute in the Encounter object is also flagged to be auto-populated at the time of creation.
Patient Schema:
{
"type": "object",
"properties": {
"id": {
"type": "string",
"cordra": {
"type": {
"autoGeneratedField": "handle"
}
}
},
"name": {
"type": "string",
"cordra": {
"preview": {
"showInPreview": true,
"excludeTitle": true,
"isPrimary": true
}
}
},
"age": {
"type": "number",
"cordra": {
"preview": {
"showInPreview": true
}
}
},
"sex": {
"type": "string",
"enum": [
"male",
"female",
"other"
],
"cordra": {
"preview": {
"showInPreview": true
}
}
},
"shareEncountersWith": {
"type": "array",
"format": "table",
"uniqueItems": true,
"items": {
"type": "string",
"cordra": {
"type": {
"handleReference": {
"types": [
"Provider",
"Lab",
"Patient"
]
}
}
}
}
},
"username": {
"type": "string",
"cordra": {
"preview": {
"showInPreview": true
},
"auth": "username"
}
},
"password": {
"type": "string",
"format": "password",
"cordra": {
"auth": "password"
}
}
}
}
Provider Schema:
{
"type": "object",
"properties": {
"id": {
"type": "string",
"cordra": {
"type": {
"autoGeneratedField": "handle"
}
}
},
"name": {
"type": "string",
"cordra": {
"preview": {
"showInPreview": true,
"isPrimary": true
}
}
},
"speciality": {
"type": "string",
"enum": [
"General Physician",
"Endocronologist"
],
"cordra": {
"preview": {
"showInPreview": true
}
}
},
"username": {
"type": "string",
"cordra": {
"preview": {
"showInPreview": true
},
"auth": "username"
}
},
"password": {
"type": "string",
"format": "password",
"cordra": {
"auth": "password"
}
}
}
}
Lab Schema:
{
"type": "object",
"properties": {
"id": {
"type": "string",
"cordra": {
"type": {
"autoGeneratedField": "handle"
}
}
},
"name": {
"type": "string",
"cordra": {
"preview": {
"showInPreview": true,
"isPrimary": true
}
}
},
"username": {
"type": "string",
"cordra": {
"preview": {
"showInPreview": true
},
"auth": "username"
}
},
"password": {
"type": "string",
"format": "password",
"cordra": {
"auth": "password"
}
}
}
}
Encounter Schema:
{
"type": "object",
"properties": {
"id": {
"type": "string",
"cordra": {
"type": {
"autoGeneratedField": "handle"
}
}
},
"timestamp": {
"type": "string",
"cordra": {
"type": {
"autoGeneratedField": "creationDate"
}
}
},
"producer": {
"type": "string",
"cordra": {
"type": {
"autoGeneratedField": "createdBy"
}
}
},
"patientId": {
"type": "string",
"cordra": {
"type": {
"handleReference": {
"types": [
"Patient"
]
}
}
}
},
"shareWith": {
"type": "array",
"format": "table",
"uniqueItems": true,
"items": {
"type": "string",
"cordra": {
"type": {
"handleReference": {
"types": [
"Provider",
"Lab",
"Patient"
]
}
}
}
}
},
"notes": {
"type": "string",
"format": "textarea"
},
"referred": {
"type": "string",
"cordra": {
"type": {
"handleReference": {
"types": [
"Provider",
"Lab"
]
}
}
}
},
"referralDetails": {
"type": "string",
"format": "textarea"
}
}
}
Using JavaScript rules, we will ensure that an encounter can be read by the referenced patient. We will also ensure that an encounter can be read or written by the referenced provider or lab.
Associate the following JavaScript with the encounter type.
var cordra = require("cordra");
exports.beforeSchemaValidation = beforeSchemaValidation;
exports.beforeDelete = beforeDelete;
function beforeDelete(encounter, context) {
if (encounter.content.patientId === context.userId) {
throw "Patients are not permitted to delete encounters";
}
}
function beforeSchemaValidation(encounter, context) {
if (encounter.content.patientId === context.userId) {
authorizePatientPermittedToMakeRequest(encounter, context);
}
encounter.acl = {};
encounter.acl.writers = [];
encounter.acl.readers = [];
addIfAbsent(encounter.acl.writers, encounter.content.patientId);
//Note that JavaScript prevents the patient from actually
//editing the content of the object, they can only edit shareWith property
addIfAbsent(encounter.acl.readers, encounter.content.patientId);
var producer = encounter.content.producer;
if (context.isNew) {
producer = context.userId;
}
addIfAbsent(encounter.acl.writers, producer);
addIfAbsent(encounter.acl.readers, producer);
if (encounter.content.referred) {
addIfAbsent(encounter.acl.readers, encounter.content.referred);
}
if (encounter.content.shareWith) {
addAll(encounter.acl.readers, encounter.content.shareWith);
}
var patient = cordra.get(encounter.content.patientId);
if (patient.content.shareEncountersWith) {
addAll(encounter.acl.readers, patient.content.shareEncountersWith);
}
return encounter;
}
function authorizePatientPermittedToMakeRequest(encounter, context) {
if (context.isNew) {
throw "A patient is not permitted to create encounters";
}
var oldEncounter = cordra.get(encounter.id);
if (!isEqual(oldEncounter.acl.readers, encounter.acl.readers)) {
throw "A patient is not permitted to directly modify the readers acl of an encounter";
}
if (!isEqual(oldEncounter.acl.writers, encounter.acl.writers)) {
throw "A patient is not permitted to modify the writers acl of an encounter";
}
if (!isEqualWithoutShareWith(oldEncounter.content, encounter.content)) {
throw "A patient is only permitted to modify the 'shareWith' property of an encounter";
}
}
function isEqualWithoutShareWith(object, oldObject) {
var objectCopy = JSON.parse(JSON.stringify(object));
var oldObjectCopy = JSON.parse(JSON.stringify(oldObject));
delete objectCopy.shareWith;
delete oldObjectCopy.shareWith;
return isEqual(objectCopy, oldObjectCopy);
}
function isEqual(a, b) {
var aJson = JSON.stringify(a);
var bJson = JSON.stringify(b);
return aJson === bJson;
}
function addAll(list, idsToAdd) {
for (var i = 0; i < idsToAdd.length; i++) {
addIfAbsent(list, idsToAdd[i]);
}
}
function addIfAbsent(list, id) {
if (list.indexOf(id) == -1) {
list.push(id);
}
}
Download the above types here
. You can then load this
information using the Cordra UI. Sign in into Cordra as admin
and select the Admin->Types dropdown menu.
Click the “Load from file” button. In the dialog that pops up, select the types file you downloaded and check
the box to delete existing objects. Click “Load” to import the types into Cordra.
That is it. The system is now ready for use.
We will use curl and the REST API to demonstrate how to use the system. JSON records used with the various commands are also shown below. Although not shown here, the Cordra UI may be used to perform equivalent actions.
For the purpose of this tutorial, the default Cordra address of https://localhost:8443/
is used. If your Cordra
installation is running at a different location, please make the appropriate substitution. Also, the example curl
commands will use the -k
flag to tell curl to trust the self-signed certificate that comes with Cordra. This flag
should not be used on production installations with real certificates.
All Ids shown in the sample curl commands were randomly generated. You will need to substitute these values with the appropriate Ids in your local system.
Before issuing any calls that require authorization, we must first authenticate and get an access token:
curl -k -X POST 'https://localhost:8443/auth/token' -H "Content-Type: application/json" --data @- << END
{
"grant_type": "password",
"username": "admin",
"password": "password"
}
END
This call will return a token that we will use in subsequent calls.
Creations return back responses that consist of the Ids allotted to the corresponding user objects.
Create a provider who is a general physician:
curl -k -X POST 'https://localhost:8443/objects/?type=Provider' -H "Content-Type: application/json" -H "Authorization: Bearer ADMIN_ACCESS_TOKEN" --data @- << END
{
"id": "",
"name": "Springfield Medical Centre",
"speciality": "General Physician",
"username": "gp",
"password": "password"
}
END
Create a provider who is an endocrinologist:
curl -k -X POST 'https://localhost:8443/objects/?type=Provider' -H "Content-Type: application/json" -H "Authorization: Bearer ADMIN_ACCESS_TOKEN" --data @- << END
{
"id": "",
"name": "Springfield Endocrinology",
"speciality": "Endocronologist",
"username": "end",
"password": "password"
}
END
Create a lab:
curl -k -X POST 'https://localhost:8443/objects/?type=Lab' -H "Content-Type: application/json" -H "Authorization: Bearer ADMIN_ACCESS_TOKEN" --data @- << END
{
"id": "",
"name": "Generic Lab",
"username": "lab",
"password": "password"
}
END
Create a patient named Jane Smith:
curl -k -X POST 'https://localhost:8443/objects/?type=Patient' -H "Content-Type: application/json" -H "Authorization: Bearer ADMIN_ACCESS_TOKEN" --data @- << END
{
"id": "",
"name": "Jane Smith",
"age": 40,
"sex": "female",
"username": "jane",
"password": "password"
}
END
Create another patient named John Smith. This patient is configured by the admin to share read access to all his new encounters with his wife Jane. As stated earlier, Jane’s Id as used in shareEncountersWith property is randomly generated in this example:
curl -k -X POST 'https://localhost:8443/objects/?type=Patient' -H "Content-Type: application/json" -H "Authorization: Bearer ADMIN_ACCESS_TOKEN" --data @- << END
{
"id": "",
"name": "John Smith",
"age": 40,
"sex": "male",
"shareEncountersWith": [
"test/a430dcf58c9acea52af0"
],
"username": "john",
"password": "password"
}
END
Note: For each different user in this section, you will need to get a new access token, as described above:
The patient visits the General Physician (GP). The GP creates this encounter. Note that id, producer, and timestamp are automatically filled by Cordra as instructed in schemas and in rules:
curl -k -X POST 'https://localhost:8443/objects/?type=Encounter' -H "Content-Type: application/json" -H "Authorization: Bearer GP_ACCESS_TOKEN" --data @- << END
{
"id": "",
"producer": "",
"timestamp": "",
"patientId": "test/e61a3587b3f7a142b8c7",
"notes": "Patient complains of fatigue. Order CBC, BMP, and Thyroid Panel tests."
}
END
The GP orders lab tests. The patient visits the lab and the lab needs access to the encounter. The patient gives the
lab access to the encounter. Since the patient can only modify the shareWith
property on an encounter, first the patient, John,
must GET the encounter object by its Id. As shown later, the patient can search for all encounters to get their Ids among other details.:
curl -k -X GET 'https://localhost:8443/objects/test/b4ab731228572b88fae1' -H "Authorization: Bearer JOHN_ACCESS_TOKEN"
Response:
{
"id": "test/b4ab731228572b88fae1",
"timestamp": "2018-09-19T19:48:52.430Z",
"producer": "test/185108997731deb1edda",
"patientId": "test/e61a3587b3f7a142b8c7",
"notes": "Patient complains of fatigue. Order CBC, BMP, and Thyroid Panel tests.",
"shareWith": []
}
The patient can then modify the encounter object, as received above, to include the Id of the lab in order to share the encounter. The patient updates the encounter object:
curl -k -X PUT 'https://localhost:8443/objects/test/b4ab731228572b88fae1?jsonPointer=/shareWith' -H "Content-Type: application/json" -H "Authorization: Bearer JOHN_ACCESS_TOKEN" --data @- << END
[
"test/9b405c77d1a2f1760287"
]
END
The lab creates an encounter including the results from the lab. They include the GP in the shareWith
property:
curl -k -X POST 'https://localhost:8443/objects/?type=Encounter' -H "Content-Type: application/json" -H "Authorization: Bearer LAB_ACCESS_TOKEN" --data @- << END
{
"id": "",
"producer": "",
"timestamp": "",
"patientId": "test/e61a3587b3f7a142b8c7",
"notes": "Thyroid Panel reveals high TSH levels.",
"shareWith": [
"test/185108997731deb1edda"
]
}
END
The patient returns to the GP, and the GP creates a 3rd encounter referring the patient to an endocrinologist.:
curl -k -X POST 'https://localhost:8443/objects/?type=Encounter' -H "Content-Type: application/json" -H "Authorization: Bearer GP_ACCESS_TOKEN" --data @- << END
{
"id": "",
"producer": "",
"timestamp": "",
"patientId": "test/e61a3587b3f7a142b8c7",
"notes": "I am referring you to see an endocrinologist",
"referred": "test/eb02409feb90de550756"
}
END
The patient searches and retrieves all the encounters:
curl -k -X GET 'https://localhost:8443/search/?query=type:Encounter' -H "Authorization: Bearer JOHN_ACCESS_TOKEN"
The patient modifies the previous two encounters sharing them with the endocrinologist.:
curl -k -X PUT 'https://localhost:8443/objects/test/b4ab731228572b88fae1?jsonPointer=/shareWith' -H "Content-Type: application/json" -H "Authorization: Bearer JOHN_ACCESS_TOKEN" --data @- << END
[
"test/9b405c77d1a2f1760287",
"test/eb02409feb90de550756"
]
END
curl -k -X PUT 'https://localhost:8443/objects/test/69a0212b19bb02e25a7d?jsonPointer=/shareWith' -H "Content-Type: application/json" -H "Authorization: Bearer JOHN_ACCESS_TOKEN" --data @- << END
[
"test/185108997731deb1edda",
"test/eb02409feb90de550756"
]
END