From 22e5abd38118472a2cf58b407b780f2e18a26ae8 Mon Sep 17 00:00:00 2001 From: Joshua Kelly Date: Wed, 22 Sep 2021 00:04:36 -0400 Subject: [PATCH] Migrate CDS Hooks + Rest modules to fastify-plugin architecture - Deprecate Config - Deprecate Http module - Replace 'mount' interface/default export on CDS Hooks and Rest API modules with fastify-plugins - Rename several of the objects in the CDS Hooks namespace so they are prefixed with CDS - Split fixture servers --- src/cds-hooks/card.ts | 14 ++++----- src/cds-hooks/index.test.ts | 2 +- src/cds-hooks/index.ts | 10 +++--- src/cds-hooks/routes.ts | 38 +++++++++++++---------- src/cds-hooks/service.ts | 38 +++++++++++------------ src/cds-hooks/suggestion.ts | 4 +-- src/cds-hooks/util.ts | 16 +++++----- src/config.ts | 21 ------------- src/http/index.ts | 22 ------------- src/index.ts | 26 +++++----------- src/rest/capabilities.ts | 12 +++---- src/rest/index.ts | 28 ++++++++++++----- src/rest/routes/capabilityStatement.ts | 12 ++++--- src/rest/routes/interactions.test.ts | 2 +- src/rest/routes/interactions.ts | 14 +++++---- test/cds-hooks/appointment-book.ts | 8 ++--- test/cds-hooks/encounter-discharge.ts | 8 ++--- test/cds-hooks/encounter-start.ts | 8 ++--- test/cds-hooks/medication-prescribe.ts | 8 ++--- test/cds-hooks/order-review.ts | 8 ++--- test/cds-hooks/order-select.ts | 8 ++--- test/cds-hooks/order-sign.ts | 8 ++--- test/cds-hooks/patient-view.ts | 8 ++--- test/fixtures/{server.ts => cdsServer.ts} | 35 ++++++++++----------- test/fixtures/restServer.ts | 12 +++++++ 25 files changed, 174 insertions(+), 196 deletions(-) delete mode 100644 src/config.ts delete mode 100644 src/http/index.ts rename test/fixtures/{server.ts => cdsServer.ts} (54%) create mode 100644 test/fixtures/restServer.ts diff --git a/src/cds-hooks/card.ts b/src/cds-hooks/card.ts index 1a28d34..5878970 100644 --- a/src/cds-hooks/card.ts +++ b/src/cds-hooks/card.ts @@ -1,8 +1,8 @@ import { randomUUID } from "crypto"; -import Suggestion, { AcceptedSuggestion } from "./suggestion.js"; +import CDSSuggestion, { AcceptedSuggestion } from "./suggestion.js"; /** - * A **Card** contains decision support from a CDS Service. + * A **CDSCard** contains decision support from a CDS Service. * * Generally speaking, cards are intended for display to an end user. The data * format of a card defines a very minimal set of required attributes with @@ -10,13 +10,13 @@ import Suggestion, { AcceptedSuggestion } from "./suggestion.js"; * instance, narrative informational decision support, actionable suggestions to * modify data, and links to SMART apps. * - * One or many {@link Suggestion} can be created with each card. + * One or many {@link CDSSuggestion} can be created with each card. * * @version https://cds-hooks.hl7.org/ballots/2020Sep/ * * @example Here's an example of creating a Card without any suggestions: * ```typescript - * new Card({ + * new CDSCard({ * summary: "High risk for opioid overdose - taper now", * detail: "Total morphine milligram equivalent (MME) is 125mg. Taper to less than 50.", * source: { @@ -31,7 +31,7 @@ import Suggestion, { AcceptedSuggestion } from "./suggestion.js"; * }) * ``` */ -export default class Card { +export default class CDSCard { /** * Unique identifier of the card. MAY be used for auditing and logging cards * and SHALL be included in any subsequent calls to the CDS service's feedback @@ -69,7 +69,7 @@ export default class Card { * prescribed, for the `medication-prescribe` activity). If suggestions are * present, `selectionBehavior` MUST also be provided. */ - suggestions?: Suggestion[]; + suggestions?: CDSSuggestion[]; /** * Describes the intended selection behavior of the suggestions in the card. * Allowed values are: at-most-one, indicating that the user may choose none @@ -93,7 +93,7 @@ export default class Card { */ links?: Link[]; - constructor(options: Partial & { source: Source; summary: string; indicator: 'info' | 'warning' | 'critical' } ) { + constructor(options: Partial & { source: Source; summary: string; indicator: 'info' | 'warning' | 'critical' } ) { this.uuid = options.uuid || randomUUID(); this.detail = options.detail; this.suggestions = options.suggestions; diff --git a/src/cds-hooks/index.test.ts b/src/cds-hooks/index.test.ts index 642dd2d..4d459b5 100644 --- a/src/cds-hooks/index.test.ts +++ b/src/cds-hooks/index.test.ts @@ -1,5 +1,5 @@ import { randomUUID } from "crypto"; -import { http as app } from "../../test/fixtures/server" +import { http as app } from "../../test/fixtures/cdsServer" /** * Setting a mock UUID for all tests diff --git a/src/cds-hooks/index.ts b/src/cds-hooks/index.ts index 7eacb6d..7972cd1 100644 --- a/src/cds-hooks/index.ts +++ b/src/cds-hooks/index.ts @@ -2,8 +2,8 @@ * @module cds-hooks */ -export { default as mount } from "./routes.js"; -export { default as Service, HookRequest, HookResponse } from "./service.js"; -export { default as Card } from "./card.js"; -export { default as Suggestion } from "./suggestion.js"; -export { NoDecisionResponse } from "./util.js"; \ No newline at end of file +export { default as default } from "./routes.js"; +export { default as CDSService, CDSHookRequest, CDSHookResponse } from "./service.js"; +export { default as CDSCard } from "./card.js"; +export { default as CDSSuggestion } from "./suggestion.js"; +export { CDSNoDecisionResponse } from "./util.js"; \ No newline at end of file diff --git a/src/cds-hooks/routes.ts b/src/cds-hooks/routes.ts index 0f79813..e406951 100644 --- a/src/cds-hooks/routes.ts +++ b/src/cds-hooks/routes.ts @@ -1,13 +1,9 @@ -import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import { FastifyPluginCallback, FastifyReply, FastifyRequest } from "fastify"; +import fastifyPlugin from "fastify-plugin"; import Service from "./service.js"; -import Config from "../config"; import { Feedback } from "./card.js"; -import { HookRequest } from "./service.js"; -import { validateHookRequest, getService, NoDecisionResponse } from "./util.js"; - -export default function mount(config: Config, http: FastifyInstance): void { - routes(http, config.cdsHooks); -} +import { CDSHookRequest } from "./service.js"; +import { validateHookRequest, getService, CDSNoDecisionResponse } from "./util.js"; /** * @deprecated This should be invoked some other way @@ -19,8 +15,14 @@ function addCorsHeaders(reply: FastifyReply): void { reply.header("Access-Control-Allow-Headers", "Content-Type, Authorization"); } +interface CDSServicePluginOptions { + services: Service[]; + cors?: boolean; +} + + /** - * Responsible for validating the HookRequest, finding the service, and calling it + * Responsible for validating the CDSHookRequest, finding the service, and calling it * - or appropriately responding with and error * * @remarks This function is expected to increase in complexity significantly if @@ -31,7 +33,7 @@ function addCorsHeaders(reply: FastifyReply): void { * * @param options - */ -function invoke(options: Config["cdsHooks"]) { +function invoke(options: CDSServicePluginOptions) { return async (request: FastifyRequest<{ Params: { id: string }}>, reply: FastifyReply) => { if (options?.cors) addCorsHeaders(reply); @@ -46,10 +48,10 @@ function invoke(options: Config["cdsHooks"]) { reply.log.info("SchemaValidationError") reply.code(400).send(request.validationError); } else { - const hookRequest = request.body as HookRequest>; + const hookRequest = request.body as CDSHookRequest>; const validationError = validateHookRequest(hookRequest, service); - // 3. Is there a dynamic validation error on this HookRequest? + // 3. Is there a dynamic validation error on this CDSHookRequest? if (validationError) { reply.log.info("HookRequestValidationError") reply.code(400).send(validationError) @@ -59,7 +61,7 @@ function invoke(options: Config["cdsHooks"]) { const response = await service.handler(hookRequest); reply.send(response); } catch (error) { - if (error instanceof NoDecisionResponse) { + if (error instanceof CDSNoDecisionResponse) { reply.log.info("NoDecisionResponse") reply.send({ cards: [] @@ -78,11 +80,11 @@ function invoke(options: Config["cdsHooks"]) { * Responsible for accepting feedback requests as defined by the protocol. Logs the request * but doesn't otherwise process or validate it. * - * @todo validate that the feedback has referrential integrity to some original HookRequest + * @todo validate that the feedback has referrential integrity to some original CDSHookRequest * * @param options - */ -function feedback(options: Config["cdsHooks"]) { +function feedback(options: CDSServicePluginOptions) { return (request: FastifyRequest<{ Params: { id: string }}>, reply: FastifyReply) => { if (options?.cors) addCorsHeaders(reply); @@ -149,7 +151,7 @@ const feedbackSchema = { * @param http - A fastify instance * @param options - The services and */ -function routes(http: FastifyInstance, options: Config["cdsHooks"]): void { +const oauthPlugin: FastifyPluginCallback = function (http, options: CDSServicePluginOptions, next) { http.route({ method: 'GET', url: '/cds-services', @@ -197,8 +199,12 @@ function routes(http: FastifyInstance, options: Config["cdsHooks"]): void { } }) } + + next(); } +export default fastifyPlugin(oauthPlugin, { name: "cds-services"}) + /** * The response to the discovery endpoint SHALL be an object containing a list * of CDS Services. diff --git a/src/cds-hooks/service.ts b/src/cds-hooks/service.ts index f9410b0..9b9ea0f 100644 --- a/src/cds-hooks/service.ts +++ b/src/cds-hooks/service.ts @@ -1,36 +1,36 @@ import { randomUUID } from "crypto"; import { Hooks, SystemAction, FhirAuthorization } from "./util"; -import Card from "./card"; +import CDSCard from "./card"; /** * ServiceHandler is a function signature for the function invoked on the - * service to resolve a HookRequest + * service to resolve a CDSHookRequest * * @todo Is it possible to structure the generic here on something other than * `any`? */ type ServiceHandler = { - (request: HookRequest): Promise | HookResponse; + (request: CDSHookRequest): Promise | CDSHookResponse; }; /** - * A **Service** that provides patient-specific recommendations and guidance + * A **CDSService** that provides patient-specific recommendations and guidance * through RESTful APIs as described by the CDS Hooks spec. * * The primary APIs are Discovery, which allows a CDS Developer to publish the * types of CDS Services it provides, and the Service endpoint that CDS Clients * use to request and invoke decision support services. * - * **Service** automatically creates an HTTP API conformant with the CDS Hooks - * spec for you with just a few lines of code. Internally, **Service** uses - * {@link http} and to deliver its HTTP API. **Service** can be used in + * **CDSService** automatically creates an HTTP API conformant with the CDS Hooks + * spec for you with just a few lines of code. Internally, **CDSService** uses + * {@link http} and to deliver its HTTP API. **CDSService** can be used in * isolation or in combination with other modules composed on top of * {@link http} like {@link rest} and {@link smart}. * - * When invoked in response to a HookRequest event by a CDS Client, Service will + * When invoked in response to a CDSHookRequest event by a CDS Client, Service will * first validate the request and then call the invocation function as defined * at construction. This function, {@link handler}, receives the prefetch - * response in the HookRequest as defined by the Service up front. You can + * response in the CDSHookRequest as defined by the Service up front. You can * perform any necessary work here necessary to satisfy the request. Calling out * to a tensorflow model, or passing parameters to an external API for example. * @@ -41,7 +41,7 @@ type ServiceHandler = { * * @example Here's an example: * ```typescript - * new Service( + * new CDSService( * { * title: "patient-view Hook Service Example", * hook: "patient-view", @@ -58,7 +58,7 @@ type ServiceHandler = { * ) * ``` */ -export default class Service implements Service { +export default class CDSService implements CDSService { /** * The id portion of the URL to this service which is available at * `{baseUrl}/cds-services/{id}` @@ -84,7 +84,7 @@ export default class Service implements Service { */ public prefetch?: PrefetchTemplate; /** - * A function to execute on HookRequest invocation events + * A function to execute on CDSHookRequest invocation events */ public handler: ServiceHandler; /** @@ -98,14 +98,14 @@ export default class Service implements Service { public extension?: Extension; /** - * Pass any options along with a function that will execute on HookRequest + * Pass any options along with a function that will execute on CDSHookRequest * invocation events * * @param options - * @param fn - */ constructor( - options: Partial & { hook: Hooks; description: string }, + options: Partial & { hook: Hooks; description: string }, handler: ServiceHandler ) { this.hook = options.hook; @@ -118,7 +118,7 @@ export default class Service implements Service { } } -export interface HookResponse { +export interface CDSHookResponse { /** * An array of Cards. Cards can provide a combination of information (for * reading), suggested actions (to be applied if a user selects them), and @@ -126,7 +126,7 @@ export interface HookResponse { * how to display cards, but this specification recommends displaying * suggestions using buttons, and links using underlined text. */ - cards?: Card[]; + cards?: CDSCard[]; /** * An array of actions that the CDS Service proposes to auto-apply. Each @@ -156,7 +156,7 @@ interface Extension { [key: string]: any; } -interface HookRequestWithFhir extends HookRequestBasic { +interface CDSHookRequestWithFhir extends CDSHookRequestBasic { /** * The base URL of the CDS Client's FHIR server. If fhirAuthorization is * provided, this field is REQUIRED. The scheme should be https @@ -171,7 +171,7 @@ interface HookRequestWithFhir extends HookRequestBasic { fhirAuthorization: FhirAuthorization; } -interface HookRequestBasic { +interface CDSHookRequestBasic { /** * The hook that triggered this CDS Service call. See: * https://cds-hooks.org/specification/current/#hooks @@ -196,4 +196,4 @@ interface HookRequestBasic { prefetch: T; } -export type HookRequest = HookRequestBasic | HookRequestWithFhir; +export type CDSHookRequest = CDSHookRequestBasic | CDSHookRequestWithFhir; diff --git a/src/cds-hooks/suggestion.ts b/src/cds-hooks/suggestion.ts index 6d02a1a..2032134 100644 --- a/src/cds-hooks/suggestion.ts +++ b/src/cds-hooks/suggestion.ts @@ -1,7 +1,7 @@ import { randomUUID } from "crypto"; import { SystemAction } from "./util"; -export default class Suggestion implements Suggestion { +export default class CDSSuggestion implements CDSSuggestion { /** * Unique identifier of the card. MAY be used for auditing and logging cards * and SHALL be included in any subsequent calls to the CDS service's feedback @@ -28,7 +28,7 @@ export default class Suggestion implements Suggestion { */ actions?: SystemAction[]; - constructor(options: Partial & { label: string }) { + constructor(options: Partial & { label: string }) { this.uuid = options.uuid || randomUUID(); this.label = options.label; this.isRecommended = options.isRecommended; diff --git a/src/cds-hooks/util.ts b/src/cds-hooks/util.ts index cf38102..618aea8 100644 --- a/src/cds-hooks/util.ts +++ b/src/cds-hooks/util.ts @@ -1,7 +1,7 @@ -import { Service } from "./index"; +import { CDSService } from "./index"; import Ajv from "ajv"; import { ValidateFunction, ErrorObject } from "ajv"; -import { HookRequest } from "./service"; +import { CDSHookRequest } from "./service"; /** * Validates that the hook request dynmically (depending on the hook and service at runtime): @@ -12,7 +12,7 @@ import { HookRequest } from "./service"; * @param reply * @returns Error | undefined */ -export function validateHookRequest(hookRequest: HookRequest>, service: Service): Error | undefined { +export function validateHookRequest(hookRequest: CDSHookRequest>, service: CDSService): Error | undefined { // @ts-expect-error hookRequest.hook needs to be co-erced into the type... const context = validateContext(hookRequest, hookRequest.hook); const prefetch = validatePrefetch(hookRequest, service); @@ -64,7 +64,7 @@ function schemaErrorFormatter(errors: ErrorObject[], dataVar: string) { * @param service * @returns */ -function validatePrefetch(request: HookRequest>, service: Service): ValidateFunction { +function validatePrefetch(request: CDSHookRequest>, service: CDSService): ValidateFunction { const ajv = new Ajv({ removeAdditional: false, useDefaults: true, @@ -74,7 +74,7 @@ function validatePrefetch(request: HookRequest>, service: Se const prefetch = request.prefetch; - function prefetchFor(service: Service) { + function prefetchFor(service: CDSService) { // Service expects prefetch and no prefetch was sent if (service.prefetch && prefetch == undefined) { return { @@ -118,7 +118,7 @@ function validatePrefetch(request: HookRequest>, service: Se return compiled } -function validateContext(request: HookRequest>, hook: Hooks): ValidateFunction { +function validateContext(request: CDSHookRequest>, hook: Hooks): ValidateFunction { const ajv = new Ajv({ removeAdditional: true, useDefaults: true, @@ -233,11 +233,11 @@ function validateContext(request: HookRequest>, hook: Hooks) return compiled } -export function getService(services: Service[], id: string): Service | undefined { +export function getService(services: CDSService[], id: string): CDSService | undefined { return services.find((service) => service.id == id) } -export class NoDecisionResponse extends Error {} +export class CDSNoDecisionResponse extends Error {} export interface FhirAuthorization { /** diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index ae5dc68..0000000 --- a/src/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { FastifyServerOptions } from "fastify"; -import { Service } from "./cds-hooks"; -import { RestResourceCapability } from "./rest/capabilities"; - -interface cdsHooksConfig { - services: Service[]; - cors: boolean; -} - -interface restConfig { - capabilityStatement: fhir4.CapabilityStatement; - restResourceCapabilities: { - [key: string]: RestResourceCapability; - }; -} - -export default interface SeroConfiguration { - rest?: restConfig; - http?: FastifyServerOptions; - cdsHooks?: cdsHooksConfig; -} \ No newline at end of file diff --git a/src/http/index.ts b/src/http/index.ts deleted file mode 100644 index ae950be..0000000 --- a/src/http/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @module http - */ - -import fastify, { FastifyInstance } from 'fastify' -import Config from '../config'; - -export default (config: Config) => { - const http = fastify(config.http); - - return http; -} - -export function start(http: FastifyInstance): void { - http.listen(8080, '0.0.0.0', (err, address) => { - if (err) { - console.error(err) - process.exit(1) - } - console.log(`Sero booting at ${address}`) - }) -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 2b33838..5ccc5f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ */ export { - mount as Rest + default as Rest } from "./rest/index.js"; export { @@ -15,24 +15,14 @@ export { SmartAuthRedirectQuerystring } from "./smart-auth/index.js"; -// @todo rename these? export { - mount as CDSHooks, - HookRequest, - HookResponse, - Card, - Service, - Suggestion, - NoDecisionResponse, + default as CDSHooks, + CDSHookRequest, + CDSHookResponse, + CDSCard, + CDSService, + CDSSuggestion, + CDSNoDecisionResponse, } from "./cds-hooks/index.js"; -export { - default as Config -} from "./config"; - -export { - default as Http, - start -} from "./http/index.js"; - export { default as Client } from "./client/index.js" \ No newline at end of file diff --git a/src/rest/capabilities.ts b/src/rest/capabilities.ts index 56b772e..b1ef573 100644 --- a/src/rest/capabilities.ts +++ b/src/rest/capabilities.ts @@ -1,8 +1,8 @@ /** * Capabiltiies helpers and state */ -import Config from "../config"; import Resources from "../resources.js" +import { RestPluginConfig } from "./index.js"; export type RestResourceCapability = Partial @@ -41,16 +41,16 @@ const version = process.env.version; const name = "Sero Server Conformance Statement"; const date = (new Date()).toString(); -export function CapabilityStatement(config: Config): fhir4.CapabilityStatement { +export function CapabilityStatement(config: RestPluginConfig): fhir4.CapabilityStatement { const restResources: fhir4.CapabilityStatementRestResource[] = Object.values(Resources).map((resource) => { - const hasOverride = config.rest && Object.prototype.hasOwnProperty.call(config.rest.restResourceCapabilities, resource as string); + const hasOverride = config.restResourceCapabilities && Object.prototype.hasOwnProperty.call(config.restResourceCapabilities, resource as string); - if (hasOverride) { + if (hasOverride && config.restResourceCapabilities) { return { type: resource as string, profile: `http://hl7.org/fhir/StructureDefinition/${resource}`, - ...config.rest?.restResourceCapabilities[resource as string] + ...config?.restResourceCapabilities[resource as string] } } else { return { @@ -104,7 +104,7 @@ export function CapabilityStatement(config: Config): fhir4.CapabilityStatement { } } -export function TerminologyCapabilities(_config: Config): fhir4.TerminologyCapabilities { +export function TerminologyCapabilities(_config: RestPluginConfig): fhir4.TerminologyCapabilities { return { resourceType: "TerminologyCapabilities", date: (new Date()).toString(), diff --git a/src/rest/index.ts b/src/rest/index.ts index 27ff091..00b18a7 100644 --- a/src/rest/index.ts +++ b/src/rest/index.ts @@ -1,11 +1,23 @@ /** * @module rest */ -import { FastifyInstance } from 'fastify' -import Config from '../config'; -import * as routes from './routes/index.js'; - -export function mount(config: Config, http: FastifyInstance): void { - routes.capabilityStatement(config, http); - routes.interactions(config, http); -} \ No newline at end of file +import { FastifyPluginCallback } from 'fastify' +import { RestResourceCapability } from './capabilities.js'; +import { capabilityStatement, interactions }from './routes/index.js'; +import fastifyPlugin from "fastify-plugin"; + +export interface RestPluginConfig { + capabilityStatement?: fhir4.CapabilityStatement; + restResourceCapabilities: { + [key: string]: RestResourceCapability; + }; +} + +const oauthPlugin: FastifyPluginCallback = function (http, options: RestPluginConfig, next) { + http.register(capabilityStatement, options); + http.register(interactions, options); + + next(); +} + +export default fastifyPlugin(oauthPlugin, { name: "fhir-rest-api"}) \ No newline at end of file diff --git a/src/rest/routes/capabilityStatement.ts b/src/rest/routes/capabilityStatement.ts index 2b6859a..666e2f7 100644 --- a/src/rest/routes/capabilityStatement.ts +++ b/src/rest/routes/capabilityStatement.ts @@ -1,8 +1,8 @@ -import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify" -import Config from "../../config" +import { FastifyPluginCallback, FastifyReply, FastifyRequest } from "fastify" +import { RestPluginConfig } from ".."; import { CapabilityStatement, TerminologyCapabilities } from "../capabilities.js"; -function capabilities(config: Config) { +function capabilities(config: RestPluginConfig) { return (request: FastifyRequest<{ Querystring: { mode: string }}>, reply: FastifyReply) => { switch (request.query.mode) { case "normative": @@ -22,7 +22,7 @@ function capabilities(config: Config) { } } -export function capabilityStatement(config: Config, http: FastifyInstance): void { +export const capabilityStatement: FastifyPluginCallback = function (http, options, next) { // Get a capability statement for the system http.route({ method: 'GET', @@ -39,6 +39,8 @@ export function capabilityStatement(config: Config, http: FastifyInstance): void } }, }, - handler: capabilities(config) + handler: capabilities(options) }) + + next() } \ No newline at end of file diff --git a/src/rest/routes/interactions.test.ts b/src/rest/routes/interactions.test.ts index 2938365..3c3ddd5 100644 --- a/src/rest/routes/interactions.test.ts +++ b/src/rest/routes/interactions.test.ts @@ -1,4 +1,4 @@ -import { http as app } from "../../../test/fixtures/server" +import { http as app } from "../../../test/fixtures/restServer" /** * Setting a mock UUID for all tests diff --git a/src/rest/routes/interactions.ts b/src/rest/routes/interactions.ts index 7f0c5e1..747ffd0 100644 --- a/src/rest/routes/interactions.ts +++ b/src/rest/routes/interactions.ts @@ -1,10 +1,10 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify" -import Config from "../../config"; +import { FastifyRequest, FastifyReply, FastifyPluginCallback } from "fastify" import Resources from "../../resources.js"; import Store, { created } from "../store.js"; import { defaultRestReourceCapability } from "../capabilities.js"; import { randomUUID } from "crypto"; +import { RestPluginConfig } from "../index.js"; export function read(resource: Resources) { return (request: FastifyRequest<{ Params: { id: string }}>, reply: FastifyReply) => { @@ -76,17 +76,17 @@ async function historyType() { throw new Error("Not Implemented"); } -function hasInteraction(interaction: 'read' | 'vread' | 'update' | 'patch' | 'delete' | 'history-instance' | 'history-type' | 'create' | 'search-type', resource: Resources, config: Config) { - const hasOverride = config.rest && Object.prototype.hasOwnProperty.call(config.rest.restResourceCapabilities, resource as string); +function hasInteraction(interaction: 'read' | 'vread' | 'update' | 'patch' | 'delete' | 'history-instance' | 'history-type' | 'create' | 'search-type', resource: Resources, config: RestPluginConfig) { + const hasOverride = Object.prototype.hasOwnProperty.call(config.restResourceCapabilities, resource as string); if (hasOverride) { - return config.rest?.restResourceCapabilities[resource].interaction?.some((i) => i.code == interaction); + return config.restResourceCapabilities[resource].interaction?.some((i) => i.code == interaction); } else { return defaultRestReourceCapability.interaction?.some((i) => i.code == interaction); } } -export function interactions(config: Config, http: FastifyInstance): void { +export const interactions: FastifyPluginCallback = function (http, config: RestPluginConfig, next) { for (const resource in Resources) { // Read the current state of the resource if (hasInteraction('read', resource as Resources, config)) { @@ -193,4 +193,6 @@ export function interactions(config: Config, http: FastifyInstance): void { }, handler: batchOrTransaction }) + + next() } \ No newline at end of file diff --git a/test/cds-hooks/appointment-book.ts b/test/cds-hooks/appointment-book.ts index ce9f234..0db64d6 100644 --- a/test/cds-hooks/appointment-book.ts +++ b/test/cds-hooks/appointment-book.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Service, Card, HookRequest } from "../../src/cds-hooks"; +import { CDSService, CDSCard, CDSHookRequest } from "../../src/cds-hooks"; /** * appointment-book @@ -29,7 +29,7 @@ import { Service, Card, HookRequest } from "../../src/cds-hooks"; * hookVersion 1.0 * hookMaturity 1 - Submitted */ -export default new Service( +export default new CDSService( { id: "1", title: "appointment-book Hook Service Example", @@ -39,10 +39,10 @@ export default new Service( patient: "Patient/{{context.patientId}}" } }, - (_request: HookRequest<{ patient: fhir4.Patient }>) => { + (_request: CDSHookRequest<{ patient: fhir4.Patient }>) => { return { cards: [ - new Card({ + new CDSCard({ detail: "This is a card", source: { label: "CDS Services Inc", diff --git a/test/cds-hooks/encounter-discharge.ts b/test/cds-hooks/encounter-discharge.ts index d985663..357d742 100644 --- a/test/cds-hooks/encounter-discharge.ts +++ b/test/cds-hooks/encounter-discharge.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Service, Card, HookRequest } from "../../src/cds-hooks"; +import { CDSService, CDSCard, CDSHookRequest } from "../../src/cds-hooks"; /** * encounter-discharge @@ -25,7 +25,7 @@ import { Service, Card, HookRequest } from "../../src/cds-hooks"; * hookVersion 1.0 * hookMaturity 1 - Submitted */ -export default new Service( +export default new CDSService( { id: "2", title: "encounter-discharge Hook Service Example", @@ -35,10 +35,10 @@ export default new Service( patient: "Patient/{{context.patientId}}" } }, - (_request: HookRequest<{ patient: fhir4.Patient }>) => { + (_request: CDSHookRequest<{ patient: fhir4.Patient }>) => { return { cards: [ - new Card({ + new CDSCard({ detail: "This is a card", source: { label: "CDS Services Inc", diff --git a/test/cds-hooks/encounter-start.ts b/test/cds-hooks/encounter-start.ts index 19db47b..59d0b31 100644 --- a/test/cds-hooks/encounter-start.ts +++ b/test/cds-hooks/encounter-start.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Service, Card, HookRequest } from "../../src/cds-hooks"; +import { CDSService, CDSCard, CDSHookRequest } from "../../src/cds-hooks"; /** * encounter-start @@ -42,7 +42,7 @@ import { Service, Card, HookRequest } from "../../src/cds-hooks"; * hookVersion 1.0 * hookMaturity 1 - Submitted */ -export default new Service( +export default new CDSService( { id: "3", title: "encounter-start Hook Service Example", @@ -52,11 +52,11 @@ export default new Service( patient: "Patient/{{context.patientId}}" } }, - (_request: HookRequest<{ patient: fhir4.Patient }>) => { + (_request: CDSHookRequest<{ patient: fhir4.Patient }>) => { return { cards: [ - new Card({ + new CDSCard({ detail: "This is a card", source: { label: "CDS Services Inc", diff --git a/test/cds-hooks/medication-prescribe.ts b/test/cds-hooks/medication-prescribe.ts index dc94b17..d03ef6f 100644 --- a/test/cds-hooks/medication-prescribe.ts +++ b/test/cds-hooks/medication-prescribe.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Service, Card, HookRequest } from "../../src/cds-hooks"; +import { CDSService, CDSCard, CDSHookRequest } from "../../src/cds-hooks"; /** * medication-prescribe @@ -28,7 +28,7 @@ import { Service, Card, HookRequest } from "../../src/cds-hooks"; * hookVersion 1.0 * hookMaturity 2 - Tested */ -export default new Service( +export default new CDSService( { id: "4", title: "medication-prescribe Hook Service Example", @@ -38,11 +38,11 @@ export default new Service( patient: "Patient/{{context.patientId}}" } }, - (_request: HookRequest<{ patient: fhir4.Patient }>) => { + (_request: CDSHookRequest<{ patient: fhir4.Patient }>) => { return { cards: [ - new Card({ + new CDSCard({ detail: "This is a card", source: { label: "CDS Services Inc", diff --git a/test/cds-hooks/order-review.ts b/test/cds-hooks/order-review.ts index bca48c0..06eca05 100644 --- a/test/cds-hooks/order-review.ts +++ b/test/cds-hooks/order-review.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Service, Card, HookRequest } from "../../src/cds-hooks"; +import { CDSService, CDSCard, CDSHookRequest } from "../../src/cds-hooks"; /** * order-review @@ -28,7 +28,7 @@ import { Service, Card, HookRequest } from "../../src/cds-hooks"; * hookVersion 1.0 * hookMaturity 3 - Tested */ -export default new Service( +export default new CDSService( { id: "5", title: "order-review Hook Service Example", @@ -38,11 +38,11 @@ export default new Service( patient: "Patient/{{context.patientId}}" } }, - (_request: HookRequest<{ patient: fhir4.Patient }>) => { + (_request: CDSHookRequest<{ patient: fhir4.Patient }>) => { return { cards: [ - new Card({ + new CDSCard({ detail: "This is a card", source: { label: "CDS Services Inc", diff --git a/test/cds-hooks/order-select.ts b/test/cds-hooks/order-select.ts index db58c37..6d4c252 100644 --- a/test/cds-hooks/order-select.ts +++ b/test/cds-hooks/order-select.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Service, Card, HookRequest } from "../../src/cds-hooks"; +import { CDSService, CDSCard, CDSHookRequest } from "../../src/cds-hooks"; /** * order-select @@ -44,7 +44,7 @@ import { Service, Card, HookRequest } from "../../src/cds-hooks"; * hookVersion 1.0 * hookMaturity 1 - Submitted */ -export default new Service( +export default new CDSService( { id: "6", title: "order-select Hook Service Example", @@ -54,11 +54,11 @@ export default new Service( patient: "Patient/{{context.patientId}}" } }, - (_request: HookRequest<{ patient: fhir4.Patient }>) => { + (_request: CDSHookRequest<{ patient: fhir4.Patient }>) => { return { cards: [ - new Card({ + new CDSCard({ detail: "This is a card", source: { label: "CDS Services Inc", diff --git a/test/cds-hooks/order-sign.ts b/test/cds-hooks/order-sign.ts index 2b30d4c..5598cb7 100644 --- a/test/cds-hooks/order-sign.ts +++ b/test/cds-hooks/order-sign.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Service, Card, HookRequest } from "../../src/cds-hooks"; +import { CDSService, CDSCard, CDSHookRequest } from "../../src/cds-hooks"; /** * order-sign @@ -30,7 +30,7 @@ import { Service, Card, HookRequest } from "../../src/cds-hooks"; * hookVersion 1.0 * hookMaturity 1 - Submitted */ -export default new Service( +export default new CDSService( { id: "7", title: "order-sign Hook Service Example", @@ -40,11 +40,11 @@ export default new Service( patient: "Patient/{{context.patientId}}" } }, - (_request: HookRequest<{ patient: fhir4.Patient }>) => { + (_request: CDSHookRequest<{ patient: fhir4.Patient }>) => { return { cards: [ - new Card({ + new CDSCard({ detail: "This is a card", source: { label: "CDS Services Inc", diff --git a/test/cds-hooks/patient-view.ts b/test/cds-hooks/patient-view.ts index 1499429..7872c3e 100644 --- a/test/cds-hooks/patient-view.ts +++ b/test/cds-hooks/patient-view.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Service, Card, HookRequest } from "../../src/cds-hooks"; +import { CDSService, CDSCard, CDSHookRequest } from "../../src/cds-hooks"; /** * patient-view @@ -21,7 +21,7 @@ import { Service, Card, HookRequest } from "../../src/cds-hooks"; * hookMaturity 5 - Mature */ -export default new Service( +export default new CDSService( { id: "8", title: "patient-view Hook Service Example", @@ -31,11 +31,11 @@ export default new Service( patient: "Patient/{{context.patientId}}" } }, - (_request: HookRequest<{ patient: fhir4.Patient }>) => { + (_request: CDSHookRequest<{ patient: fhir4.Patient }>) => { return { cards: [ - new Card({ + new CDSCard({ detail: "This is a card", source: { label: "CDS Services Inc", diff --git a/test/fixtures/server.ts b/test/fixtures/cdsServer.ts similarity index 54% rename from test/fixtures/server.ts rename to test/fixtures/cdsServer.ts index f607810..9787dd7 100644 --- a/test/fixtures/server.ts +++ b/test/fixtures/cdsServer.ts @@ -1,5 +1,5 @@ import { - Config, CDSHooks, Http, Rest, + CDSHooks, } from "../../src" import appointmentBookExample from "../cds-hooks/appointment-book"; @@ -10,25 +10,22 @@ import orderReviewExample from "../cds-hooks/order-review"; import orderSelectExample from "../cds-hooks/order-select"; import orderSignExample from "../cds-hooks/order-sign"; import patientViewExample from "../cds-hooks/patient-view"; +import Http from "fastify"; -const config: Config = { - cdsHooks: { - services: [ - appointmentBookExample, - medicationPrescribeExample, - encounterDischargeExample, - encounterStartExample, - orderReviewExample, - orderSelectExample, - orderSignExample, - patientViewExample, - ], - cors: true - } +const pluginConfig = { + services: [ + appointmentBookExample, + medicationPrescribeExample, + encounterDischargeExample, + encounterStartExample, + orderReviewExample, + orderSelectExample, + orderSignExample, + patientViewExample, + ], + cors: true } -export const http = Http(config); +export const http = Http(); -// Move these to the register flow/plugin style? -CDSHooks(config, http); -Rest(config, http); \ No newline at end of file +http.register(CDSHooks, pluginConfig) \ No newline at end of file diff --git a/test/fixtures/restServer.ts b/test/fixtures/restServer.ts new file mode 100644 index 0000000..4628ccc --- /dev/null +++ b/test/fixtures/restServer.ts @@ -0,0 +1,12 @@ +import { Rest } from "../../src" + +import Http from "fastify"; +import { RestPluginConfig } from "../../src/rest"; + +const pluginConfig: RestPluginConfig = { + restResourceCapabilities: {} +} + +export const http = Http(); + +http.register(Rest, pluginConfig) \ No newline at end of file