diff --git a/.gitignore b/.gitignore index c69cf0909..36c542824 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,5 @@ dist/* # projections file to easily jump to/from test file .projections.json -# Jetbrains ide +# Jetbrains ide .idea \ No newline at end of file diff --git a/.pnp.cjs b/.pnp.cjs index 09136ec06..8f9e6ce74 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -38,6 +38,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@graphql-tools/apollo-engine-loader", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:7.3.26"],\ ["@graphql-typed-document-node/core", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:3.1.2"],\ ["@mailchimp/mailchimp_transactional", "npm:1.0.50"],\ + ["@metriport/api", "npm:3.1.4"],\ ["@types/jest", "npm:29.4.0"],\ ["@types/lodash", "npm:4.14.191"],\ ["@types/mailchimp__mailchimp_transactional", "npm:1.0.5"],\ @@ -47,6 +48,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@typescript-eslint/eslint-plugin", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:5.52.0"],\ ["axios", "npm:1.3.4"],\ ["date-fns", "npm:2.29.3"],\ + ["dayjs", "npm:1.11.7"],\ + ["driver-license-validator", "npm:3.1.1"],\ ["eslint", "npm:8.34.0"],\ ["eslint-config-prettier", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:8.6.0"],\ ["eslint-config-standard-with-typescript", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:34.0.0"],\ @@ -157,6 +160,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@graphql-tools/apollo-engine-loader", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:7.3.26"],\ ["@graphql-typed-document-node/core", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:3.1.2"],\ ["@mailchimp/mailchimp_transactional", "npm:1.0.50"],\ + ["@metriport/api", "npm:3.1.4"],\ ["@types/jest", "npm:29.4.0"],\ ["@types/lodash", "npm:4.14.191"],\ ["@types/mailchimp__mailchimp_transactional", "npm:1.0.5"],\ @@ -166,6 +170,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@typescript-eslint/eslint-plugin", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:5.52.0"],\ ["axios", "npm:1.3.4"],\ ["date-fns", "npm:2.29.3"],\ + ["dayjs", "npm:1.11.7"],\ + ["driver-license-validator", "npm:3.1.1"],\ ["eslint", "npm:8.34.0"],\ ["eslint-config-prettier", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:8.6.0"],\ ["eslint-config-standard-with-typescript", "virtual:d0a76b174a54d973a27fb74b962aa2f26df55e18a188b76dd96ab65ec80e4a0b001d63550589d8a4f2f542a16ec7ef50e51453b019e1529d4f43d2542f174b98#npm:34.0.0"],\ @@ -3405,6 +3411,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@metriport/api", [\ + ["npm:3.1.4", {\ + "packageLocation": "./.yarn/cache/@metriport-api-npm-3.1.4-cf49b64e5d-39f6623d3d.zip/node_modules/@metriport/api/",\ + "packageDependencies": [\ + ["@metriport/api", "npm:3.1.4"],\ + ["axios", "npm:1.3.4"],\ + ["zod", "npm:3.21.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@nodelib/fs.scandir", [\ ["npm:2.1.5", {\ "packageLocation": "./.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-a970d595bd.zip/node_modules/@nodelib/fs.scandir/",\ @@ -6113,6 +6130,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["driver-license-validator", [\ + ["npm:3.1.1", {\ + "packageLocation": "./.yarn/cache/driver-license-validator-npm-3.1.1-40c37f982a-7d7ca31990.zip/node_modules/driver-license-validator/",\ + "packageDependencies": [\ + ["driver-license-validator", "npm:3.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["dset", [\ ["npm:3.1.2", {\ "packageLocation": "./.yarn/cache/dset-npm-3.1.2-c711fbe49b-4f8066f517.zip/node_modules/dset/",\ diff --git a/.yarn/cache/@metriport-api-npm-3.1.4-cf49b64e5d-39f6623d3d.zip b/.yarn/cache/@metriport-api-npm-3.1.4-cf49b64e5d-39f6623d3d.zip new file mode 100644 index 000000000..ff56eba1f Binary files /dev/null and b/.yarn/cache/@metriport-api-npm-3.1.4-cf49b64e5d-39f6623d3d.zip differ diff --git a/.yarn/cache/driver-license-validator-npm-3.1.1-40c37f982a-7d7ca31990.zip b/.yarn/cache/driver-license-validator-npm-3.1.1-40c37f982a-7d7ca31990.zip new file mode 100644 index 000000000..d081c4291 Binary files /dev/null and b/.yarn/cache/driver-license-validator-npm-3.1.1-40c37f982a-7d7ca31990.zip differ diff --git a/.yarnrc.yml b/.yarnrc.yml index 86ab42388..207f01a83 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,5 +1,5 @@ plugins: - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - spec: "@yarnpkg/plugin-interactive-tools" + spec: '@yarnpkg/plugin-interactive-tools' yarnPath: .yarn/releases/yarn-3.4.1.cjs diff --git a/extensions/index.ts b/extensions/index.ts index 4f5faedd7..06a1f0937 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -1,31 +1,33 @@ +// import { AvaAi } from './avaAi' import { Awell } from './awell' import { CalDotCom } from './calDotCom' -import { Healthie } from './healthie' -import { HelloWorld } from './hello-world' -import { Twilio } from './twilio' +import { Cloudinary } from './cloudinary' import { DropboxSign } from './dropboxSign' import { Elation } from './elation' -import { MessageBird } from './messagebird' -import { MathExtension } from './math' -import { Mailgun } from './mailgun' import { Formsort } from './formsort' -// import { AvaAi } from './avaAi' +import { Healthie } from './healthie' +import { HelloWorld } from './hello-world' import { Mailchimp } from './mailchimp' -import { Cloudinary } from './cloudinary' +import { Mailgun } from './mailgun' +import { MathExtension } from './math' +import { MessageBird } from './messagebird' +import { Metriport } from './metriport' +import { Twilio } from './twilio' export const extensions = [ // AvaAi, Best to disable this until we cleared out data privacy & HIPAA with OpenAI Awell, - Cloudinary, - HelloWorld, - Healthie, - Twilio, CalDotCom, + Cloudinary, DropboxSign, Elation, - Mailgun, - MessageBird, - MathExtension, Formsort, + Healthie, + HelloWorld, Mailchimp, + Mailgun, + MathExtension, + MessageBird, + Metriport, + Twilio, ] diff --git a/extensions/metriport/CHANGELOG.md b/extensions/metriport/CHANGELOG.md new file mode 100644 index 000000000..ef8e98770 --- /dev/null +++ b/extensions/metriport/CHANGELOG.md @@ -0,0 +1 @@ +# Metriport changelog \ No newline at end of file diff --git a/extensions/metriport/README.md b/extensions/metriport/README.md new file mode 100644 index 000000000..4ea7881c7 --- /dev/null +++ b/extensions/metriport/README.md @@ -0,0 +1,3 @@ +# Metriport extension + +(TBD requires more info) \ No newline at end of file diff --git a/extensions/metriport/actions/document/fields.ts b/extensions/metriport/actions/document/fields.ts new file mode 100644 index 000000000..f806b3604 --- /dev/null +++ b/extensions/metriport/actions/document/fields.ts @@ -0,0 +1,28 @@ +import { FieldType, type Field } from '../../../../lib/types' + +export const listFields = { + patientId: { + id: 'patientId', + label: 'Patient ID', + description: 'The ID of the Patient for which to list their available Documents', + type: FieldType.STRING, + required: true, + }, + facilityId: { + id: 'facilityId', + label: 'Facility ID', + description: 'The ID of the Facility where the patient is receiving care', + type: FieldType.STRING, + required: true, + }, +} satisfies Record + +export const getUrlFields = { + fileName: { + id: 'fileName', + label: 'File Name', + description: 'The file name of the document', + type: FieldType.STRING, + required: true, + }, +} satisfies Record diff --git a/extensions/metriport/actions/document/list.ts b/extensions/metriport/actions/document/list.ts new file mode 100644 index 000000000..457c2aa42 --- /dev/null +++ b/extensions/metriport/actions/document/list.ts @@ -0,0 +1,46 @@ +import { type Action, type DataPointDefinition } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { listFields } from './fields' +import { startQuerySchema } from './validation' + +const dataPoints = { + patientId: { + key: 'patientId', + valueType: 'string', + }, +} satisfies Record + +export const queryDocs: Action< + typeof listFields, + typeof settings, + keyof typeof dataPoints +> = { + key: 'listDocs', + category: Category.EHR_INTEGRATIONS, + title: 'List Documents', + description: + 'Triggers a document query for the specified patient across HIEs.', + fields: listFields, + previewable: true, + dataPoints, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const { patientId, facilityId } = startQuerySchema.parse(payload.fields) + + const api = createMetriportApi(payload.settings) + + await api.startDocumentQuery(patientId, facilityId) + + await onComplete({ + data_points: { + patientId: String(patientId), + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/document/query.ts b/extensions/metriport/actions/document/query.ts new file mode 100644 index 000000000..4c5281815 --- /dev/null +++ b/extensions/metriport/actions/document/query.ts @@ -0,0 +1,46 @@ +import { type Action, type DataPointDefinition } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { listFields } from './fields' +import { startQuerySchema } from './validation' + +const dataPoints = { + patientId: { + key: 'patientId', + valueType: 'string', + }, +} satisfies Record + +export const queryDocs: Action< + typeof listFields, + typeof settings, + keyof typeof dataPoints +> = { + key: 'queryDocs', + category: Category.EHR_INTEGRATIONS, + title: 'Start Document Query', + description: + 'Triggers a document query for the specified patient across HIEs.', + fields: listFields, + previewable: true, + dataPoints, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const { patientId, facilityId } = startQuerySchema.parse(payload.fields) + + const api = createMetriportApi(payload.settings) + + await api.startDocumentQuery(patientId, facilityId) + + await onComplete({ + data_points: { + patientId: String(patientId), + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/document/validation.ts b/extensions/metriport/actions/document/validation.ts new file mode 100644 index 000000000..09bbbf5ff --- /dev/null +++ b/extensions/metriport/actions/document/validation.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' + +export const startQuerySchema = z.object({ + patientId: z + .string({ errorMap: () => ({ message: 'Missing patientId' }) }) + .min(1), + facilityId: z + .string({ errorMap: () => ({ message: 'Missing facilityId' }) }) + .min(1), +}) diff --git a/extensions/metriport/actions/facility/create.ts b/extensions/metriport/actions/facility/create.ts new file mode 100644 index 000000000..bebcf0cf6 --- /dev/null +++ b/extensions/metriport/actions/facility/create.ts @@ -0,0 +1,54 @@ +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { facilityFields } from './fields' +import { facilityIdDataPoint } from './dataPoints' +import { facilityCreateSchema } from './validation' + +export const createFacility: Action< + typeof facilityFields, + typeof settings, + keyof typeof facilityIdDataPoint +> = { + key: 'createFacility', + category: Category.EHR_INTEGRATIONS, + title: 'Create Facility', + description: + 'Creates a Facility in Metriport where your patients receive care.', + fields: facilityFields, + previewable: true, + dataPoints: facilityIdDataPoint, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const facility = facilityCreateSchema.parse(payload.fields) + + const metriportFacility = { + name: facility.name, + npi: facility.npi, + tin: facility.tin, + address: { + addressLine1: facility.addressLine1, + addressLine2: facility.addressLine2, + city: facility.city, + state: facility.state, + zip: facility.zip, + country: facility.country, + }, + } + + const api = createMetriportApi(payload.settings) + + const { id } = await api.createFacility(metriportFacility) + + await onComplete({ + data_points: { + facilityId: String(id), + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/facility/dataPoints.ts b/extensions/metriport/actions/facility/dataPoints.ts new file mode 100644 index 000000000..b9ea75927 --- /dev/null +++ b/extensions/metriport/actions/facility/dataPoints.ts @@ -0,0 +1,29 @@ +import { type DataPointDefinition } from '../../../../lib/types' +import { address } from '../../shared/dataPoints' + +export const facilityIdDataPoint = { + facilityId: { + key: 'facilityId', + valueType: 'string', + }, +} satisfies Record + +export const facilityDataPoints = { + name: { + key: 'name', + valueType: 'string', + }, + npi: { + key: 'npi', + valueType: 'string', + }, + active: { + key: 'active', + valueType: 'boolean', + }, + tin: { + key: 'tin', + valueType: 'string', + }, + ...address, +} satisfies Record diff --git a/extensions/metriport/actions/facility/fields.ts b/extensions/metriport/actions/facility/fields.ts new file mode 100644 index 000000000..372d3b150 --- /dev/null +++ b/extensions/metriport/actions/facility/fields.ts @@ -0,0 +1,57 @@ +import { FieldType, type Field } from '../../../../lib/types' +import { address } from '../../shared/fields' + +export const facilityFields = { + name: { + id: 'name', + label: 'Name', + description: 'The name of your organization', + type: FieldType.STRING, + required: true, + }, + npi: { + id: 'npi', + label: 'National Provider Identifier (NPI)', + description: + 'The 10 digit National Provider Identifier (NPI) that will be used to make requests on behalf of the Facility', + type: FieldType.NUMERIC, + required: true, + }, + tin: { + id: 'tin', + label: 'Tax Identification Number (TIN)', + description: + 'The 10 digit National Provider Identifier (NPI) that will be used to make requests on behalf of the Facility', + type: FieldType.NUMERIC, + required: true, + }, + active: { + id: 'active', + label: 'Active', + description: + 'Whether or not this Facility is currently active - this is usually true.', + type: FieldType.BOOLEAN, + }, + ...address, +} satisfies Record + +export const facilityWithIdFields = { + id: { + id: 'id', + label: 'Facility ID', + description: 'The ID of the facility to update', + type: FieldType.STRING, + required: true, + }, + ...facilityFields, +} satisfies Record + +export const getFields = { + facilityId: { + id: 'facilityId', + label: 'Facility ID', + description: 'The facility ID', + type: FieldType.STRING, + required: true, + }, +} satisfies Record diff --git a/extensions/metriport/actions/facility/get.ts b/extensions/metriport/actions/facility/get.ts new file mode 100644 index 000000000..45816185c --- /dev/null +++ b/extensions/metriport/actions/facility/get.ts @@ -0,0 +1,47 @@ +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { getFields } from './fields' +import { stringId } from '../../validation/generic.zod' +import { facilityDataPoints } from './dataPoints' + +export const getFacility: Action< + typeof getFields, + typeof settings, + keyof typeof facilityDataPoints +> = { + key: 'getFacility', + category: Category.EHR_INTEGRATIONS, + title: 'Get Facility', + description: 'Gets a Facility', + fields: getFields, + previewable: true, + dataPoints: facilityDataPoints, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const facilityId = stringId.parse(payload.fields.facilityId) + + const api = createMetriportApi(payload.settings) + const facility = await api.getFacility(facilityId) + + await onComplete({ + data_points: { + name: facility.name, + tin: facility.tin, + npi: facility.npi, + active: String(facility.active), + addressLine1: facility.address.addressLine1, + addressLine2: facility.address.addressLine2, + city: facility.address.city, + state: facility.address.state, + zip: facility.address.zip, + country: facility.address.country, + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/facility/update.ts b/extensions/metriport/actions/facility/update.ts new file mode 100644 index 000000000..1131f94ed --- /dev/null +++ b/extensions/metriport/actions/facility/update.ts @@ -0,0 +1,48 @@ +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { facilityWithIdFields } from './fields' +import { facilityUpdateSchema } from './validation' + +export const updateFacility: Action< + typeof facilityWithIdFields, + typeof settings +> = { + key: 'updateFacility', + category: Category.EHR_INTEGRATIONS, + title: 'Update Facility', + description: + 'Updates a Facility in Metriport where your patients receive care.', + fields: facilityWithIdFields, + previewable: true, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const facility = facilityUpdateSchema.parse(payload.fields) + + const metriportFacility = { + id: facility.id, + name: facility.name, + tin: facility.tin, + npi: facility.npi, + address: { + addressLine1: facility.addressLine1, + addressLine2: facility.addressLine2, + city: facility.city, + state: facility.state, + zip: facility.zip, + country: facility.country, + }, + } + + const api = createMetriportApi(payload.settings) + + await api.updateFacility(metriportFacility) + + await onComplete() + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/facility/validation.ts b/extensions/metriport/actions/facility/validation.ts new file mode 100644 index 000000000..edd3e2f7e --- /dev/null +++ b/extensions/metriport/actions/facility/validation.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' +import { addressSchema } from '@metriport/api' + +export const facilityCreateSchema = z + .object({ + name: z.string().min(1), + npi: z.string().min(1), + tin: z.string().optional().nullable(), + active: z.boolean().optional().nullable(), + }) + .merge(addressSchema) + +export const facilityUpdateSchema = z + .object({ + id: z.string().min(1), + }) + .merge(facilityCreateSchema) diff --git a/extensions/metriport/actions/index.ts b/extensions/metriport/actions/index.ts new file mode 100644 index 000000000..e18e15fb5 --- /dev/null +++ b/extensions/metriport/actions/index.ts @@ -0,0 +1,15 @@ +import { createOrganization } from './organization/create' +import { updateOrganization } from './organization/update' +import { getOrganization } from './organization/get' +import { createFacility } from './facility/create' +import { updateFacility } from './facility/update' +import { getFacility } from './facility/get' + +export const actions = { + createOrganization, + updateOrganization, + getOrganization, + createFacility, + updateFacility, + getFacility, +} diff --git a/extensions/metriport/actions/link/create.ts b/extensions/metriport/actions/link/create.ts new file mode 100644 index 000000000..40b8096d8 --- /dev/null +++ b/extensions/metriport/actions/link/create.ts @@ -0,0 +1,46 @@ +import { type Action, type DataPointDefinition } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { createFields } from './fields' +import { linkCreateSchema } from './validation' + +const dataPoints = { + patientId: { + key: 'patientId', + valueType: 'string', + }, +} satisfies Record + +export const createLink: Action< + typeof createFields, + typeof settings, + keyof typeof dataPoints +> = { + key: 'createLink', + category: Category.EHR_INTEGRATIONS, + title: 'Create Link', + description: "Create an link using Metriport's API.", + fields: createFields, + previewable: true, + dataPoints, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const { patientId, linkSource, facilityId, entityId } = + linkCreateSchema.parse(payload.fields) + + const api = createMetriportApi(payload.settings) + + await api.createLink(patientId, facilityId, entityId, linkSource) + + await onComplete({ + data_points: { + patientId: String(patientId), + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/link/fields.ts b/extensions/metriport/actions/link/fields.ts new file mode 100644 index 000000000..d15807d7a --- /dev/null +++ b/extensions/metriport/actions/link/fields.ts @@ -0,0 +1,42 @@ +import { FieldType, type Field } from '../../../../lib/types' + +export const getAllFields = { + patientId: { + id: 'patientId', + label: 'Patient ID', + description: + 'The ID of the Patient that will be linked to the entity in the HIE', + type: FieldType.STRING, + required: true, + }, + facilityId: { + id: 'facilityId', + label: 'Facility ID', + description: + 'The ID of the Facility that is currently providing the Patient care', + type: FieldType.STRING, + required: true, + }, +} satisfies Record + +export const removeFields = { + ...getAllFields, + linkSource: { + id: 'linkSource', + label: 'Link Source', + description: 'The HIE to link to - currently COMMONWELL is supported', + type: FieldType.STRING, + required: true, + }, +} satisfies Record + +export const createFields = { + ...removeFields, + entityId: { + id: 'entityId', + label: 'Entity ID', + description: 'The ID of the entity in the HIE to link the Patient to', + type: FieldType.STRING, + required: true, + }, +} satisfies Record diff --git a/extensions/metriport/actions/link/getAll.ts b/extensions/metriport/actions/link/getAll.ts new file mode 100644 index 000000000..c5f42e4b8 --- /dev/null +++ b/extensions/metriport/actions/link/getAll.ts @@ -0,0 +1,46 @@ +import { type Action, type DataPointDefinition } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { getAllFields } from './fields' +import { getAllLinksSchema } from './validation' + +// NEED TO SEE WHILE TESTING +const dataPoints = { + patientId: { + key: 'patientId', + valueType: 'string', + }, +} satisfies Record + +export const getAllLinks: Action< + typeof getAllFields, + typeof settings, + keyof typeof dataPoints +> = { + key: 'getAllLinks', + category: Category.EHR_INTEGRATIONS, + title: 'Get All Links', + description: "Get all links using Metriport's API.", + fields: getAllFields, + previewable: true, + dataPoints, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const { patientId, facilityId } = getAllLinksSchema.parse(payload.fields) + + const api = createMetriportApi(payload.settings) + + await api.listLinks(patientId, facilityId) + + await onComplete({ + data_points: { + patientId: String(patientId), + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/link/remove.ts b/extensions/metriport/actions/link/remove.ts new file mode 100644 index 000000000..9158e6728 --- /dev/null +++ b/extensions/metriport/actions/link/remove.ts @@ -0,0 +1,47 @@ +import { type Action, type DataPointDefinition } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { removeFields } from './fields' +import { linkRemoveSchema } from './validation' + +const dataPoints = { + patientId: { + key: 'patientId', + valueType: 'string', + }, +} satisfies Record + +export const removeLink: Action< + typeof removeFields, + typeof settings, + keyof typeof dataPoints +> = { + key: 'removeLink', + category: Category.EHR_INTEGRATIONS, + title: 'Remove Link', + description: "Remove an link using Metriport's API.", + fields: removeFields, + previewable: true, + dataPoints, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const { patientId, linkSource, facilityId } = linkRemoveSchema.parse( + payload.fields + ) + + const api = createMetriportApi(payload.settings) + + await api.removeLink(patientId, facilityId, linkSource) + + await onComplete({ + data_points: { + patientId: String(patientId), + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/link/validation.ts b/extensions/metriport/actions/link/validation.ts new file mode 100644 index 000000000..725693926 --- /dev/null +++ b/extensions/metriport/actions/link/validation.ts @@ -0,0 +1,25 @@ +import { z } from 'zod' +import { MedicalDataSource } from '@metriport/api' + +export const getAllLinksSchema = z.object({ + patientId: z + .string({ errorMap: () => ({ message: 'Missing patientId' }) }) + .min(1), + facilityId: z + .string({ errorMap: () => ({ message: 'Missing facilityId' }) }) + .min(1), +}) + +export const linkRemoveSchema = z + .object({ + linkSource: z.nativeEnum(MedicalDataSource), + }) + .merge(getAllLinksSchema) + +export const linkCreateSchema = z + .object({ + entityId: z + .string({ errorMap: () => ({ message: 'Missing entityId' }) }) + .min(1), + }) + .merge(linkRemoveSchema) diff --git a/extensions/metriport/actions/organization/create.ts b/extensions/metriport/actions/organization/create.ts new file mode 100644 index 000000000..45b49901a --- /dev/null +++ b/extensions/metriport/actions/organization/create.ts @@ -0,0 +1,52 @@ +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { orgFields } from './fields' +import { orgIdDataPoint } from './dataPoints' +import { orgCreateSchema } from './validation' + +export const createOrganization: Action< + typeof orgFields, + typeof settings, + keyof typeof orgIdDataPoint +> = { + key: 'createOrganization', + category: Category.EHR_INTEGRATIONS, + title: 'Create Organization', + description: 'Registers your Organization in Metriport.', + fields: orgFields, + previewable: true, + dataPoints: orgIdDataPoint, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const organization = orgCreateSchema.parse(payload.fields) + + const metriportOrg = { + name: organization.name, + type: organization.type, + location: { + addressLine1: organization.addressLine1, + addressLine2: organization.addressLine2, + city: organization.city, + state: organization.state, + zip: organization.zip, + country: organization.country, + }, + } + + const api = createMetriportApi(payload.settings) + + const { id } = await api.createOrganization(metriportOrg) + + await onComplete({ + data_points: { + organizationId: String(id), + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/organization/dataPoints.ts b/extensions/metriport/actions/organization/dataPoints.ts new file mode 100644 index 000000000..e787f8257 --- /dev/null +++ b/extensions/metriport/actions/organization/dataPoints.ts @@ -0,0 +1,21 @@ +import { type DataPointDefinition } from '../../../../lib/types' +import { address } from '../../shared/dataPoints' + +export const orgIdDataPoint = { + organizationId: { + key: 'organizationId', + valueType: 'string', + }, +} satisfies Record + +export const orgDataPoints = { + type: { + key: 'type', + valueType: 'string', + }, + name: { + key: 'name', + valueType: 'string', + }, + ...address, +} satisfies Record diff --git a/extensions/metriport/actions/organization/fields.ts b/extensions/metriport/actions/organization/fields.ts new file mode 100644 index 000000000..4df9360b8 --- /dev/null +++ b/extensions/metriport/actions/organization/fields.ts @@ -0,0 +1,34 @@ +import { FieldType, type Field } from '../../../../lib/types' +import { address } from '../../shared/fields' + +export const orgFields = { + name: { + id: 'name', + label: 'Name', + description: 'The name of your organization', + type: FieldType.STRING, + required: true, + }, + type: { + id: 'type', + label: 'Type', + description: + 'The type of your organization, can be one of: acuteCare, ambulatory, hospital, labSystems, pharmacy, postAcuteCare', + type: FieldType.STRING, + required: true, + }, + ...address, +} satisfies Record + +export const orgWithIdFields = { + id: { + id: 'id', + label: 'Organization ID', + description: 'The ID of the organization to update', + type: FieldType.STRING, + required: true, + }, + ...orgFields, +} satisfies Record + +export const getFields = {} satisfies Record diff --git a/extensions/metriport/actions/organization/get.ts b/extensions/metriport/actions/organization/get.ts new file mode 100644 index 000000000..78aeaacb4 --- /dev/null +++ b/extensions/metriport/actions/organization/get.ts @@ -0,0 +1,46 @@ +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { type getFields } from './fields' +import { orgDataPoints } from './dataPoints' + +export const getOrganization: Action< + typeof getFields, + typeof settings, + keyof typeof orgDataPoints +> = { + key: 'getOrganization', + category: Category.EHR_INTEGRATIONS, + title: 'Get Organization', + description: 'Gets the Organization representing your legal corporate entity', + fields: {}, + previewable: true, + dataPoints: orgDataPoints, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const api = createMetriportApi(payload.settings) + const organization = await api.getOrganization() + + if (organization !== undefined) { + await onComplete({ + data_points: { + type: organization.type, + name: organization.name, + addressLine1: organization.location.addressLine1, + addressLine2: organization.location.addressLine2, + city: organization.location.city, + state: organization.location.state, + zip: organization.location.zip, + country: organization.location.country, + }, + }) + } + + throw new Error('Organization not found') + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/organization/update.ts b/extensions/metriport/actions/organization/update.ts new file mode 100644 index 000000000..a6316c8a9 --- /dev/null +++ b/extensions/metriport/actions/organization/update.ts @@ -0,0 +1,46 @@ +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { orgWithIdFields } from './fields' +import { orgUpdateSchema } from './validation' + +export const updateOrganization: Action< + typeof orgWithIdFields, + typeof settings +> = { + key: 'updateOrganization', + category: Category.EHR_INTEGRATIONS, + title: 'Update Organization', + description: "Updates your Organization's details.", + fields: orgWithIdFields, + previewable: true, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const organization = orgUpdateSchema.parse(payload.fields) + + const metriportOrg = { + id: organization.id, + name: organization.name, + type: organization.type, + location: { + addressLine1: organization.addressLine1, + addressLine2: organization.addressLine2, + city: organization.city, + state: organization.state, + zip: organization.zip, + country: organization.country, + }, + } + + const api = createMetriportApi(payload.settings) + + await api.updateOrganization(metriportOrg) + + await onComplete() + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/organization/validation.ts b/extensions/metriport/actions/organization/validation.ts new file mode 100644 index 000000000..09a9b769b --- /dev/null +++ b/extensions/metriport/actions/organization/validation.ts @@ -0,0 +1,15 @@ +import { z } from 'zod' +import { addressSchema, orgTypeSchema } from '@metriport/api' + +export const orgCreateSchema = z + .object({ + name: z.string().min(1), + type: orgTypeSchema, + }) + .merge(addressSchema) + +export const orgUpdateSchema = z + .object({ + id: z.string().min(1), + }) + .merge(orgCreateSchema) diff --git a/extensions/metriport/actions/patient/create.ts b/extensions/metriport/actions/patient/create.ts new file mode 100644 index 000000000..2778dfc2b --- /dev/null +++ b/extensions/metriport/actions/patient/create.ts @@ -0,0 +1,111 @@ +import { + type PatientCreate as MetriportPatientCreate, + usStateSchema, +} from '@metriport/api' +import { isValid } from 'driver-license-validator' +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { createFields } from './fields' +import { stringId } from '../../validation/generic.zod' +import { type PatientCreate, patientCreateSchema } from './validation' +import { patientIdDataPoint } from './dataPoints' + +export const createPatient: Action< + typeof createFields, + typeof settings, + keyof typeof patientIdDataPoint +> = { + key: 'createPatient', + category: Category.EHR_INTEGRATIONS, + title: 'Create Patient', + description: + 'Creates a Patient in Metriport for the specified Facility where the patient is receiving care.', + fields: createFields, + previewable: true, + dataPoints: patientIdDataPoint, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const patient = patientCreateSchema.parse(payload.fields) + + const facilityId = stringId.parse(payload.fields.facilityId) + + const patientMetriport = convertToMetriportPatient(patient) + + const api = createMetriportApi(payload.settings) + + const { id } = await api.createPatient(patientMetriport, facilityId) + + await onComplete({ + data_points: { + patientId: String(id), + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} + +export const convertToMetriportPatient = ( + patient: PatientCreate +): MetriportPatientCreate => { + const patientMetriport: MetriportPatientCreate = { + firstName: patient.firstName, + lastName: patient.lastName, + dob: patient.dob, + genderAtBirth: patient.genderAtBirth, + address: { + addressLine1: patient.addressLine1, + addressLine2: patient.addressLine2, + city: patient.city, + state: patient.state, + zip: patient.zip, + country: patient.country, + }, + personalIdentifiers: [], + contact: { + phone: patient.phone, + email: patient.email, + }, + } + + if ( + patient.driversLicenseState !== undefined && + patient.driversLicenseValue === undefined + ) { + throw new Error( + 'Drivers license value is required when drivers license state is provided' + ) + } else if ( + patient.driversLicenseState === undefined && + patient.driversLicenseValue !== undefined + ) { + throw new Error( + 'Drivers license state is required when drivers license value is provided' + ) + } + + if ( + patient.driversLicenseState !== undefined && + patient.driversLicenseState.length > 0 && + patient.driversLicenseValue !== undefined && + patient.driversLicenseValue.length > 0 + ) { + const valid = isValid(patient.driversLicenseValue, { + states: patient.driversLicenseState, + }) + + if (valid) { + ;(patientMetriport.personalIdentifiers ?? []).push({ + type: 'driversLicense', + value: patient.driversLicenseValue, + state: usStateSchema.parse(patient.driversLicenseState), + }) + } + } + + return patientMetriport +} diff --git a/extensions/metriport/actions/patient/dataPoints.ts b/extensions/metriport/actions/patient/dataPoints.ts new file mode 100644 index 000000000..91dfafb4c --- /dev/null +++ b/extensions/metriport/actions/patient/dataPoints.ts @@ -0,0 +1,45 @@ +import { type DataPointDefinition } from '../../../../lib/types' +import { address } from '../../shared/dataPoints' + +export const patientIdDataPoint = { + patientId: { + key: 'patientId', + valueType: 'string', + }, +} satisfies Record + +export const patientDataPoints = { + firstName: { + key: 'firstName', + valueType: 'string', + }, + lastName: { + key: 'lastName', + valueType: 'string', + }, + dob: { + key: 'dob', + valueType: 'date', + }, + genderAtBirth: { + key: 'genderAtBirth', + valueType: 'string', + }, + driversLicenseValue: { + key: 'driversLicenseValue', + valueType: 'string', + }, + driversLicenseState: { + key: 'driversLicenseState', + valueType: 'string', + }, + ...address, + phone: { + key: 'phone', + valueType: 'string', + }, + email: { + key: 'email', + valueType: 'string', + }, +} satisfies Record diff --git a/extensions/metriport/actions/patient/delete.ts b/extensions/metriport/actions/patient/delete.ts new file mode 100644 index 000000000..94cf5a4e5 --- /dev/null +++ b/extensions/metriport/actions/patient/delete.ts @@ -0,0 +1,29 @@ +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { stringId } from '../../validation/generic.zod' +import { deleteFields } from './fields' + +export const deletePatient: Action = { + key: 'deletePatient', + category: Category.EHR_INTEGRATIONS, + title: 'Delete Patient', + description: 'Removes the specified Patient.', + fields: deleteFields, + previewable: true, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const patientId = stringId.parse(payload.fields.patientId) + const facilityId = stringId.parse(payload.fields.facilityId) + + const api = createMetriportApi(payload.settings) + await api.deletePatient(patientId, facilityId) + + await onComplete() + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/patient/fields.ts b/extensions/metriport/actions/patient/fields.ts new file mode 100644 index 000000000..ce90c3aa2 --- /dev/null +++ b/extensions/metriport/actions/patient/fields.ts @@ -0,0 +1,97 @@ +import { FieldType, type Field } from '../../../../lib/types' +import { address } from '../../shared/fields' + +export const createFields = { + facilityId: { + id: 'facilityId', + label: 'Facility ID', + description: `The ID of the facility to create the Patient in`, + type: FieldType.STRING, + required: true, + }, + firstName: { + id: 'firstName', + label: 'First Name', + description: `The Patient's first name`, + type: FieldType.STRING, + required: true, + }, + lastName: { + id: 'lastName', + label: 'Last Name', + description: `The Patient's last name`, + type: FieldType.STRING, + required: true, + }, + dob: { + id: 'dob', + label: 'Date of Birth', + description: `The Patient's date of birth (DOB), formatted YYYY-MM-DD`, + type: FieldType.STRING, + required: true, + }, + genderAtBirth: { + id: 'genderAtBirth', + label: 'Gender at Birth', + description: `The Patient's gender at birth, can be one of M or F`, + type: FieldType.STRING, + required: true, + }, + driversLicenseValue: { + id: 'driversLicenseValue', + label: 'Drivers License Value', + description: `The Patient's driver's license number`, + type: FieldType.STRING, + }, + driversLicenseState: { + id: 'driversLicenseState', + label: 'Drivers License State', + description: `The 2 letter state acronym where this ID was issued, for example: CA`, + type: FieldType.STRING, + }, + ...address, + phone: { + id: 'phone', + label: 'Phone', + description: `The Patient's 10 digit phone number, formatted 1234567899`, + type: FieldType.NUMERIC, + }, + email: { + id: 'email', + label: 'Email', + description: `The Patient's email address`, + type: FieldType.STRING, + }, +} satisfies Record + +export const updateFields = { + id: { + id: 'id', + label: 'Patient ID', + description: 'The ID of the patient to update', + type: FieldType.STRING, + required: true, + }, + ...createFields, +} satisfies Record + +export const getFields = { + patientId: { + id: 'patientId', + label: 'Patient ID', + description: 'The patient ID', + type: FieldType.STRING, + required: true, + }, +} satisfies Record + +export const deleteFields = { + ...getFields, + facilityId: { + id: 'facilityId', + label: 'Facility ID', + description: 'The facility ID', + type: FieldType.STRING, + required: true, + }, +} diff --git a/extensions/metriport/actions/patient/get.ts b/extensions/metriport/actions/patient/get.ts new file mode 100644 index 000000000..fcd1fd34e --- /dev/null +++ b/extensions/metriport/actions/patient/get.ts @@ -0,0 +1,68 @@ +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { getFields } from './fields' +import { stringId } from '../../validation/generic.zod' +import { patientDataPoints } from './dataPoints' +import { isNil } from 'lodash' + +export const getPatient: Action< + typeof getFields, + typeof settings, + keyof typeof patientDataPoints +> = { + key: 'getPatient', + category: Category.EHR_INTEGRATIONS, + title: 'Get Patient', + description: 'Gets a Patient.', + fields: getFields, + previewable: true, + dataPoints: patientDataPoints, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const patientId = stringId.parse(payload.fields.patientId) + + const api = createMetriportApi(payload.settings) + const patient = await api.getPatient(patientId) + + if (isNil(patient.personalIdentifiers)) { + throw new Error('Patient does not have any personal identifiers.') + } + + if (Array.isArray(patient.address)) { + patient.address = patient.address[0] + } + + if (Array.isArray(patient.contact)) { + patient.contact = patient.contact[0] + } + + const driversLicense = patient.personalIdentifiers.find( + (id) => id.type === 'driversLicense' + ) + + await onComplete({ + data_points: { + firstName: patient.firstName, + lastName: patient.lastName, + dob: patient.dob, + genderAtBirth: patient.genderAtBirth, + driversLicenseValue: driversLicense?.value, + driversLicenseState: driversLicense?.state, + addressLine1: patient.address.addressLine1, + addressLine2: patient.address.addressLine2, + city: patient.address.city, + state: patient.address.state, + zip: patient.address.zip, + country: patient.address.country, + phone: patient.contact?.phone, + email: patient.contact?.email, + }, + }) + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/patient/update.ts b/extensions/metriport/actions/patient/update.ts new file mode 100644 index 000000000..dcd21fb33 --- /dev/null +++ b/extensions/metriport/actions/patient/update.ts @@ -0,0 +1,38 @@ +import { type Action } from '../../../../lib/types' +import { Category } from '../../../../lib/types/marketplace' +import { type settings } from '../../settings' +import { createMetriportApi } from '../../client' +import { handleErrorMessage } from '../../shared/errorHandler' +import { updateFields } from './fields' +import { stringId } from '../../validation/generic.zod' +import { patientUpdateSchema } from './validation' +import { convertToMetriportPatient } from './create' + +export const updatePatient: Action = { + key: 'updatePatient', + category: Category.EHR_INTEGRATIONS, + title: 'Update Patient', + description: 'Updates the specified Patient.', + fields: updateFields, + previewable: true, + onActivityCreated: async (payload, onComplete, onError): Promise => { + try { + const patient = patientUpdateSchema.parse(payload.fields) + + const facilityId = stringId.parse(payload.fields.facilityId) + + const metriportPatient = convertToMetriportPatient(patient) + + const api = createMetriportApi(payload.settings) + + await api.updatePatient( + { id: patient.id, ...metriportPatient }, + facilityId + ) + + await onComplete() + } catch (err) { + await handleErrorMessage(err, onError) + } + }, +} diff --git a/extensions/metriport/actions/patient/validation.ts b/extensions/metriport/actions/patient/validation.ts new file mode 100644 index 000000000..07a7a6d83 --- /dev/null +++ b/extensions/metriport/actions/patient/validation.ts @@ -0,0 +1,23 @@ +import * as z from 'zod' +import { addressSchema, genderAtBirthSchema } from '@metriport/api' + +export const patientCreateSchema = z + .object({ + firstName: z.string().min(1), + lastName: z.string().min(1), + dob: z.string().length(10), // YYYY-MM-DD + genderAtBirth: genderAtBirthSchema, + driversLicenseState: z.string().optional(), + driversLicenseValue: z.string().optional(), + phone: z.string().optional(), + email: z.string().email().optional(), + }) + .merge(addressSchema) + +export type PatientCreate = z.infer + +export const patientUpdateSchema = z + .object({ + id: z.string().min(1), + }) + .merge(patientCreateSchema) diff --git a/extensions/metriport/client.ts b/extensions/metriport/client.ts new file mode 100644 index 000000000..380bd431d --- /dev/null +++ b/extensions/metriport/client.ts @@ -0,0 +1,13 @@ +import { MetriportMedicalApi } from '@metriport/api' +import { type settings } from './settings' +import { settingsSchema } from './validation/settings.zod' + +export const createMetriportApi = ( + payloadSettings: Record +): MetriportMedicalApi => { + const { apiKey, baseUrl } = settingsSchema.parse(payloadSettings) + + return new MetriportMedicalApi(apiKey, { + baseAddress: baseUrl, + }) +} diff --git a/extensions/metriport/index.ts b/extensions/metriport/index.ts new file mode 100644 index 000000000..d1bbb8837 --- /dev/null +++ b/extensions/metriport/index.ts @@ -0,0 +1,24 @@ +import { actions } from './actions' +import { type Extension } from '../../lib/types' +import { settings } from './settings' +import { AuthorType, Category } from '../../lib/types/marketplace' +// import { webhooks } from './webhooks' + +export const Metriport: Extension = { + key: 'metriport', + title: 'Metriport', + description: + 'Metriport helps digital health companies access and manage health and medical data, through a single universal API.', + + // TODO: WE NEED JUST THE ICON NO NAME 60X60 + icon_url: + 'https://uploads-ssl.webflow.com/63e8455460afc21f779ddb79/63e845b3e00682d54cfd92fc_metriport-logo-p-500.png', + category: Category.EHR_INTEGRATIONS, + author: { + authorType: AuthorType.EXTERNAL, + authorName: 'Metriport', + }, + settings, + actions, + // webhooks, +} diff --git a/extensions/metriport/settings.ts b/extensions/metriport/settings.ts new file mode 100644 index 000000000..d143b72a6 --- /dev/null +++ b/extensions/metriport/settings.ts @@ -0,0 +1,18 @@ +import { type Setting } from '../../lib/types' + +export const settings = { + apiKey: { + key: 'api_key', + label: 'API Key', + obfuscated: true, + description: 'The API Key for the Metriport Medical API.', + required: true, + }, + baseUrl: { + key: 'base_url', + label: 'Base URL', + obfuscated: false, + description: 'The base URL of the Metriport Medical API.', + required: false, + }, +} satisfies Record diff --git a/extensions/metriport/shared/dataPoints.ts b/extensions/metriport/shared/dataPoints.ts new file mode 100644 index 000000000..348590813 --- /dev/null +++ b/extensions/metriport/shared/dataPoints.ts @@ -0,0 +1,28 @@ +import { type DataPointDefinition } from '../../../lib/types' + +export const address = { + addressLine1: { + key: 'addressLine1', + valueType: 'string', + }, + addressLine2: { + key: 'addressLine2', + valueType: 'string', + }, + city: { + key: 'city', + valueType: 'string', + }, + state: { + key: 'state', + valueType: 'string', + }, + zip: { + key: 'zip', + valueType: 'string', + }, + country: { + key: 'country', + valueType: 'string', + }, +} satisfies Record diff --git a/extensions/metriport/shared/errorHandler.ts b/extensions/metriport/shared/errorHandler.ts new file mode 100644 index 000000000..28d1124f8 --- /dev/null +++ b/extensions/metriport/shared/errorHandler.ts @@ -0,0 +1,56 @@ +import { fromZodError } from 'zod-validation-error' +import { ZodError } from 'zod' +import { AxiosError } from 'axios' +import { type OnErrorCallback } from '../../../lib/types' + +export const handleErrorMessage = async ( + err: any, + onError: OnErrorCallback +): Promise => { + if (err instanceof ZodError) { + const error = fromZodError(err) + await onError({ + events: [ + { + date: new Date().toISOString(), + text: { en: error.message }, + error: { + category: 'WRONG_INPUT', + message: error.message, + }, + }, + ], + }) + } else if (err instanceof AxiosError) { + await onError({ + events: [ + { + date: new Date().toISOString(), + text: { + en: `${err.status ?? '(no status code)'} Error: ${err.message}`, + }, + error: { + category: 'SERVER_ERROR', + message: `${err.status ?? '(no status code)'} Error: ${ + err.message + }`, + }, + }, + ], + }) + } else { + const message = (err as Error).message + await onError({ + events: [ + { + date: new Date().toISOString(), + text: { en: message }, + error: { + category: 'SERVER_ERROR', + message, + }, + }, + ], + }) + } +} diff --git a/extensions/metriport/shared/fields.ts b/extensions/metriport/shared/fields.ts new file mode 100644 index 000000000..aa9ff5231 --- /dev/null +++ b/extensions/metriport/shared/fields.ts @@ -0,0 +1,45 @@ +import { FieldType, type Field } from '../../../lib/types' + +export const address = { + addressLine1: { + id: 'addressLine1', + label: 'Address Line 1', + description: 'The address', + type: FieldType.STRING, + required: true, + }, + addressLine2: { + id: 'addressLine2', + label: 'Address Line 2', + description: 'The address details', + type: FieldType.STRING, + }, + city: { + id: 'city', + label: 'City', + description: 'The city', + type: FieldType.STRING, + required: true, + }, + state: { + id: 'state', + label: 'State', + description: 'The 2 letter state acronym, for example: CA', + type: FieldType.STRING, + required: true, + }, + zip: { + id: 'zip', + label: 'Zip', + description: '5 digit zip code', + type: FieldType.STRING, + required: true, + }, + country: { + id: 'country', + label: 'Country', + description: 'Must be “USA”', + type: FieldType.STRING, + required: true, + }, +} satisfies Record diff --git a/extensions/metriport/validation/generic.zod.ts b/extensions/metriport/validation/generic.zod.ts new file mode 100644 index 000000000..4965eac8d --- /dev/null +++ b/extensions/metriport/validation/generic.zod.ts @@ -0,0 +1,9 @@ +import { z } from 'zod' + +/** + * stringId is a REQUIRED field, so please use a z.coerce...optional() for non- + * required numbers + */ +export const stringId = z.coerce.string({ + invalid_type_error: 'Requires a valid ID', +}) diff --git a/extensions/metriport/validation/settings.zod.ts b/extensions/metriport/validation/settings.zod.ts new file mode 100644 index 000000000..ca0522965 --- /dev/null +++ b/extensions/metriport/validation/settings.zod.ts @@ -0,0 +1,9 @@ +import { z } from 'zod' + +export const settingsSchema = z.object({ + baseUrl: z + .string({ errorMap: () => ({ message: 'Missing baseUrl' }) }) + .min(1) + .optional(), + apiKey: z.string({ errorMap: () => ({ message: 'Missing apiKey' }) }).min(1), +}) diff --git a/package.json b/package.json index a080c025f..6e56fbf97 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,11 @@ "@fastify/cors": "^8.2.1", "@google-cloud/pubsub": "^3.4.1", "@mailchimp/mailchimp_transactional": "^1.0.50", + "@metriport/api": "^3.1.4", "axios": "^1.3.4", "date-fns": "^2.29.3", + "dayjs": "^1.11.7", + "driver-license-validator": "^3.1.1", "fastify": "^4.13.0", "form-data": "^4.0.0", "graphql": "^16.6.0", diff --git a/yarn.lock b/yarn.lock index 32de378d3..2cad5d5f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -67,6 +67,7 @@ __metadata: "@graphql-tools/apollo-engine-loader": ^7.3.26 "@graphql-typed-document-node/core": ^3.1.2 "@mailchimp/mailchimp_transactional": ^1.0.50 + "@metriport/api": ^3.1.4 "@types/jest": ^29.4.0 "@types/lodash": ^4.14.191 "@types/mailchimp__mailchimp_transactional": ^1.0.5 @@ -76,6 +77,8 @@ __metadata: "@typescript-eslint/eslint-plugin": ^5.52.0 axios: ^1.3.4 date-fns: ^2.29.3 + dayjs: ^1.11.7 + driver-license-validator: ^3.1.1 eslint: ^8.34.0 eslint-config-prettier: ^8.6.0 eslint-config-standard-with-typescript: ^34.0.0 @@ -2099,6 +2102,16 @@ __metadata: languageName: node linkType: hard +"@metriport/api@npm:^3.1.4": + version: 3.1.4 + resolution: "@metriport/api@npm:3.1.4" + dependencies: + axios: ^1.3.4 + zod: ^3.20.2 + checksum: 39f6623d3d8561a0177f89f4b3e31b50bc2d9eb37818d04db49686b9258bc1227219a6a3739bd0a670beb7bdeaf49f0b0d31225477d7ecd9425eb42a4bc53d1d + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4119,7 +4132,7 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.8.29": +"dayjs@npm:^1.11.7, dayjs@npm:^1.8.29": version: 1.11.7 resolution: "dayjs@npm:1.11.7" checksum: 5003a7c1dd9ed51385beb658231c3548700b82d3548c0cfbe549d85f2d08e90e972510282b7506941452c58d32136d6362f009c77ca55381a09c704e9f177ebb @@ -4308,6 +4321,13 @@ __metadata: languageName: node linkType: hard +"driver-license-validator@npm:^3.1.1": + version: 3.1.1 + resolution: "driver-license-validator@npm:3.1.1" + checksum: 7d7ca31990a5608d9c759075b912f5b40e822090ddb8fa7b9889a782374291e071f028b84a5e22107c902beba42a89f95768336a599c51512516baa01a01ed9c + languageName: node + linkType: hard + "dset@npm:^3.1.2": version: 3.1.2 resolution: "dset@npm:3.1.2" @@ -10383,7 +10403,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.21.4": +"zod@npm:^3.20.2, zod@npm:^3.21.4": version: 3.21.4 resolution: "zod@npm:3.21.4" checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f