Medical Records Application

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.

Narrative

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.

Design

We will create four different types of digital objects:

  1. Patient type to capture information about patients. Patient Id, Name, Age, and Sex are captured.
  2. Provider type to capture information about providers. Provider Id, Name, and Speciality are captured.
  3. Lab type to capture information about labs. Lab Id and Name are captured.
  4. 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).

Authentication and Authorization

While we assume PKI will normally be enabled for a production application for security, and the system can require that patients, providers, and labs authenticate via private keys, we described the use of passwords below to keep the example simple. In addition to the aforementioned attributes for Patient, Provider, and Lab, we add password attribute as can be seen in the schemas below. In that sense, patients, providers, and labs are all treated as users of the system.

A patient has read access to encounters that are about them. A provider has read/write access to encounters that are generated by them. Other providers (or labs) will only have access to encounters if they are explicitly shared. We will see how later in this tutorial.

When signed in as the admin user select Admin->Authorization from the menu bar to specify type-level access control. This JSON is stored on the Design object and will be loaded automatically when you “Load from file” as described later on. For keeping this description simple, this particular authorization configuration allows Patients, Providers, and Labs to be read by all users but only be modified by the representative party. Encounter objects do not allow readers or writers at the type level as their particular readers and writers are calculated in JavaScript for each specific instance of an Encounter.

Type-level Access Control:

{
  "schemaAcls": {
    "CordraDesign": {
      "defaultAclRead": [
        "public"
      ],
      "defaultAclWrite": [],
      "aclCreate": []
    },
    "Schema": {
      "defaultAclRead": [
        "public"
      ],
      "defaultAclWrite": [],
      "aclCreate": []
    },
    "Patient": {
      "defaultAclRead": [
        "authenticated"
      ],
      "defaultAclWrite": [
        "self"
      ],
      "aclCreate": []
    },
    "Encounter": {
      "defaultAclRead": [],
      "defaultAclWrite": [],
      "aclCreate": [
        "authenticated"
      ]
    },
    "Lab": {
      "defaultAclRead": [
        "authenticated"
      ],
      "defaultAclWrite": [
        "self"
      ],
      "aclCreate": []
    },
    "Provider": {
      "defaultAclRead": [
        "authenticated"
      ],
      "defaultAclWrite": [
        "self"
      ],
      "aclCreate": []
    }
  },
  "defaultAcls": {
    "defaultAclRead": [
      "authenticated"
    ],
    "defaultAclWrite": [],
    "aclCreate": [
      "authenticated"
    ]
  }
}

Schemas

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"
    }
  }
}

Rules

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);
    }
}

Setup

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.

Using the Application

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.

Authenticating

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.

Create users

Creations return back with 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

Create encounters

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/objects/?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