Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for eClinicalWorks/Healow #215

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/api/src/app/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';

@Controller('health')
export class AppController {
@ApiTags('health')
@ApiOkResponse({
description: 'Returns 🚀 if the application is running',
type: String,
})
@Get('/')
getData() {
return '🚀';
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { EpicModule } from './epic/epic.module';
import { VeradigmModule } from './veradigm/veradigm.module';
import { TenantModule } from './tenant/tenant.module';
import { VAModule } from './va/va.module';
import { HealowModule } from './healow/healow.module';

const imports: ModuleMetadata['imports'] = [
StaticModule,
LoginProxyModule,
TenantModule,
VAModule,
HealowModule,
];

const opConfigured = checkIfOnPatientConfigured();
Expand Down
9 changes: 8 additions & 1 deletion apps/api/src/app/cerner/cerner.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Controller, Get, Logger, Query, Res } from '@nestjs/common';
import { Response } from 'express';
import { CernerService } from './cerner.service';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UnifiedTenantEndpoint } from '../tenant/tenant.service';

@Controller('v1/cerner')
export class CernerController {
constructor(private readonly cernerService: CernerService) {}

@Get('tenants')
@ApiTags('tenant')
@ApiOkResponse({
description: 'The tenants were successfully retrieved',
type: [UnifiedTenantEndpoint],
})
@Get('dstu2/tenants')
async getData(@Res() response: Response, @Query('query') query: string) {
try {
const data = await this.cernerService.queryTenants(query);
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/app/cerner/cerner.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Injectable } from '@nestjs/common';
import { CernerDSTU2TenantEndpoints, DSTU2Endpoint } from '@mere/cerner';
import { CernerDSTU2TenantEndpoints, TenantEndpoint } from '@mere/cerner';

@Injectable()
export class CernerService {
private readonly items = CernerDSTU2TenantEndpoints;

async queryTenants(query: string): Promise<DSTU2Endpoint[]> {
async queryTenants(query: string): Promise<TenantEndpoint[]> {
return filteredItemsWithQuery(this.items, query);
}
}

function filteredItemsWithQuery(items: DSTU2Endpoint[], query: string) {
function filteredItemsWithQuery(items: TenantEndpoint[], query: string) {
if (query === '' || query === undefined) {
return items.sort((x, y) => (x.name > y.name ? 1 : -1)).slice(0, 100);
}
Expand Down
26 changes: 18 additions & 8 deletions apps/api/src/app/epic/epic.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ import {
} from '@nestjs/common';
import { Response } from 'express';
import { EpicService } from './epic.service';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UnifiedTenantEndpoint } from '../tenant/tenant.service';

@Controller('v1/epic')
export class EpicController {
constructor(private readonly epicService: EpicService) {}

@ApiTags('tenant')
@ApiOkResponse({
description: 'The tenants were successfully retrieved',
type: [UnifiedTenantEndpoint],
})
@Get('dstu2/tenants')
async getDSTU2Tenants(
@NestRequest() request: Request,
Expand All @@ -28,14 +35,17 @@ export class EpicController {
}
}

@ApiTags('tenant')
@ApiOkResponse({
description: 'The tenants were successfully retrieved',
type: [UnifiedTenantEndpoint],
})
@Get('tenants')
async getTenants(@Res() response: Response, @Query('query') query: string) {
try {
const data = await this.epicService.queryTenants(query);
response.json(data);
} catch (e) {
Logger.error(e);
response.status(500).send({ message: 'There was an error' });
}
async getTenants(
@NestRequest() request: Request,
@Res() response: Response,
@Query('query') query: string,
) {
return await this.getDSTU2Tenants(request, response, query);
}
}
6 changes: 3 additions & 3 deletions apps/api/src/app/epic/epic.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Injectable } from '@nestjs/common';
import { EpicDSTU2TenantEndpoints, DSTU2Endpoint } from '@mere/epic';
import { EpicDSTU2TenantEndpoints, TenantEndpoint } from '@mere/epic';

@Injectable()
export class EpicService {
private readonly items = EpicDSTU2TenantEndpoints;

async queryTenants(query: string): Promise<DSTU2Endpoint[]> {
async queryTenants(query: string): Promise<TenantEndpoint[]> {
return filteredItemsWithQuery(this.items, query);
}
}

function filteredItemsWithQuery(items: DSTU2Endpoint[], query: string) {
function filteredItemsWithQuery(items: TenantEndpoint[], query: string) {
if (query === '' || query === undefined) {
return items.sort((x, y) => (x.name > y.name ? 1 : -1)).slice(0, 100);
}
Expand Down
26 changes: 26 additions & 0 deletions apps/api/src/app/healow/healow.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller, Get, Logger, Query, Res } from '@nestjs/common';
import { Response } from 'express';
import { HealowService } from './healow.service';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UnifiedTenantEndpoint } from '../tenant/tenant.service';

@Controller('v1/healow')
export class HealowController {
constructor(private readonly HealowService: HealowService) {}

@ApiTags('tenant')
@ApiOkResponse({
description: 'The tenants were successfully retrieved',
type: [UnifiedTenantEndpoint],
})
@Get('r4/tenants')
async getData(@Res() response: Response, @Query('query') query: string) {
try {
const data = await this.HealowService.queryR4Tenants(query);
response.json(data);
} catch (e) {
Logger.error(e);
response.status(500).send({ message: 'There was an error' });
}
}
}
10 changes: 10 additions & 0 deletions apps/api/src/app/healow/healow.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DynamicModule, Logger, Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { HealowService } from './healow.service';
import { HealowController } from './healow.controller';

@Module({
controllers: [HealowController],
providers: [HealowService],
})
export class HealowModule {}
70 changes: 70 additions & 0 deletions apps/api/src/app/healow/healow.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Injectable } from '@nestjs/common';
import { HealowR4TenantEndpoints, TenantEndpoint } from '@mere/healow';

@Injectable()
export class HealowService {
private readonly items = HealowR4TenantEndpoints;

async queryR4Tenants(query: string): Promise<TenantEndpoint[]> {
return filteredItemsWithQuery(this.items, query);
}
}

function filteredItemsWithQuery(items: TenantEndpoint[], query: string) {
if (query === '' || query === undefined) {
return items.sort((x, y) => (x.name > y.name ? 1 : -1)).slice(0, 100);
}
return items
.map((item) => {
// Match against each token, take highest score
const vals = item.name
.split(' ')
.map((token) => stringSimilarity(token, query));
const rating = Math.max(...vals);
return { rating, item };
})
.filter((item) => item.rating > 0.05)
.sort((a, b) => b.rating - a.rating)
.slice(0, 50)
.map((item) => item.item);
}

/**
* Compares the similarity between two strings using an n-gram comparison method.
* The grams default to length 2.
* @param str1 The first string to compare.
* @param str2 The second string to compare.
* @param gramSize The size of the grams. Defaults to length 2.
*/
export function stringSimilarity(str1: string, str2: string, gramSize = 2) {
if (!str1?.length || !str2?.length) {
return 0.0;
}

//Order the strings by length so the order they're passed in doesn't matter
//and so the smaller string's ngrams are always the ones in the set
const s1 = str1.length < str2.length ? str1 : str2;
const s2 = str1.length < str2.length ? str2 : str1;

const pairs1 = getNGrams(s1, gramSize);
const pairs2 = getNGrams(s2, gramSize);
const set = new Set<string>(pairs1);

const total = pairs2.length;
let hits = 0;
for (const item of pairs2) {
if (set.delete(item)) {
hits++;
}
}
return hits / total;
}

function getNGrams(s: string, len: number) {
s = ' '.repeat(len - 1) + s.toLowerCase() + ' '.repeat(len - 1);
const v = new Array(s.length - len + 1);
for (let i = 0; i < v.length; i++) {
v[i] = s.slice(i, i + len);
}
return v;
}
6 changes: 6 additions & 0 deletions apps/api/src/app/onpatient/onpatient.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Controller, Get, Logger, Query, Res } from '@nestjs/common';
import { Response } from 'express';
import { OnPatientService } from './onpatient.service';
import { ApiResponse, ApiTags } from '@nestjs/swagger';

@Controller('v1/onpatient')
export class OnPatientController {
constructor(private readonly onPatientService: OnPatientService) {}

@ApiTags('app-redirect')
@ApiResponse({
status: 302,
description: 'Redirects from the OnPatient authorization page to the app',
})
@Get('callback')
async getData(@Res() response: Response, @Query('code') code: string) {
try {
Expand Down
35 changes: 33 additions & 2 deletions apps/api/src/app/proxy/controllers/proxy.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,52 @@ import {
Param,
Request as NestRequest,
Res,
Query,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { ProxyService } from '../services';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';

@ApiTags('proxy')
@Controller('?*/proxy')
export class ProxyController {
private readonly logger = new Logger(ProxyController.name);

constructor(private proxyService: ProxyService) {}

@All('')
@ApiQuery({
name: 'target',
required: true,
type: String,
description: 'The target relative URL to proxy to',
})
@ApiQuery({
name: 'target_type',
required: true,
schema: {
type: 'string',
enum: ['token', 'base', 'register'],
},
description: 'The type of the target URL',
})
@ApiQuery({
name: 'serviceId',
required: true,
type: String,
description: 'The serviceId of the EPIC instance to proxy to',
})
async proxy(
@Res() response: Response,
@NestRequest() request: Request,
@Param() params: any,
@NestRequest()
request: Request<
unknown,
unknown,
unknown,
{ target: string; target_type: string; serviceId: string },
Record<string, any>
>,
@Param() params: Record<string, string>,
) {
try {
Logger.debug(
Expand Down
Loading
Loading