moving models down to lib dir.
adding base client to conduit lib. adding tests renaming all Pouchdb record references to "Document".
This commit is contained in:
parent
5a61ed15c6
commit
0a5d71691f
|
@ -1,54 +1,10 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {Source} from '../models/database/source';
|
||||
// import * as PouchDB from 'pouchdb';
|
||||
import * as PouchDB from 'pouchdb/dist/pouchdb';
|
||||
import {PouchdbRepository} from '../../lib/database/pouchdb_repository';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FastenDbService {
|
||||
|
||||
localPouchDb: PouchDB.Database
|
||||
export class FastenDbService extends PouchdbRepository {
|
||||
constructor() {
|
||||
this.localPouchDb = new PouchDB('kittens');
|
||||
super("fasten");
|
||||
}
|
||||
|
||||
|
||||
async createSource(source: Source): Promise<string> {
|
||||
return this.createRecord(source);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// CRUD Operators
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Get the active PouchDB instance. Throws an error if no PouchDB instance is
|
||||
// available (ie, user has not yet been configured with call to .configureForUser()).
|
||||
public getDB(): any {
|
||||
if(!this.localPouchDb) {
|
||||
throw( new Error( "Database is not available - please configure an instance." ) );
|
||||
}
|
||||
return this.localPouchDb;
|
||||
}
|
||||
|
||||
// create a new record. Returns a promise of the generated id.
|
||||
private createRecord(record: Source) : Promise<string> {
|
||||
// make sure we always "populate" the ID for every record before submitting
|
||||
record.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()
|
||||
.put(record)
|
||||
.then(( result ): string => {
|
||||
return( result.id );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//public only for testing
|
||||
public getRecord(id: string): Promise<any> {
|
||||
return this.getDB()
|
||||
.get(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import {Observable} from 'rxjs';
|
|||
import {environment} from '../../environments/environment';
|
||||
import {map, tap} from 'rxjs/operators';
|
||||
import {ResponseWrapper} from '../models/response-wrapper';
|
||||
import {LighthouseSourceMetadata} from '../models/lighthouse/lighthouse-source-metadata';
|
||||
import {LighthouseSourceMetadata} from '../../lib/models/lighthouse/lighthouse-source-metadata';
|
||||
import * as Oauth from '@panva/oauth4webapi';
|
||||
|
||||
@Injectable({
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
import {BaseClient} from './base_client';
|
||||
import {Source} from '../../../models/database/source';
|
||||
|
||||
class TestClient extends BaseClient {
|
||||
constructor(source: Source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
|
||||
describe('BaseClient', () => {
|
||||
let client: TestClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient(new Source({
|
||||
"authorization_endpoint": "https://auth.logicahealth.org/authorize",
|
||||
"token_endpoint": "https://auth.logicahealth.org/token",
|
||||
"introspection_endpoint": "https://auth.logicahealth.org/introspect",
|
||||
"userinfo_endpoint": "",
|
||||
"scopes_supported": ["openid", "fhirUser", "patient/*.read", "offline_access"],
|
||||
"issuer": "https://auth.logicahealth.org",
|
||||
"grant_types_supported": ["authorization_code"],
|
||||
"response_types_supported": ["code"],
|
||||
"aud": "",
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
"api_endpoint_base_url": "https://api.logicahealth.org/fastenhealth/data",
|
||||
"client_id": "12b14c49-a4da-42f7-9e6f-2f19db622962",
|
||||
"redirect_uri": "https://lighthouse.fastenhealth.com/sandbox/callback/logica",
|
||||
"confidential": false,
|
||||
"source_type": "logica",
|
||||
"patient": "smart-1288992",
|
||||
"access_token": "xxx.xxx.xxx",
|
||||
"refresh_token": "xxx.xxx.",
|
||||
"expires_at": 1664949030,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(client).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('GetRequest', () => {
|
||||
it('should make an authorized request', async () => {
|
||||
const resp = await client.GetRequest("Patient/smart-1288992")
|
||||
|
||||
expect(resp).toEqual({
|
||||
resourceType: "Patient",
|
||||
id: "source:aetna:patient"
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
})
|
|
@ -0,0 +1,94 @@
|
|||
import {Source} from '../../../models/database/source';
|
||||
import * as Oauth from '@panva/oauth4webapi';
|
||||
import {IResourceInterface} from '../../interface';
|
||||
|
||||
// BaseClient is an abstract/partial class, its intended to be used by FHIR clients, and generically handle OAuth requests.
|
||||
export abstract class BaseClient {
|
||||
|
||||
private oauthClient: Oauth.Client
|
||||
private oauthAuthorizationServer: Oauth.AuthorizationServer
|
||||
public source: Source
|
||||
public headers: Headers = new Headers()
|
||||
|
||||
protected constructor(source: Source) {
|
||||
this.source = source
|
||||
|
||||
//init Oauth client based on source configuration
|
||||
this.oauthClient = {
|
||||
client_id: source.client_id,
|
||||
client_secret: "placeholder" //this is always a placeholder, if client_secret is required (for confidential clients), token_endpoint will be Lighthouse server.
|
||||
}
|
||||
this.oauthAuthorizationServer = {
|
||||
issuer: source.issuer,
|
||||
authorization_endpoint: source.authorization_endpoint,
|
||||
token_endpoint: source.token_endpoint,
|
||||
introspection_endpoint: source.introspection_endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async GetRequest(resourceSubpathOrNext: string): Promise<IResourceInterface> {
|
||||
|
||||
//check if the url is absolute
|
||||
let resourceUrl: string
|
||||
if (!(resourceSubpathOrNext.indexOf('http://') === 0 || resourceSubpathOrNext.indexOf('https://') === 0)) {
|
||||
//not absolute, so lets prefix with the source api endpoint
|
||||
resourceUrl = `${this.source.api_endpoint_base_url.trimEnd()}/${resourceSubpathOrNext.trimStart()}`
|
||||
} else {
|
||||
resourceUrl = resourceSubpathOrNext
|
||||
}
|
||||
|
||||
//refresh the source if required
|
||||
this.source = await this.refreshExpiredTokenIfRequired(this.source)
|
||||
|
||||
//make a request to the protected resource
|
||||
const resp = await Oauth.protectedResourceRequest(this.source.access_token, 'GET', new URL(resourceUrl), this.headers, null)
|
||||
|
||||
if(resp.status >=300 || resp.status < 200){
|
||||
// b, _ := io.ReadAll(resp.Body)
|
||||
throw new Error(`An error occurred during request ${resourceUrl} - ${resp.status} - ${resp.statusText} ${await resp.text()}`)
|
||||
}
|
||||
return resp.json()
|
||||
// err = ParseBundle(resp.Body, decodeModelPtr)
|
||||
// return err
|
||||
}
|
||||
|
||||
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.
|
||||
if(source.expires_at > Math.floor(Date.now() / 1000)) { //not expired return
|
||||
return Promise.resolve(source)
|
||||
}
|
||||
if(!source.refresh_token){
|
||||
return Promise.reject(new Error("access token is expired, but no refresh token available"))
|
||||
}
|
||||
|
||||
console.log("access token expired, refreshing...")
|
||||
return Oauth.refreshTokenGrantRequest(this.oauthAuthorizationServer, this.oauthClient, source.refresh_token)
|
||||
.then((refreshTokenResp) => {
|
||||
return Oauth.processRefreshTokenResponse(this.oauthAuthorizationServer, this.oauthClient, refreshTokenResp)
|
||||
})
|
||||
.then((newToken) => {
|
||||
if(newToken.access_token != source.access_token){
|
||||
// {
|
||||
// access_token: 'token',
|
||||
// token_type: 'bearer',
|
||||
// expires_in: 60,
|
||||
// scope: 'api:read',
|
||||
// refresh_token: 'refresh_token',
|
||||
// }
|
||||
|
||||
source.access_token = newToken.access_token as string
|
||||
// @ts-ignore
|
||||
source.expires_at = Math.floor(Date.now() / 1000) + parseInt(newToken.expires_in);
|
||||
|
||||
// update the "source" credential with new data (which will need to be sent
|
||||
// Don't overwrite `RefreshToken` with an empty value
|
||||
if(newToken.refresh_token != ""){
|
||||
source.refresh_token = newToken.refresh_token as string
|
||||
}
|
||||
}
|
||||
return source
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import {IDatabaseRepository} from '../database/interface';
|
||||
|
||||
export interface IClient {
|
||||
GetRequest(resourceSubpath: string): Promise<any>
|
||||
SyncAll(db: IDatabaseRepository): Promise<any>
|
||||
|
||||
//Manual client ONLY functions
|
||||
SyncAllBundle(db: IDatabaseRepository, bundleFile: any): Promise<any>
|
||||
}
|
||||
|
||||
export interface IResourceInterface {
|
||||
resourceType: string
|
||||
id?: string
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import {Source} from '../../app/models/database/source';
|
||||
import {Source} from '../models/database/source';
|
||||
// import {SourceSummary} from '../../app/models/fasten/source-summary';
|
||||
|
||||
export interface IDatabaseRecord {
|
||||
export interface IDatabaseDocument {
|
||||
populateId(): void
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {IDatabaseRepository} from './interface';
|
||||
import {NewRepositiory} from './pouchdb_repository';
|
||||
import {SourceType} from '../../app/models/database/types';
|
||||
import {Source} from '../../app/models/database/source';
|
||||
import {SourceType} from '../models/database/types';
|
||||
import {Source} from '../models/database/source';
|
||||
import {DocType} from './constants';
|
||||
|
||||
describe('PouchdbRepository', () => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Source} from '../../app/models/database/source';
|
||||
import {IDatabasePaginatedResponse, IDatabaseRecord, IDatabaseRepository} from './interface';
|
||||
import {Source} from '../models/database/source';
|
||||
import {IDatabasePaginatedResponse, IDatabaseDocument, IDatabaseRepository} from './interface';
|
||||
import * as PouchDB from 'pouchdb/dist/pouchdb';
|
||||
// import * as PouchDB from 'pouchdb';
|
||||
import {DocType} from './constants';
|
||||
|
@ -8,7 +8,7 @@ export function NewRepositiory(databaseName: string = 'fasten'): IDatabaseReposi
|
|||
return new PouchdbRepository(databaseName)
|
||||
}
|
||||
|
||||
class PouchdbRepository implements IDatabaseRepository {
|
||||
export class PouchdbRepository implements IDatabaseRepository {
|
||||
|
||||
localPouchDb: PouchDB.Database
|
||||
constructor(public databaseName: string) {
|
||||
|
@ -19,29 +19,29 @@ class PouchdbRepository implements IDatabaseRepository {
|
|||
}
|
||||
|
||||
public async CreateSource(source: Source): Promise<string> {
|
||||
return this.createRecord(source);
|
||||
return this.createDocument(source);
|
||||
}
|
||||
|
||||
public async GetSource(source_id: string): Promise<Source> {
|
||||
return this.getRecord(source_id)
|
||||
.then((record) => {
|
||||
return new Source(record)
|
||||
return this.getDocument(source_id)
|
||||
.then((doc) => {
|
||||
return new Source(doc)
|
||||
})
|
||||
}
|
||||
|
||||
public async GetSources(): Promise<IDatabasePaginatedResponse> {
|
||||
return this.findRecordByDocType(DocType.Source)
|
||||
.then((recordsWrapper) => {
|
||||
return this.findDocumentByDocType(DocType.Source)
|
||||
.then((docWrapper) => {
|
||||
|
||||
recordsWrapper.rows = recordsWrapper.rows.map((record) => {
|
||||
return new Source(record.doc)
|
||||
docWrapper.rows = docWrapper.rows.map((result) => {
|
||||
return new Source(result.doc)
|
||||
})
|
||||
return recordsWrapper
|
||||
return docWrapper
|
||||
})
|
||||
}
|
||||
|
||||
public async DeleteSource(source_id: string): Promise<boolean> {
|
||||
return this.deleteRecord(source_id)
|
||||
return this.deleteDocument(source_id)
|
||||
}
|
||||
|
||||
|
||||
|
@ -58,27 +58,27 @@ class PouchdbRepository implements IDatabaseRepository {
|
|||
return this.localPouchDb;
|
||||
}
|
||||
|
||||
// create a new record. Returns a promise of the generated id.
|
||||
private createRecord(record: IDatabaseRecord) : Promise<string> {
|
||||
// make sure we always "populate" the ID for every record before submitting
|
||||
record.populateId()
|
||||
// create a new document. Returns a promise of the generated id.
|
||||
private 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()
|
||||
.put(record)
|
||||
.put(doc)
|
||||
.then(( result ): string => {
|
||||
return( result.id );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private getRecord(id: string): Promise<any> {
|
||||
private getDocument(id: string): Promise<any> {
|
||||
return this.GetDB()
|
||||
.get(id)
|
||||
}
|
||||
|
||||
private findRecordByDocType(docType: DocType, includeDocs: boolean = true): Promise<IDatabasePaginatedResponse> {
|
||||
private findDocumentByDocType(docType: DocType, includeDocs: boolean = true): Promise<IDatabasePaginatedResponse> {
|
||||
return this.GetDB()
|
||||
.allDocs({
|
||||
include_docs: includeDocs,
|
||||
|
@ -87,10 +87,10 @@ class PouchdbRepository implements IDatabaseRepository {
|
|||
})
|
||||
}
|
||||
|
||||
private async deleteRecord(id: string): Promise<boolean> {
|
||||
const recordToDelete = await this.getRecord(id)
|
||||
private async deleteDocument(id: string): Promise<boolean> {
|
||||
const docToDelete = await this.getDocument(id)
|
||||
return this.GetDB()
|
||||
.remove(recordToDelete)
|
||||
.remove(docToDelete)
|
||||
.then((result) => {
|
||||
return result.ok
|
||||
})
|
||||
|
|
|
@ -10,9 +10,9 @@ export class Source extends LighthouseSourceMetadata{
|
|||
|
||||
patient: string
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
id_token: string
|
||||
expires_at: number
|
||||
refresh_token?: string
|
||||
id_token?: string
|
||||
expires_at: number //seconds since epoch
|
||||
|
||||
constructor(object: any) {
|
||||
super()
|
Loading…
Reference in New Issue