diff --git a/src/index.js b/src/index.js index f0567aa..4d410b4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,34 +1,34 @@ -import React from "react" -import ReactDOM from "react-dom" -import App from "./components/App" -import { Provider } from "react-redux" -import STORE from "./redux" -import PatientDetail from "./components/PatientDetail" -import PatientList from "./components/PatientList" -import { Router, Route, Switch } from "react-router" -import createHistory from "history/createHashHistory" -import jQuery from "jquery" +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./components/App"; +import { Provider } from "react-redux"; +import STORE from "./redux"; +import PatientDetail from "./components/PatientDetail"; +import PatientList from "./components/PatientList"; +import { Router, Route, Switch } from "react-router"; +import createHistory from "history/createHashHistory"; +import jQuery from "jquery"; -window.$ = window.jQuery = jQuery +window.$ = window.jQuery = jQuery; -const history = createHistory() +const history = createHistory(); ReactDOM.render( - - - - - - - - - - , - document.getElementById("main") -) + + + + + + + + + + , + document.getElementById("main") +); $(function () { - $("body").tooltip({ - selector : ".patient-detail-page [title]" - }) -}) \ No newline at end of file + $("body").tooltip({ + selector: ".patient-detail-page [title]", + }); +}); diff --git a/src/lib/PatientSearch.js b/src/lib/PatientSearch.js index ffb3214..958ea28 100644 --- a/src/lib/PatientSearch.js +++ b/src/lib/PatientSearch.js @@ -1,7 +1,7 @@ -import moment from "moment" -import { CODE_SYSTEMS } from "./constants" -import { parseQueryString, request } from "." -import { intVal, getPath } from "." +import moment from "moment"; +import { CODE_SYSTEMS } from "./constants"; +import { parseQueryString, request } from "."; +import { intVal, getPath } from "."; /** * This is just a helper class that is used as a query builder. It has some @@ -9,869 +9,867 @@ import { intVal, getPath } from "." * compile those into a query string that can be passed to the Patient endpoint * of a fhir api server. */ -export default class PatientSearch -{ - /** - * The constructor just creates an empty instance. Use the setter methods - * to set query params and then call compile to build the query string. - */ - constructor(options = {}) { - - this.__cache__ = {}; - - this.__scheduled__ = {}; - - /** - * The list of conditions that should be included in the query. - * @type {Object} A map of unique keys and condition objects - * @private - */ - this.conditions = { ...(options.conditions || {}) }; - - /** - * The desired minimal age of the patients as an object like - * { value: 5, units: "years" } - * @type {Object} - * @private - */ - this.minAge = null; - - /** - * The desired maximal age of the patients as an object like - * { value: 5, units: "years" } - * @type {Object} - * @private - */ - this.maxAge = null; - - /** - * The patient gender to search for (male|female) - * @type {String} - * @private - */ - this.gender = options.gender || null; - - /** - * How many patients to fetch per page. Defaults to null meaning that - * this param will not be included in the query and we are leaving it - * for the server to decide. - * @type {Number} - * @private - */ - this.limit = options.limit || null; - - /** - * How many patients to skip. Defaults to null meaning that - * this param will not be included in the query and we are leaving it - * for the server to decide. - * @type {Number} - * @private - */ - this.offset = null; - - /** - * How many pages to skip. Defaults to null meaning that - * this param will not be included in the query and we are leaving it - * for the server to decide. - * @type {Number} - * @private - */ - this.page = null; - - /** - * A collection of additional parameters - * @type {Object} - * @private - */ - this.params = {}; - - /** - * This is like a flag that toggle the instance to different modes - * (currently only advanced and default are supported) - * @type {String} "advanced" or "default" - * @private - */ - this.queryType = options.queryType || "default"; - - /** - * The query string to use if in advanced mode - * @type {String} - * @private - */ - this.queryString = options.queryString || ""; - - /** - * The sort parameters list - * @type {String} - * @private - */ - this.sort = options.sort || ""; - - /** - * All the tags that the patients should be filtered by - * @type {Array} - * @private - */ - this.tags = String(options.tags || "").split(/\s*,\s*/).filter(Boolean); - } - - /** - * Schedule a prop change to be made before the next compile - * @param {Object} props - */ - schedule(props) { - this.__scheduled__ = { ...this.__scheduled__, ...props }; - } +export default class PatientSearch { + /** + * The constructor just creates an empty instance. Use the setter methods + * to set query params and then call compile to build the query string. + */ + constructor(options = {}) { + this.__cache__ = {}; - hasParam(name) { - return this.params.hasOwnProperty(name); - } + this.__scheduled__ = {}; /** - * Sets a param by name. Note that this is a lower level interface. It does - * not know anything about the parameter thus it will not handle UI - * dependencies (eg. it won't reset the offset for you) - * @param {Name} name The name of the parameter to set - * @param {*} value The value to set. Use undefined to remove a parameter - * @returns {PatientSearch} Returns the instance + * The list of conditions that should be included in the query. + * @type {Object} A map of unique keys and condition objects + * @private */ - setParam(name, value) { - let has = this.hasParam(name) - if (value === undefined) { - if (has) { - delete this.params[name] - } - } - else { - this.params[name] = value - } - this.offset = null; - this.cacheId = null; - return this - } + this.conditions = { ...(options.conditions || {}) }; /** - * Sets the query type. In advanced mode a query string is provided and - * parsed and all the other parameters are ignored. In default mode the - * query string is ignored and only the other params are used. - * @param {String} type "advanced" or anything else for "default" - * @returns {PatientSearch} Returns the instance + * The desired minimal age of the patients as an object like + * { value: 5, units: "years" } + * @type {Object} + * @private */ - setQueryType(type) { - this.queryType = type == "advanced" ? "advanced" : "default" - return this - } + this.minAge = null; /** - * Sets the query string to be used while in advanced mode. Note that this - * will not be used if not in advanced mode but the query string will still - * be persisted so that if the user switches the UI to advanced the last - * query can be displayed... - * @param {String} query The query string to use if in advanced mode - * @returns {PatientSearch} Returns the instance + * The desired maximal age of the patients as an object like + * { value: 5, units: "years" } + * @type {Object} + * @private */ - setQueryString(query) { - this.queryString = String(query || "") - return this - } + this.maxAge = null; /** - * Adds a condition to the list of patient conditions - * @param {String} key Unique string identifier for that condition - * @param {Object} condition The condition to add - * @returns {PatientSearch} Returns the instance + * The patient gender to search for (male|female) + * @type {String} + * @private */ - addCondition(key, condition) { - this.conditions[key] = condition; - this.__cache__.patientIDs = null; - return this; - } + this.gender = options.gender || null; /** - * Removes the condition identified by it's key. If that condition is not - * currently included it does nothing - * @param {*} key Unique string identifier for that condition - * @returns {PatientSearch} Returns the instance + * How many patients to fetch per page. Defaults to null meaning that + * this param will not be included in the query and we are leaving it + * for the server to decide. + * @type {Number} + * @private */ - removeCondition(key) { - if (this.conditions.hasOwnProperty(key)) { - delete this.conditions[key]; - this.__cache__.patientIDs = null; - } - return this; - } + this.limit = options.limit || null; /** - * Replaces the entire set of conditions at once - * @param {Object} conditions The new conditions to set - * @returns {PatientSearch} Returns the instance + * How many patients to skip. Defaults to null meaning that + * this param will not be included in the query and we are leaving it + * for the server to decide. + * @type {Number} + * @private */ - setConditions(conditions) { - this.conditions = { ...conditions }; - this.schedule({ - offset: null, - cacheId: null - }); - this.__cache__.patientIDs = null; - return this; - } - - addTag(tag) { - if (this.tags.findIndex(tag) == -1) { - this.tags.push(tag); - } - return this; - } - - removeTag(tag) { - let index = this.tags.findIndex(tag); - if (index > -1) { - this.tags.splice(index, 1); - } - return this; - } - - setTags(tags) { - this.tags = [ ...tags ] - this.schedule({ - offset: null, - cacheId: null - }); - return this; - } + this.offset = null; /** - * Sets the desired min age af the patients. This can also be set to null - * (or other falsy value) to exclude the minAge restrictions from the query. - * @param {object} age The age - * @param {number} age.value The age as number of units - * @param {string} age.units The units for the value (years|months|days) - * @returns {PatientSearch} Returns the instance + * How many pages to skip. Defaults to null meaning that + * this param will not be included in the query and we are leaving it + * for the server to decide. + * @type {Number} + * @private */ - setMinAge(age) { - this.minAge = age; - this.schedule({ - offset: null, - cacheId: null - }); - return this; - } + this.page = null; /** - * Sets the desired max age af the patients. This can also be set to null - * (or other falsy value) to exclude the maxAge restrictions from the query. - * @param {object} age The age - * @param {number} age.value The age as number of units - * @param {string} age.units The units for the value (years|months|days) - * @returns {PatientSearch} Returns the instance + * A collection of additional parameters + * @type {Object} + * @private */ - setMaxAge(age) { - this.maxAge = age; - this.schedule({ - offset: null, - cacheId: null - }); - return this; - } + this.params = {}; /** - * Sets the min and max ages depending on the specified age group keyword - * @param {*} group Can be one of infant, child, adult, elderly. - * Anything else will clear the age constraints! - * @returns {PatientSearch} Returns the instance + * This is like a flag that toggle the instance to different modes + * (currently only advanced and default are supported) + * @type {String} "advanced" or "default" + * @private */ - setAgeGroup(group) { - this.ageGroup = group; - this.schedule({ - offset: null, - cacheId: null - }); - - switch (group) { - - // infant - 0 to 12 months - case "infant": - this.setMinAge(null); - this.setMaxAge({ value: 1, units: "years" }); - break; - - // child - 1 to 18 years - case "child": - this.setMinAge({ value: 1 , units: "years" }); - this.setMaxAge({ value: 18, units: "years" }); - break; - - // adult - 18 to 65 years - case "adult": - this.setMinAge({ value: 18, units: "years" }); - this.setMaxAge({ value: 65, units: "years" }); - break; - - // Elderly - 65+ - case "elderly": - this.setMinAge({ value: 65, units: "years" }); - this.setMaxAge(null); - break; - - // Anything else clears the birthdate param - default: - this.setMinAge(null); - this.setMaxAge(null); - // this.ageGroup = null; - break; - } - return this; - } + this.queryType = options.queryType || "default"; /** - * Sets the gender to search for. Can be "male" or "female". Any falsy value - * will clear the gender param - * @param {String} gender "male" or "female" - * @returns {PatientSearch} Returns the instance + * The query string to use if in advanced mode + * @type {String} + * @private */ - setGender(gender) { - if (gender !== this.gender) { - this.gender = gender; - this.schedule({ - offset : null, - cacheId: null - }); - } - return this; - } + this.queryString = options.queryString || ""; /** - * Sets how many patients will be fetched per page - * @param {number|string} limit The number of records to fetch - * @returns {PatientSearch} Returns the instance + * The sort parameters list + * @type {String} + * @private */ - setLimit(limit) { - this.limit = intVal(limit) - if (this.limit < 1) { - this.limit = null; - } - return this; - } + this.sort = options.sort || ""; /** - * Sets how many patients will be skipped - * @param {string} cacheId The id generated by the server (_getpages) - * @param {number|string} offset The number of records to skip - * @returns {PatientSearch} Returns the instance + * All the tags that the patients should be filtered by + * @type {Array} + * @private */ - setOffset(cacheId, offset) { - this.offset = intVal(offset) - this.cacheId = cacheId - if (this.offset < 1) { - this.offset = null; - this.cacheId = null; - } - return this; + this.tags = String(options.tags || "") + .split(/\s*,\s*/) + .filter(Boolean); + } + + /** + * Schedule a prop change to be made before the next compile + * @param {Object} props + */ + schedule(props) { + this.__scheduled__ = { ...this.__scheduled__, ...props }; + } + + hasParam(name) { + return this.params.hasOwnProperty(name); + } + + /** + * Sets a param by name. Note that this is a lower level interface. It does + * not know anything about the parameter thus it will not handle UI + * dependencies (eg. it won't reset the offset for you) + * @param {Name} name The name of the parameter to set + * @param {*} value The value to set. Use undefined to remove a parameter + * @returns {PatientSearch} Returns the instance + */ + setParam(name, value) { + let has = this.hasParam(name); + if (value === undefined) { + if (has) { + delete this.params[name]; + } + } else { + this.params[name] = value; } - - /** - * Sets how many pages will be skipped - * @param {number|string} page The number of pages to skip - * @returns {PatientSearch} Returns the instance - */ - setPage(page) { - this.page = intVal(page); - if (this.page < 1) { - this.page = null; - } - return this; + this.offset = null; + this.cacheId = null; + return this; + } + + /** + * Sets the query type. In advanced mode a query string is provided and + * parsed and all the other parameters are ignored. In default mode the + * query string is ignored and only the other params are used. + * @param {String} type "advanced" or anything else for "default" + * @returns {PatientSearch} Returns the instance + */ + setQueryType(type) { + this.queryType = type == "advanced" ? "advanced" : "default"; + return this; + } + + /** + * Sets the query string to be used while in advanced mode. Note that this + * will not be used if not in advanced mode but the query string will still + * be persisted so that if the user switches the UI to advanced the last + * query can be displayed... + * @param {String} query The query string to use if in advanced mode + * @returns {PatientSearch} Returns the instance + */ + setQueryString(query) { + this.queryString = String(query || ""); + return this; + } + + /** + * Adds a condition to the list of patient conditions + * @param {String} key Unique string identifier for that condition + * @param {Object} condition The condition to add + * @returns {PatientSearch} Returns the instance + */ + addCondition(key, condition) { + this.conditions[key] = condition; + this.__cache__.patientIDs = null; + return this; + } + + /** + * Removes the condition identified by it's key. If that condition is not + * currently included it does nothing + * @param {*} key Unique string identifier for that condition + * @returns {PatientSearch} Returns the instance + */ + removeCondition(key) { + if (this.conditions.hasOwnProperty(key)) { + delete this.conditions[key]; + this.__cache__.patientIDs = null; } - - - /** - * Sets the sorting to use - * @param {string} sort A fhir sort string like "status,-date,category" - * @returns {PatientSearch} Returns the instance - */ - setSort(sort) { - this.sort = sort; - this.offset = null; - this.cacheId = null; - return this + return this; + } + + /** + * Replaces the entire set of conditions at once + * @param {Object} conditions The new conditions to set + * @returns {PatientSearch} Returns the instance + */ + setConditions(conditions) { + this.conditions = { ...conditions }; + this.schedule({ + offset: null, + cacheId: null, + }); + this.__cache__.patientIDs = null; + return this; + } + + addTag(tag) { + if (this.tags.findIndex(tag) == -1) { + this.tags.push(tag); } + return this; + } - /** - * Returns another PatientSearch instance with the exact same state as this. - * @returns {PatientSearch} Returns the new copy - */ - clone() { - let inst = new PatientSearch(); - - inst.conditions = { ...this.conditions }; - inst.params = { ...this.params }; - inst.tags = [ ...this.tags ]; - - inst.setSort(this.sort) - .setAgeGroup(this.ageGroup) - .setMinAge(this.minAge) - .setMaxAge(this.maxAge) - .setGender(this.gender) - .setLimit(this.limit) - .setOffset(this.cacheId, this.offset) - .setQueryType(this.queryType) - .setQueryString(this.queryString); - - return inst; + removeTag(tag) { + let index = this.tags.findIndex(tag); + if (index > -1) { + this.tags.splice(index, 1); } - - /** - * Clear all params. If you call compile after clear only the "_format=json" - * part should be returned - * @returns {PatientSearch} Returns the instance - */ - reset() { - this.conditions = {}; - this.minAge = null; - this.maxAge = null; - this.gender = null; - this.limit = null; - this.offset = null; - this.cacheId = null; - this.ageGroup = null; - this.page = null; - this.params = {}; - this.queryString = ""; - this.queryType = "default" - this.sort = ""; - this.tags = []; - return this; + return this; + } + + setTags(tags) { + this.tags = [...tags]; + this.schedule({ + offset: null, + cacheId: null, + }); + return this; + } + + /** + * Sets the desired min age af the patients. This can also be set to null + * (or other falsy value) to exclude the minAge restrictions from the query. + * @param {object} age The age + * @param {number} age.value The age as number of units + * @param {string} age.units The units for the value (years|months|days) + * @returns {PatientSearch} Returns the instance + */ + setMinAge(age) { + this.minAge = age; + this.schedule({ + offset: null, + cacheId: null, + }); + return this; + } + + /** + * Sets the desired max age af the patients. This can also be set to null + * (or other falsy value) to exclude the maxAge restrictions from the query. + * @param {object} age The age + * @param {number} age.value The age as number of units + * @param {string} age.units The units for the value (years|months|days) + * @returns {PatientSearch} Returns the instance + */ + setMaxAge(age) { + this.maxAge = age; + this.schedule({ + offset: null, + cacheId: null, + }); + return this; + } + + /** + * Sets the min and max ages depending on the specified age group keyword + * @param {*} group Can be one of infant, child, adult, elderly. + * Anything else will clear the age constraints! + * @returns {PatientSearch} Returns the instance + */ + setAgeGroup(group) { + this.ageGroup = group; + this.schedule({ + offset: null, + cacheId: null, + }); + + switch (group) { + // infant - 0 to 12 months + case "infant": + this.setMinAge(null); + this.setMaxAge({ value: 1, units: "years" }); + break; + + // child - 1 to 18 years + case "child": + this.setMinAge({ value: 1, units: "years" }); + this.setMaxAge({ value: 18, units: "years" }); + break; + + // adult - 18 to 65 years + case "adult": + this.setMinAge({ value: 18, units: "years" }); + this.setMaxAge({ value: 65, units: "years" }); + break; + + // Elderly - 65+ + case "elderly": + this.setMinAge({ value: 65, units: "years" }); + this.setMaxAge(null); + break; + + // Anything else clears the birthdate param + default: + this.setMinAge(null); + this.setMaxAge(null); + // this.ageGroup = null; + break; } - - /** - * Returns an object representing the current state of the instance. - * The object contains COPIES of the current param values. - * @returns {Object} - */ - getState() { - return { - conditions : this.conditions, - minAge : this.minAge, - maxAge : this.maxAge, - gender : this.gender, - limit : this.limit, - offset : this.offset, - cacheId : this.cacheId, - page : this.page, - ageGroup : this.ageGroup, - params : { ...this.params }, - queryString : this.queryString, - queryType : this.queryType, - sort : this.sort, - tags : [ ...this.tags ] - }; + return this; + } + + /** + * Sets the gender to search for. Can be "male" or "female". Any falsy value + * will clear the gender param + * @param {String} gender "male" or "female" + * @returns {PatientSearch} Returns the instance + */ + setGender(gender) { + if (gender !== this.gender) { + this.gender = gender; + this.schedule({ + offset: null, + cacheId: null, + }); + } + return this; + } + + /** + * Sets how many patients will be fetched per page + * @param {number|string} limit The number of records to fetch + * @returns {PatientSearch} Returns the instance + */ + setLimit(limit) { + this.limit = intVal(limit); + if (this.limit < 1) { + this.limit = null; + } + return this; + } + + /** + * Sets how many patients will be skipped + * @param {string} cacheId The id generated by the server (_getpages) + * @param {number|string} offset The number of records to skip + * @returns {PatientSearch} Returns the instance + */ + setOffset(cacheId, offset) { + this.offset = intVal(offset); + this.cacheId = cacheId; + if (this.offset < 1) { + this.offset = null; + this.cacheId = null; + } + return this; + } + + /** + * Sets how many pages will be skipped + * @param {number|string} page The number of pages to skip + * @returns {PatientSearch} Returns the instance + */ + setPage(page) { + this.page = intVal(page); + if (this.page < 1) { + this.page = null; + } + return this; + } + + /** + * Sets the sorting to use + * @param {string} sort A fhir sort string like "status,-date,category" + * @returns {PatientSearch} Returns the instance + */ + setSort(sort) { + this.sort = sort; + this.offset = null; + this.cacheId = null; + return this; + } + + /** + * Returns another PatientSearch instance with the exact same state as this. + * @returns {PatientSearch} Returns the new copy + */ + clone() { + let inst = new PatientSearch(); + + inst.conditions = { ...this.conditions }; + inst.params = { ...this.params }; + inst.tags = [...this.tags]; + + inst + .setSort(this.sort) + .setAgeGroup(this.ageGroup) + .setMinAge(this.minAge) + .setMaxAge(this.maxAge) + .setGender(this.gender) + .setLimit(this.limit) + .setOffset(this.cacheId, this.offset) + .setQueryType(this.queryType) + .setQueryString(this.queryString); + + return inst; + } + + /** + * Clear all params. If you call compile after clear only the "_format=json" + * part should be returned + * @returns {PatientSearch} Returns the instance + */ + reset() { + this.conditions = {}; + this.minAge = null; + this.maxAge = null; + this.gender = null; + this.limit = null; + this.offset = null; + this.cacheId = null; + this.ageGroup = null; + this.page = null; + this.params = {}; + this.queryString = ""; + this.queryType = "default"; + this.sort = ""; + this.tags = []; + return this; + } + + /** + * Returns an object representing the current state of the instance. + * The object contains COPIES of the current param values. + * @returns {Object} + */ + getState() { + return { + conditions: this.conditions, + minAge: this.minAge, + maxAge: this.maxAge, + gender: this.gender, + limit: this.limit, + offset: this.offset, + cacheId: this.cacheId, + page: this.page, + ageGroup: this.ageGroup, + params: { ...this.params }, + queryString: this.queryString, + queryType: this.queryType, + sort: this.sort, + tags: [...this.tags], + }; + } + + /** + * Compiles and returns the query string that can be send to the Patient + * endpoint. + * @return {String} The compiled query string (without the "?" in front) + */ + compile(encode = true) { + let params = []; + + [ + // conditions + "minAge", + "maxAge", + "gender", + "limit", + "offset", + // "params", + "queryType", + "queryString", + "sort", //, + // tags + ].forEach((prop) => { + if (this.__scheduled__.hasOwnProperty(prop)) { + this[prop] = this.__scheduled__[prop]; + delete this.__scheduled__[prop]; + } + }); + + // Tags ---------------------------------------------------------------- + if (this.tags.length && !this.params._id) { + params.push({ name: "_tag", value: this.tags.join(",") }); } - /** - * Compiles and returns the query string that can be send to the Patient - * endpoint. - * @return {String} The compiled query string (without the "?" in front) - */ - compile(encode=true) { - let params = []; - - [ - // conditions - "minAge", - "maxAge", - "gender", - "limit", - "offset", - // "params", - "queryType", - "queryString", - "sort"//, - // tags - ].forEach(prop => { - if (this.__scheduled__.hasOwnProperty(prop)) { - this[prop] = this.__scheduled__[prop]; - delete this.__scheduled__[prop]; - } - }) - - // Tags ---------------------------------------------------------------- - if (this.tags.length && !this.params._id) { - params.push({ name: "_tag", value: this.tags.join(",") }); + // Advanced query ------------------------------------------------------ + if (this.queryType == "advanced") { + let str = this.queryString.trim(); + if (str) { + let _query = parseQueryString(str); + for (let name in _query) { + params.push({ name, value: _query[name] }); } + } + } - // Advanced query ------------------------------------------------------ - if (this.queryType == "advanced") { - let str = this.queryString.trim() - if (str) { - let _query = parseQueryString(str); - for (let name in _query) { - params.push({ name, value: _query[name] }); - } - } + // Default query ------------------------------------------------------- + else { + // Custom params --------------------------------------------------- + Object.keys(this.params).forEach((k) => { + if (String(this.params[k]).trim()) { + params.push({ + name: k, + value: this.params[k], + }); } - - // Default query ------------------------------------------------------- - else { - - // Custom params --------------------------------------------------- - Object.keys(this.params).forEach(k => { - if (String(this.params[k]).trim()) { - params.push({ - name : k, - value: this.params[k] - }); - } - }); - - // sort ------------------------------------------------------------ - if (this.sort) { - String(this.sort).split(",").forEach(token => { - if (token.indexOf("-") === 0) { - params.push({ - name : "_sort", - value: token - }) - } - else { - params.push({ - name : "_sort", - value: token - }) - } - }) - // params.push({ - // name : "_sort", - // value: this.sort - // }) - } - - if (!this.params._id) { - - // Min age ----------------------------------------------------- - if (this.minAge) { - let d = moment().subtract( - this.minAge.value, - this.minAge.units - ); - params.push({ - name : "birthdate", - value: "le" + d.format('YYYY-MM-DD') - }); - } - - // Max age ----------------------------------------------------- - if (this.maxAge) { - let d = moment().subtract( - this.maxAge.value, - this.maxAge.units - ); - params.push({ - name : "birthdate", - value: "ge" + d.format('YYYY-MM-DD') - }); - } - - // exclude deceased patients if age is specified --------------- - if (this.maxAge || this.minAge) { - let existing = params.find(p => p.name === "deceased"); - if (existing) { - existing.value = false; - } - else { - params.push({ - name : "deceased", - value: false - }); - } - } - - // Gender ------------------------------------------------------ - if (this.gender) { - params.push({ - name : "gender", - value: this.gender - }); - } + }); + + // sort ------------------------------------------------------------ + if (this.sort) { + String(this.sort) + .split(",") + .forEach((token) => { + if (token.indexOf("-") === 0) { + params.push({ + name: "_sort", + value: token, + }); + } else { + params.push({ + name: "_sort", + value: token, + }); } + }); + // params.push({ + // name : "_sort", + // value: this.sort + // }) + } + + if (!this.params._id) { + // Min age ----------------------------------------------------- + if (this.minAge) { + let d = moment().subtract(this.minAge.value, this.minAge.units); + params.push({ + name: "birthdate", + value: "le" + d.format("YYYY-MM-DD"), + }); } - // limit --------------------------------------------------------------- - if (this.limit) { - params.push({ - name : "_count", - value: this.limit - }); + // Max age ----------------------------------------------------- + if (this.maxAge) { + let d = moment().subtract(this.maxAge.value, this.maxAge.units); + params.push({ + name: "birthdate", + value: "ge" + d.format("YYYY-MM-DD"), + }); } - // offset -------------------------------------------------------------- - if (this.offset && this.cacheId) { + // exclude deceased patients if age is specified --------------- + if (this.maxAge || this.minAge) { + let existing = params.find((p) => p.name === "deceased"); + if (existing) { + existing.value = false; + } else { params.push({ - name: "_getpages", - value: this.cacheId - }, { - name : "_getpagesoffset", - value: this.offset + name: "deceased", + value: false, }); + } } - // page offset ---------------------------------------------------------- - if (this.page) { - params.push({ - name: "_page", - value: this.page, - }); + // Gender ------------------------------------------------------ + if (this.gender) { + params.push({ + name: "gender", + value: this.gender, + }); } + } + } - // Compile and return -------------------------------------------------- - return params.map(p => ( - encode ? - encodeURIComponent(p.name) + "=" + encodeURIComponent(p.value) : - p.name + "=" + p.value - )).join("&"); + // limit --------------------------------------------------------------- + if (this.limit) { + params.push({ + name: "_count", + value: this.limit, + }); } - /** - * Checks if there are any conditions chosen at the moment - * @returns {Boolean} - */ - hasConditions() { - for (let key in this.conditions) { - if (this.conditions.hasOwnProperty(key)) { - return true; - } + // offset -------------------------------------------------------------- + if (this.offset && this.cacheId) { + params.push( + { + name: "_getpages", + value: this.cacheId, + }, + { + name: "_getpagesoffset", + value: this.offset, } - return false; + ); } - /** - * Compiles the current conditions into URL-encoded parameter list - * @returns {String} - */ - compileConditions() { - // let params = []; - - // for (let key in this.conditions) { - // let condition = this.conditions[key] - - // // system - // let value = []; - // for (let system in condition.codes) { - // let systemUrl = (CODE_SYSTEMS[system] || {}).url; - - // // system.code[n] - OR - // condition.codes[system].forEach(c => { - // value.push( systemUrl ? systemUrl + "|" + c : c ); - // }) - // } - - // if (value.length) { - // params.push({ - // name : "code", - // value: value.join(",") - // }) - // } - // } - - // return params.map(p => ( - // encodeURIComponent(p.name) + "=" + encodeURIComponent(p.value) - // )).join("&"); - let out = [] - for (let key in this.conditions) { - let condition = this.conditions[key] - - // system - let value = []; - for (let system in condition.codes) { - let systemUrl = (CODE_SYSTEMS[system] || {}).url || "http://snomed.info/sct"; - - // system.code[n] - OR - condition.codes[system].forEach(c => { - value.push(systemUrl + "|" + c); - }) - } + // page offset ---------------------------------------------------------- + if (this.page) { + params.push({ + name: "_page", + value: this.page, + }); + } - if (value.length) { - out.push(value.join(",")); - } - } - return out.length ? "code=" + encodeURIComponent(out.join(",")) : ""; + // Compile and return -------------------------------------------------- + return params + .map((p) => + encode + ? encodeURIComponent(p.name) + "=" + encodeURIComponent(p.value) + : p.name + "=" + p.value + ) + .join("&"); + } + + /** + * Checks if there are any conditions chosen at the moment + * @returns {Boolean} + */ + hasConditions() { + for (let key in this.conditions) { + if (this.conditions.hasOwnProperty(key)) { + return true; + } } + return false; + } + + /** + * Compiles the current conditions into URL-encoded parameter list + * @returns {String} + */ + compileConditions() { + // let params = []; + + // for (let key in this.conditions) { + // let condition = this.conditions[key] + + // // system + // let value = []; + // for (let system in condition.codes) { + // let systemUrl = (CODE_SYSTEMS[system] || {}).url; + + // // system.code[n] - OR + // condition.codes[system].forEach(c => { + // value.push( systemUrl ? systemUrl + "|" + c : c ); + // }) + // } + + // if (value.length) { + // params.push({ + // name : "code", + // value: value.join(",") + // }) + // } + // } + + // return params.map(p => ( + // encodeURIComponent(p.name) + "=" + encodeURIComponent(p.value) + // )).join("&"); + let out = []; + for (let key in this.conditions) { + let condition = this.conditions[key]; + + // system + let value = []; + for (let system in condition.codes) { + let systemUrl = + (CODE_SYSTEMS[system] || {}).url || "http://snomed.info/sct"; + + // system.code[n] - OR + condition.codes[system].forEach((c) => { + value.push(systemUrl + "|" + c); + }); + } - getConditionKeys() { - let out = [] - for (let key in this.conditions) { - let condition = this.conditions[key] + if (value.length) { + out.push(value.join(",")); + } + } + return out.length ? "code=" + encodeURIComponent(out.join(",")) : ""; + } + + getConditionKeys() { + let out = []; + for (let key in this.conditions) { + let condition = this.conditions[key]; + + // system + let value = []; + for (let system in condition.codes) { + let systemUrl = + (CODE_SYSTEMS[system] || {}).url || "http://snomed.info/sct"; + + // system.code[n] - OR + condition.codes[system].forEach((c) => { + value.push(systemUrl + "|" + c); + }); + } - // system - let value = []; - for (let system in condition.codes) { - let systemUrl = (CODE_SYSTEMS[system] || {}).url || "http://snomed.info/sct"; + if (value.length) { + out.push(value.join(",")); + } + } + return out; + } + + /** + * Returns a promise resolved with a list of patient IDs that have the + * specified condition(s) + * @param {String} baseURL + * @returns {Promise} + */ + getPatientIDs(server) { + if (this.__cache__.patientIDs) { + return Promise.resolve(this.__cache__.patientIDs); + } - // system.code[n] - OR - condition.codes[system].forEach(c => { - value.push(systemUrl + "|" + c); - }) - } + let conditions = this.compileConditions(); - if (value.length) { - out.push(value.join(",")); - } - } - return out; + if (!conditions) { + return Promise.resolve([]); } /** - * Returns a promise resolved with a list of patient IDs that have the - * specified condition(s) - * @param {String} baseURL - * @returns {Promise} + * The keys (eg: "http://snomed.info/sct|44054006") that were set by the + * user. + * @type {Array} + * @private */ - getPatientIDs(server) { - - if (this.__cache__.patientIDs) { - return Promise.resolve(this.__cache__.patientIDs); - } - - let conditions = this.compileConditions(); + let conditionKeys = this.getConditionKeys(); - if (!conditions) { - return Promise.resolve([]); - } + /** + * Map of patient IDs as keys and array of condition keys as values. + * @private + */ + let patientIDs = {}; - /** - * The keys (eg: "http://snomed.info/sct|44054006") that were set by the - * user. - * @type {Array} - * @private - */ - let conditionKeys = this.getConditionKeys(); - - /** - * Map of patient IDs as keys and array of condition keys as values. - * @private - */ - let patientIDs = {}; - - /** - * Handles the JSON response (single page) of the conditions query. - * Collects the patient IDs and their condition codes into the - * patientIDs local variable. When all the pages are fetched, cleans up - * the IDs to only contain those that have all the conditions specified - * by the user. - * @param {Object} response The JSON Conditions bundle response - * @returns {Promise} Array of patient ID strings (can be empty) - */ - const handleConditionsResponse = response => { - - // Collect the data - if (response.entry) { - response.entry.forEach(condition => { - let patientID = server.type == "DSTU-2" ? - condition.resource.patient.reference.split("/").pop(): - condition.resource.subject.reference.split("/").pop(); - if (!patientIDs[patientID]) { - patientIDs[patientID] = []; - } - patientIDs[patientID].push( - (getPath(condition, "resource.code.coding.0.system") || "http://snomed.info/sct") - + "|" + getPath(condition, "resource.code.coding.0.code") - ); - }); - - let nextLink = (response.link || []).find(l => l.relation == "next"); - if (nextLink) { - return request({ url: nextLink.url }).then(handleConditionsResponse); - } - } - // console.log(conditionKeys, patientIDs) - // Clean up and only leave patients having all the conditions - patientIDs = Object.keys(patientIDs).filter(key => { - return conditionKeys.every( - conditionKey => patientIDs[key].indexOf(conditionKey) > -1 - ); - }); - // console.log(patientIDs) + /** + * Handles the JSON response (single page) of the conditions query. + * Collects the patient IDs and their condition codes into the + * patientIDs local variable. When all the pages are fetched, cleans up + * the IDs to only contain those that have all the conditions specified + * by the user. + * @param {Object} response The JSON Conditions bundle response + * @returns {Promise} Array of patient ID strings (can be empty) + */ + const handleConditionsResponse = (response) => { + // Collect the data + if (response.entry) { + response.entry.forEach((condition) => { + let patientID = + server.type == "DSTU-2" + ? condition.resource.patient.reference.split("/").pop() + : condition.resource.subject.reference.split("/").pop(); + if (!patientIDs[patientID]) { + patientIDs[patientID] = []; + } + patientIDs[patientID].push( + (getPath(condition, "resource.code.coding.0.system") || + "http://snomed.info/sct") + + "|" + + getPath(condition, "resource.code.coding.0.code") + ); + }); - // finally return a promise resolved with the compiled ID array - return Promise.resolve(patientIDs); + let nextLink = (response.link || []).find((l) => l.relation == "next"); + if (nextLink) { + return request({ url: nextLink.url }).then(handleConditionsResponse); } - - // The conditions to search for - let params = [conditions]; - - // only need the patient - skip the rest to reduce the response - params.push( - server.type == "DSTU-2" ? - "_elements=patient,code" : - "_elements=subject,code" + } + // console.log(conditionKeys, patientIDs) + // Clean up and only leave patients having all the conditions + patientIDs = Object.keys(patientIDs).filter((key) => { + return conditionKeys.every( + (conditionKey) => patientIDs[key].indexOf(conditionKey) > -1 ); - - // Set bigger limit here to reduce the chance of having to - // make other queries to fetch subsequent pages - params.push("_count=500"); - - // Tags (not currently available in STU2) - if (this.tags.length) { - params.push( "_tag=" + encodeURIComponent(this.tags.join(",")) ); - } - - return request({ - url: `${server.url}/Condition?${params.join("&")}` - }) - .then(handleConditionsResponse) - .then(ids => { - this.__cache__.patientIDs = ids; - return ids; - }); + }); + // console.log(patientIDs) + + // finally return a promise resolved with the compiled ID array + return Promise.resolve(patientIDs); + }; + + // The conditions to search for + let params = [conditions]; + + // only need the patient - skip the rest to reduce the response + params.push( + server.type == "DSTU-2" + ? "_elements=patient,code" + : "_elements=subject,code" + ); + + // Set bigger limit here to reduce the chance of having to + // make other queries to fetch subsequent pages + params.push("_count=500"); + + // Tags (not currently available in STU2) + if (this.tags.length) { + params.push("_tag=" + encodeURIComponent(this.tags.join(","))); } - /** - * Fetches the patients matching the user-defined conditions. The actual - * strategy may vary but regardless of the implementation, a promise is - * returned that should eventually be resolved with the result bundle. - * @param {String} baseURL - * @returns {Promise} - */ - fetch(server) { - - let data = this.compile() + return request({ + url: `${server.url}/Condition?${params.join("&")}`, + }) + .then(handleConditionsResponse) + .then((ids) => { + this.__cache__.patientIDs = ids; + return ids; + }); + } + + /** + * Fetches the patients matching the user-defined conditions. The actual + * strategy may vary but regardless of the implementation, a promise is + * returned that should eventually be resolved with the result bundle. + * @param {String} baseURL + * @returns {Promise} + */ + fetch(server) { + let data = this.compile(); + + // STU2 does not work with the deceased param + if (server.type == "DSTU-2") { + data = data.replace(/\bdeceased=(true|false)\b/gi, ""); + } - // STU2 does not work with the deceased param - if (server.type == "DSTU-2") { - data = data.replace(/\bdeceased=(true|false)\b/gi, ""); + // prepare the base options for the patient ajax request + let options = { + url: `${server.url}/Patient/_search`, + method: "POST", + processData: false, + data, + headers: { + accept: "application/fhir+json", + "content-type": "application/x-www-form-urlencoded", + }, + }; + + return this.getPatientIDs(server) + .then((ids) => { + if (ids.length) { + // if IDs were found - add them to the patient query + options.data = [ + options.data, + "_id=" + encodeURIComponent(ids.join(",")), + ] + .filter(Boolean) + .join("&"); + } else { + // If conditions were specified but no patients were found to + // have those conditions, then we should exit early. + if (this.hasConditions()) { + return Promise.reject( + "No patients found with the specified conditions!" + ); + } } - - // prepare the base options for the patient ajax request - let options = { - url: `${server.url}/Patient/_search`, - method: "POST", - processData: false, - data, - headers: { - accept: "application/fhir+json", - "content-type": "application/x-www-form-urlencoded" - } - }; - - return this.getPatientIDs(server) - .then(ids => { - if (ids.length) { - // if IDs were found - add them to the patient query - options.data = [ - options.data, - "_id=" + encodeURIComponent(ids.join(",")) - ].filter(Boolean).join("&"); - } - else { - // If conditions were specified but no patients were found to - // have those conditions, then we should exit early. - if (this.hasConditions()) { - return Promise.reject( - "No patients found with the specified conditions!" - ); - } - } - return options; - }) - .then(request); - } + return options; + }) + .then(request); + } }