provide a consistent way to retrun results from WebWorkers.

fixed Username field in login page.
added support for CORS relay
fixed spec files.
This commit is contained in:
Jason Kulatunga 2022-10-12 18:47:12 -07:00
parent 6af72266f6
commit 4f90a9eedb
23 changed files with 214 additions and 118 deletions

View File

@ -23,3 +23,9 @@ docker run --rm -it -p 5984:5984 -v `pwd`/.couchdb/data:/opt/couchdb/data -v `pw
- WebUI:
- username: `testuser`
- password: `testuser`
# Running tests
- ng test --include='**/base_client.spec.ts'
- ng test --include='lib/**/*.spec.ts'

View File

@ -6,6 +6,4 @@ Find & replace the following
- `fastenhealth` - find and replace this with your binary name
- make sure you rename the folder as well.
# Running tests
- ng test --include='**/base_client.spec.ts'

View File

@ -0,0 +1,52 @@
package handler
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
//TODO, there are security implications to this, we need to make sure we lock this down.
func CORSProxy(c *gin.Context) {
//appConfig := c.MustGet("CONFIG").(config.Interface)
corsUrl := fmt.Sprintf("https://%s", strings.TrimPrefix(c.Param("proxyPath"), "/"))
remote, err := url.Parse(corsUrl)
remote.RawQuery = c.Request.URL.Query().Encode()
if err != nil {
panic(err)
}
proxy := httputil.ReverseProxy{}
//Define the director func
//This is a good place to log, for example
proxy.Director = func(req *http.Request) {
req.Header = c.Request.Header
req.Header.Add("X-Forwarded-Host", req.Host)
req.Header.Add("X-Origin-Host", remote.Host)
req.Host = remote.Host
req.URL.Scheme = remote.Scheme
req.URL.Host = remote.Host
log.Printf(c.Param("proxyPath"))
req.URL.Path = remote.Path
//TODO: throw an error if the remote.Host is not allowed
}
proxy.ModifyResponse = func(r *http.Response) error {
//b, _ := ioutil.ReadAll(r.Body)
//buf := bytes.NewBufferString("Monkey")
//buf.Write(b)
//r.Body = ioutil.NopCloser(buf)
r.Header.Set("Access-Control-Allow-Methods", "GET,HEAD")
r.Header.Set("Access-Control-Allow-Credentials", "true")
r.Header.Set("Access-Control-Allow-Origin", "*")
return nil
}
proxy.ServeHTTP(c.Writer, c.Request)
}

View File

@ -44,6 +44,8 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
api.GET("/metadata/source", handler.GetMetadataSource)
r.Any("/database/*proxyPath", handler.CouchDBProxy)
r.GET("/cors/*proxyPath", handler.CORSProxy)
r.OPTIONS("/cors/*proxyPath", handler.CORSProxy)
}
}

View File

@ -4,4 +4,6 @@ export class SourceSyncMessage {
source: Source
userIdentifier: string
encryptionKey?: string
response?: any
}

View File

@ -7,18 +7,15 @@
<form (ngSubmit)="signinSubmit()" #userForm="ngForm">
<div class="form-group">
<label>Email</label>
<input [(ngModel)]="existingUser.username" name="username" #username="ngModel" required minlength="2" type="text" class="form-control" placeholder="Enter your email">
<label>Username</label>
<input [(ngModel)]="existingUser.username" name="username" #username="ngModel" required minlength="2" type="text" class="form-control" placeholder="Enter your username">
<div *ngIf="username.invalid && (username.dirty || username.touched)" class="alert alert-danger">
<div *ngIf="username.errors?.['required']">
Email is required.
Username is required.
</div>
<div *ngIf="username.errors?.['minlength']">
Email must be at least 4 characters long.
</div>
<div *ngIf="username.errors?.['email']">
Email is not a valid email address.
Username must be at least 4 characters long.
</div>
</div>
</div><!-- form-group -->

View File

@ -14,6 +14,8 @@ import {SourceType} from '../../../lib/models/database/source_types';
import {QueueService} from '../../workers/queue.service';
import {ToastService} from '../../services/toast.service';
import {ToastNotification, ToastType} from '../../models/fasten/toast';
import {SourceSyncMessage} from '../../models/queue/source-sync-message';
import {UpsertSummary} from '../../../lib/models/fasten/upsert-summary';
// If you dont import this angular will import the wrong "Location"
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
@ -192,7 +194,7 @@ export class MedicalSourcesComponent implements OnInit {
expires_at: parseInt(getAccessTokenExpiration(payload, new BrowserAdapter())),
})
await this.fastenDb.CreateSource(dbSourceCredential).then(console.log)
await this.fastenDb.UpsertSource(dbSourceCredential).then(console.log)
this.queueSourceSyncWorker(sourceType as SourceType, dbSourceCredential)
})
@ -245,29 +247,40 @@ export class MedicalSourcesComponent implements OnInit {
// so that we can show incompelte statuses
this.queueService.runSourceSyncWorker(source)
.subscribe((msg) => {
const sourceSyncMessage = JSON.parse(msg)
const sourceSyncMessage = JSON.parse(msg) as SourceSyncMessage
delete this.status[sourceType]
// window.location.reload();
console.log("source sync-all response:", sourceSyncMessage)
//remove item from available sources list, add to connected sources.
this.availableSourceList.splice(this.availableSourceList.findIndex((item) => item.metadata.source_type == sourceType), 1);
if(this.connectedSourceList.findIndex((item) => item.metadata.source_type == sourceType) == -1){
//only add this as a connected source if its "new"
this.connectedSourceList.push({source: sourceSyncMessage.source, metadata: this.metadataSources[sourceType]})
}
const toastNotificaiton = new ToastNotification()
toastNotificaiton.type = ToastType.Success
toastNotificaiton.message = `Successfully connected ${sourceType}`
this.toastService.show(toastNotificaiton)
const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Success
toastNotification.message = `Successfully connected ${sourceType}`
const upsertSummary = sourceSyncMessage.response as UpsertSummary
if(upsertSummary && upsertSummary.totalResources != upsertSummary.updatedResources.length){
toastNotification.message += `\n (total: ${upsertSummary.totalResources}, updated: ${upsertSummary.updatedResources.length})`
} else if(upsertSummary){
toastNotification.message += `\n (total: ${upsertSummary.totalResources})`
}
this.toastService.show(toastNotification)
},
(err) => {
delete this.status[sourceType]
// window.location.reload();
const toastNotificaiton = new ToastNotification()
toastNotificaiton.type = ToastType.Error
toastNotificaiton.message = `An error occurred while accessing ${sourceType}: ${err}`
toastNotificaiton.autohide = false
this.toastService.show(toastNotificaiton)
const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Error
toastNotification.message = `An error occurred while accessing ${sourceType}: ${err}`
toastNotification.autohide = false
this.toastService.show(toastNotification)
console.error(err)
});
}

View File

@ -21,13 +21,14 @@ export class SourceSyncWorker implements DoWork<string, string> {
const client = NewClient(sourceSyncMessage.source.source_type, sourceSyncMessage.source)
//TODO: validate the FHIR version from the datasource matches the client
// if the source token has been refreshed, we need to store it in the DB.
// await db.CreateSource()
// await db.UpsertSource()
console.log("!!!!!!!!!!!!!!STARTING WORKER SYNC!!!!!!!!!", sourceSyncMessage)
return client.SyncAll(db)
.then((resp) => {
console.log("!!!!!!!!!!!!!COMPLETE WORKER SYNC!!!!!!!!!!", resp)
return JSON.stringify(resp)
sourceSyncMessage.response = resp
return JSON.stringify(sourceSyncMessage)
})
.catch((err) => {

View File

@ -2,6 +2,7 @@ import {IClient} from '../interface';
import {FHIR401Client} from './base/fhir401_r4_client';
import {Source} from '../../models/database/source';
import {IDatabaseRepository} from '../../database/interface';
import {UpsertSummary} from '../../models/fasten/upsert-summary';
export class AetnaClient extends FHIR401Client implements IClient {
constructor(source: Source) {
@ -13,11 +14,11 @@ export class AetnaClient extends FHIR401Client implements IClient {
* @param db
* @constructor
*/
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
const bundle = await this.GetResourceBundlePaginated("Patient")
const wrappedResourceModels = await this.ProcessBundle(bundle)
//todo, create the resources in dependency order
return await db.CreateResources(wrappedResourceModels)
return await db.UpsertResources(wrappedResourceModels)
}
}

View File

@ -2,6 +2,7 @@ import {IClient} from '../interface';
import {FHIR401Client} from './base/fhir401_r4_client';
import {Source} from '../../models/database/source';
import {IDatabaseRepository} from '../../database/interface';
import {UpsertSummary} from '../../models/fasten/upsert-summary';
export class AthenaClient extends FHIR401Client implements IClient {
constructor(source: Source) {
@ -13,7 +14,7 @@ export class AthenaClient extends FHIR401Client implements IClient {
* @param db
* @constructor
*/
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
const supportedResources: string[] = [
"AllergyIntolerance",
//"Binary",

View File

@ -55,6 +55,13 @@ export abstract class BaseClient {
} else {
resourceUrl = resourceSubpathOrNext
}
if(this.source.cors_relay_required){
//this endpoint requires a CORS relay
//get the path to the Fasten server, and append `cors/` and then append the request url
let resourceParts = new URL(resourceUrl)
resourceUrl = this.getCORSProxyPath() + `${resourceParts.hostname}${resourceParts.pathname}${resourceParts.search}`
}
//refresh the source if required
this.source = await this.refreshExpiredTokenIfRequired(this.source)
@ -80,6 +87,12 @@ export abstract class BaseClient {
// Private methods
/////////////////////////////////////////////////////////////////////////////
private getCORSProxyPath(): string {
const basePath = globalThis.location.pathname.split('/web').slice(0, 1)[0];
return `${globalThis.location.origin}${basePath || '/'}cors/`
}
private async refreshExpiredTokenIfRequired(source: Source): Promise<Source> {
//check if token has expired, and a refreshtoken is available
// Note: source.expires_at is in seconds, Date.now() is in milliseconds.

View File

@ -4,9 +4,11 @@ import {IResourceBundleRaw} from '../../interface';
import {ResourceFhir} from '../../../models/database/resource_fhir';
import {NewRepositiory} from '../../../database/pouchdb_repository';
import {Base64} from '../../../utils/base64';
import * as PouchDB from 'pouchdb/dist/pouchdb';
// @ts-ignore
import * as FHIR401Client_ProcessBundle from './fixtures/FHIR401Client_ProcessBundle.json';
import {IDatabaseRepository} from '../../../database/interface';
class TestClient extends FHIR401Client {
constructor(source: Source) {
@ -59,23 +61,36 @@ describe('FHIR401Client', () => {
})
describe('SyncAll', () => {
let repository: IDatabaseRepository;
beforeEach(async () => {
repository = NewRepositiory(null, null, new PouchDB("FHIR401Client-testing"));
});
afterEach(async () => {
if(repository){
const db = await repository.GetDB()
db.destroy() //wipe the db.
}
})
it('should correctly add resources to the database', async () => {
//setup
let response = new Response(JSON.stringify(FHIR401Client_ProcessBundle));
Object.defineProperty(response, "url", { value: `${client.source.api_endpoint_base_url}/Patient/${client.source.patient}/$everything`});
spyOn(window, "fetch").and.returnValue(Promise.resolve(response));
const db = NewRepositiory("fastentest")
//test
const resp = await client.SyncAll(db)
const resp = await client.SyncAll(repository)
const firstResourceFhir = resp[0]
const resourceIdParts = firstResourceFhir.split(":")
//expect
expect(resp.length).toEqual(206);
expect(resp.totalResources).toEqual(206);
expect(resp.updatedResources).toEqual([]);
expect(firstResourceFhir).toEqual('resource_fhir:c291cmNlOmFldG5hOjEyMzQ1:Patient:c088b7af-fc41-43cc-ab80-4a9ab8d47cd9');
expect(Base64.Decode(resourceIdParts[1])).toEqual("source:aetna:12345");
});
})

View File

@ -3,6 +3,7 @@ import {BaseClient} from './base_client';
import {Source} from '../../../models/database/source';
import {IDatabaseRepository} from '../../../database/interface';
import {ResourceFhir} from '../../../models/database/resource_fhir';
import {UpsertSummary} from '../../../models/fasten/upsert-summary';
export class FHIR401Client extends BaseClient implements IClient {
@ -18,13 +19,13 @@ export class FHIR401Client extends BaseClient implements IClient {
* @param db
* @constructor
*/
public async SyncAll(db: IDatabaseRepository): Promise<string[]> {
public async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
const bundle = await this.GetPatientBundle(this.source.patient)
const wrappedResourceModels = await this.ProcessBundle(bundle)
//todo, create the resources in dependency order
return db.CreateResources(wrappedResourceModels)
return db.UpsertResources(wrappedResourceModels)
}
@ -35,7 +36,7 @@ export class FHIR401Client extends BaseClient implements IClient {
* @param resourceNames
* @constructor
*/
public async SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise<string[]>{
public async SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise<UpsertSummary>{
//Store the Patient
const patientResource = await this.GetPatient(this.source.patient)
@ -45,7 +46,7 @@ export class FHIR401Client extends BaseClient implements IClient {
patientResourceFhir.source_resource_id = patientResource.id
patientResourceFhir.resource_raw = patientResource
await db.CreateResource(patientResourceFhir)
const upsertSummary = await db.UpsertResource(patientResourceFhir)
//error map storage.
let syncErrors = {}
@ -56,7 +57,9 @@ export class FHIR401Client extends BaseClient implements IClient {
try {
let bundle = await this.GetResourceBundlePaginated(`${resourceType}?patient=${this.source.patient}`)
let wrappedResourceModels = await this.ProcessBundle(bundle)
await db.CreateResources(wrappedResourceModels)
let resourceUpsertSummary = await db.UpsertResources(wrappedResourceModels)
upsertSummary.updatedResources = upsertSummary.updatedResources.concat(resourceUpsertSummary.updatedResources)
upsertSummary.totalResources += resourceUpsertSummary.totalResources
}
catch (e) {
console.error(`An error occurred while processing ${resourceType} bundle ${this.source.patient}`)
@ -66,7 +69,7 @@ export class FHIR401Client extends BaseClient implements IClient {
}
//TODO: correctly return newly inserted documents
return []
return upsertSummary
}
/**
@ -75,7 +78,7 @@ export class FHIR401Client extends BaseClient implements IClient {
* @param bundleFile
* @constructor
*/
public async SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise<any> {
public async SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise<UpsertSummary> {
return Promise.reject(new Error("not implemented"));
}

View File

@ -2,6 +2,7 @@ import {IClient} from '../interface';
import {FHIR401Client} from './base/fhir401_r4_client';
import {Source} from '../../models/database/source';
import {IDatabaseRepository} from '../../database/interface';
import {UpsertSummary} from '../../models/fasten/upsert-summary';
export class BlueButtonClient extends FHIR401Client implements IClient {
constructor(source: Source) {
@ -13,7 +14,7 @@ export class BlueButtonClient extends FHIR401Client implements IClient {
* @param db
* @constructor
*/
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
const supportedResources: string[] = [
"ExplanationOfBenefit",
"Coverage",

View File

@ -2,6 +2,7 @@ import {IClient} from '../interface';
import {FHIR401Client} from './base/fhir401_r4_client';
import {Source} from '../../models/database/source';
import {IDatabaseRepository} from '../../database/interface';
import {UpsertSummary} from '../../models/fasten/upsert-summary';
export class CernerClient extends FHIR401Client implements IClient {
constructor(source: Source) {
@ -15,7 +16,7 @@ export class CernerClient extends FHIR401Client implements IClient {
* @param db
* @constructor
*/
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
const supportedResources: string[] = [
"AllergyIntolerance",
"CarePlan",

View File

@ -2,6 +2,7 @@ import {IClient} from '../interface';
import {FHIR401Client} from './base/fhir401_r4_client';
import {Source} from '../../models/database/source';
import {IDatabaseRepository} from '../../database/interface';
import {UpsertSummary} from '../../models/fasten/upsert-summary';
export class EpicClient extends FHIR401Client implements IClient {
constructor(source: Source) {
@ -15,7 +16,7 @@ export class EpicClient extends FHIR401Client implements IClient {
* @param db
* @constructor
*/
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
const supportedResources: string[] = [
"AllergyIntolerance",
"CarePlan",

View File

@ -2,6 +2,7 @@ import {IClient} from '../interface';
import {FHIR401Client} from './base/fhir401_r4_client';
import {Source} from '../../models/database/source';
import {IDatabaseRepository} from '../../database/interface';
import {UpsertSummary} from '../../models/fasten/upsert-summary';
export class HealthITClient extends FHIR401Client implements IClient {
constructor(source: Source) {
@ -15,7 +16,7 @@ export class HealthITClient extends FHIR401Client implements IClient {
* @param db
* @constructor
*/
async SyncAll(db: IDatabaseRepository): Promise<string[]> {
async SyncAll(db: IDatabaseRepository): Promise<UpsertSummary> {
const supportedResources: string[] = [
"AllergyIntolerance",
"CarePlan",

View File

@ -1,4 +1,5 @@
import {IDatabaseRepository} from '../database/interface';
import {UpsertSummary} from '../models/fasten/upsert-summary';
export interface IClient {
fhirVersion: string
@ -10,13 +11,13 @@ export interface IClient {
* @param db
* @constructor
*/
SyncAll(db: IDatabaseRepository): Promise<any>
SyncAll(db: IDatabaseRepository): Promise<UpsertSummary>
SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise<string[]>
SyncAllByResourceName(db: IDatabaseRepository, resourceNames: string[]): Promise<UpsertSummary>
//Manual client ONLY functions
SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise<any>
SyncAllFromBundleFile(db: IDatabaseRepository, bundleFile: any): Promise<UpsertSummary>
}
//This is the "raw" Fhir resource

View File

@ -3,6 +3,7 @@ import {ResourceFhir} from '../models/database/resource_fhir';
import {SourceSummary} from '../models/fasten/source-summary';
import {Summary} from '../models/fasten/summary';
import {User} from '../models/fasten/user';
import {UpsertSummary} from '../models/fasten/upsert-summary';
// import {SourceSummary} from '../../app/models/fasten/source-summary';
export interface IDatabaseDocument {
@ -28,7 +29,7 @@ export interface IDatabaseRepository {
// GetUserByEmail(context.Context, string) (*models.User, error)
// GetCurrentUser(context.Context) *models.User
CreateSource(source: Source): Promise<string>
UpsertSource(source: Source): Promise<UpsertSummary>
GetSource(source_id: string): Promise<Source>
DeleteSource(source_id: string): Promise<boolean>
GetSourceSummary(source_id: string): Promise<SourceSummary>
@ -40,8 +41,8 @@ export interface IDatabaseRepository {
// GetResourceBySourceId(context.Context, string, string) (*models.ResourceFhir, error)
// ListResources(context.Context, models.ListResourceQueryOptions) ([]models.ResourceFhir, error)
// GetPatientForSources(ctx context.Context) ([]models.ResourceFhir, error)
CreateResource(resource: ResourceFhir): Promise<string>
CreateResources(resources: ResourceFhir[]): Promise<string[]>
UpsertResource(resource: ResourceFhir): Promise<UpsertSummary>
UpsertResources(resources: ResourceFhir[]): Promise<UpsertSummary>
GetResource(resource_id: string): Promise<ResourceFhir>
GetResources(): Promise<IDatabasePaginatedResponse>
GetResourcesForSource(source_id: string, source_resource_type?: string): Promise<IDatabasePaginatedResponse>

View File

@ -3,17 +3,19 @@ import {NewRepositiory} from './pouchdb_repository';
import {SourceType} from '../models/database/source_types';
import {Source} from '../models/database/source';
import {DocType} from './constants';
import * as PouchDB from 'pouchdb/dist/pouchdb';
describe('PouchdbRepository', () => {
let repository: IDatabaseRepository;
beforeEach(async () => {
repository = NewRepositiory();
repository = NewRepositiory(null, null, new PouchDB("PouchdbRepository-testing"));
});
afterEach(async () => {
if(repository){
await repository.GetDB().destroy() //wipe the db.
const db = await repository.GetDB()
db.destroy() //wipe the db.
}
})
@ -24,24 +26,25 @@ describe('PouchdbRepository', () => {
describe('CreateSource', () => {
it('should return an id', async () => {
const createdId = await repository.CreateSource(new Source({
const createdId = await repository.UpsertSource(new Source({
patient: 'patient',
source_type: SourceType.Aetna,
}))
expect(createdId).toEqual("source:aetna:patient");
expect(createdId.totalResources).toEqual(1);
expect(createdId.updatedResources[0]).toEqual("source:aetna:patient");
});
})
describe('GetSource', () => {
it('should return an source', async () => {
const createdId = await repository.CreateSource(new Source({
const createdResource = await repository.UpsertSource(new Source({
patient: 'patient',
source_type: SourceType.Aetna,
access_token: 'hello-world',
}))
const createdSource = await repository.GetSource(createdId)
const createdSource = await repository.GetSource(createdResource.updatedResources[0])
expect(createdSource.doc_type).toEqual(DocType.Source);
expect(createdSource.patient).toEqual('patient');
@ -52,13 +55,13 @@ describe('PouchdbRepository', () => {
describe('DeleteSource', () => {
it('should delete a source', async () => {
const createdId = await repository.CreateSource(new Source({
const createdResource = await repository.UpsertSource(new Source({
patient: 'patient-to-delete',
source_type: SourceType.Aetna,
access_token: 'hello-world',
}))
console.log(createdId)
const deletedSource = await repository.DeleteSource(createdId)
console.log(createdResource)
const deletedSource = await repository.DeleteSource(createdResource.updatedResources[0])
expect(deletedSource).toBeTruthy();
});
@ -66,19 +69,19 @@ describe('PouchdbRepository', () => {
describe('GetSources', () => {
it('should return a list of sources', async () => {
await repository.CreateSource(new Source({
await repository.UpsertSource(new Source({
patient: 'patient1',
source_type: SourceType.Aetna,
access_token: 'hello-world1',
}))
await repository.CreateSource(new Source({
await repository.UpsertSource(new Source({
patient: 'patient2',
source_type: SourceType.Aetna,
access_token: 'hello-world2',
}))
await repository.CreateSource(new Source({
await repository.UpsertSource(new Source({
patient: 'patient3',
source_type: SourceType.Aetna,
access_token: 'hello-world3',

View File

@ -9,6 +9,7 @@ import {Base64} from '../utils/base64';
import * as PouchDB from 'pouchdb/dist/pouchdb';
import * as PouchCrypto from 'crypto-pouch';
import {PouchdbUpsert} from './plugins/upsert';
import {UpsertSummary} from '../models/fasten/upsert-summary';
PouchDB.plugin(PouchCrypto);
@ -42,8 +43,8 @@ PouchDB.plugin(PouchCrypto);
* Eventually this method should dyanmically dtermine the version of the repo to return from the env.
* @constructor
*/
export function NewRepositiory(userIdentifier?: string, encryptionKey?: string): IDatabaseRepository {
return new PouchdbRepository(userIdentifier, encryptionKey)
export function NewRepositiory(userIdentifier?: string, encryptionKey?: string, localPouchDb?: PouchDB.Database): IDatabaseRepository {
return new PouchdbRepository(userIdentifier, encryptionKey, localPouchDb)
}
export class PouchdbRepository implements IDatabaseRepository {
@ -60,7 +61,7 @@ export class PouchdbRepository implements IDatabaseRepository {
* @param userIdentifier
* @param encryptionKey
*/
constructor(userIdentifier?: string, encryptionKey?: string) {
constructor(userIdentifier?: string, encryptionKey?: string, localPouchDb?: PouchDB.Database) {
this.remotePouchEndpoint = `${globalThis.location.protocol}//${globalThis.location.host}${this.getBasePath()}/database`
//setup PouchDB Plugins
@ -71,6 +72,10 @@ export class PouchdbRepository implements IDatabaseRepository {
this.encryptionKey = encryptionKey
// this.enableSync(userIdentifier)
}
if(localPouchDb){
console.warn("using local pouchdb, this should only be used for testing")
this.pouchDb = localPouchDb
}
}
@ -96,7 +101,7 @@ export class PouchdbRepository implements IDatabaseRepository {
///////////////////////////////////////////////////////////////////////////////////////
// Source
public async CreateSource(source: Source): Promise<string> {
public async UpsertSource(source: Source): Promise<UpsertSummary> {
return this.upsertDocument(source);
}
@ -156,11 +161,11 @@ export class PouchdbRepository implements IDatabaseRepository {
///////////////////////////////////////////////////////////////////////////////////////
// Resource
public async CreateResource(resource: ResourceFhir): Promise<string> {
public async UpsertResource(resource: ResourceFhir): Promise<UpsertSummary> {
return this.upsertDocument(resource);
}
public async CreateResources(resources: ResourceFhir[]): Promise<string[]> {
public async UpsertResources(resources: ResourceFhir[]): Promise<UpsertSummary> {
return this.upsertBulk(resources);
}
@ -228,7 +233,7 @@ export class PouchdbRepository implements IDatabaseRepository {
// update/insert a new document. Returns a promise of the generated id.
protected upsertDocument(newDoc: IDatabaseDocument) : Promise<string> {
protected upsertDocument(newDoc: IDatabaseDocument) : Promise<UpsertSummary> {
// make sure we always "populate" the ID for every document before submitting
newDoc.populateId()
@ -285,21 +290,28 @@ export class PouchdbRepository implements IDatabaseRepository {
})
})
.then(( result ): string => {
return( result.id );
.then(( result ): UpsertSummary => {
// // success, res is {rev: '1-xxx', updated: true, id: 'myDocId'}
const updateSummary = new UpsertSummary()
updateSummary.totalResources = 1
if(result.updated){
updateSummary.updatedResources = [result.id]
}
);
return updateSummary;
});
}
protected upsertBulk(docs: IDatabaseDocument[]): Promise<string[]> {
return this.GetDB()
.then((db) => {
protected upsertBulk(docs: IDatabaseDocument[]): Promise<UpsertSummary> {
return Promise.all(docs.map((doc) => {
doc.populateId();
return this.upsertDocument(doc)
}))
})).then((results) => {
return results.reduce((prev, current ) => {
prev.totalResources += current.totalResources
prev.updatedResources = prev.updatedResources.concat(current.updatedResources)
return prev
}, new UpsertSummary())
})
}
@ -333,44 +345,6 @@ export class PouchdbRepository implements IDatabaseRepository {
})
}
//DEPRECATED
/**
* create multiple documents, returns a list of generated ids
* @deprecated
* @param docs
* @protected
*/
protected createBulk(docs: IDatabaseDocument[]): Promise<string[]> {
return this.GetDB()
.then((db) => {
return db.bulkDocs(docs.map((doc) => { doc.populateId(); return doc }))
})
.then((results): string[] => {
return results.map((result) => result.id)
})
}
/**
* create a new document. Returns a promise of the generated id.
* @deprecated
* @param doc
* @protected
*/
protected createDocument(doc: IDatabaseDocument) : Promise<string> {
// make sure we always "populate" the ID for every document before submitting
doc.populateId()
// NOTE: All friends are given the key-prefix of "friend:". This way, when we go
// to query for friends, we can limit the scope to keys with in this key-space.
return this.GetDB()
.then((db) => db.put(doc))
.then(( result ): string => {
return( result.id );
}
);
}
///////////////////////////////////////////////////////////////////////////////////////
// Sync private/protected methods
///////////////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,8 @@
import {Source} from '../database/source';
import {ResourceFhir} from '../database/resource_fhir';
import {ResourceTypeCounts} from './source-summary';
export class UpsertSummary {
updatedResources: string[] = []
totalResources: number = 0
}

View File

@ -1,11 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"skipLibCheck": true, //without this the "dom" and the "webworker" libs conflict
"resolveJsonModule": true,
"outDir": "./out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [