Skip to main content
fi-fhir docs

Overview

GraphQL API Layer Design

This document describes the GraphQL API layer for fi-fhir, providing a flexible query interface for healthcare events and resources.

Overview

The GraphQL API provides:

  • Queries: Retrieve events, patients, workflows, and configurations
  • Mutations: Submit events, trigger workflows, manage subscriptions
  • Subscriptions: Real-time event streaming via WebSocket
┌─────────────────────────────────────────────────────────────┐
│                      GraphQL API                             │
├─────────────────────────────────────────────────────────────┤
│  Queries              Mutations            Subscriptions     │
│  ────────             ─────────            ─────────────     │
│  events()             submitEvent()        eventStream()     │
│  event(id)            parseMessage()       workflowEvents()  │
│  patient(mrn)         triggerWorkflow()                      │
│  workflow(name)       createSubscription()                   │
│  health()             deleteSubscription()                   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Event Store / Router                      │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │   Parser    │  │  Workflow   │  │    FHIR     │          │
│  │   Engine    │  │   Engine    │  │  Subscriptions│        │
│  └─────────────┘  └─────────────┘  └─────────────┘          │
└─────────────────────────────────────────────────────────────┘

Schema Design

Core Types

# Scalar types
scalar DateTime
scalar JSON

# Event interface - all events implement this
interface Event {
  id: ID!
  type: EventType!
  timestamp: DateTime!
  source: String!
  sourceFormat: SourceFormat!
  correlationId: String
}

# Event type enum
enum EventType {
  PATIENT_ADMIT
  PATIENT_DISCHARGE
  PATIENT_TRANSFER
  PATIENT_UPDATE
  LAB_RESULT
  LAB_ORDERED
  APPOINTMENT_SCHEDULED
  APPOINTMENT_CANCELLED
  APPOINTMENT_NOSHOW
  CLAIM_SUBMITTED
  CLAIM_ADJUDICATED
  VITAL_SIGN
  CONDITION
  PROCEDURE
  IMMUNIZATION
  DOCUMENT
}

# Source format enum
enum SourceFormat {
  HL7V2
  FHIR
  CSV
  EDI_837
  EDI_835
  CDA
}

# Patient type
type Patient {
  mrn: ID!
  identifiers: [Identifier!]!
  familyName: String!
  givenName: String!
  middleName: String
  dateOfBirth: DateTime
  gender: String
  address: Address
  phone: String
  email: String
}

# Identifier type
type Identifier {
  value: String!
  type: String!
  system: String
  assigner: String
}

# Address type
type Address {
  line1: String
  line2: String
  city: String
  state: String
  postalCode: String
  country: String
}

# Provider type
type Provider {
  npi: String
  id: String
  familyName: String!
  givenName: String!
  specialty: String
  organizationName: String
}

# Location type
type Location {
  facility: String
  unit: String
  room: String
  bed: String
}

# Encounter type
type Encounter {
  id: ID!
  class: String!
  status: String
  admitDateTime: DateTime
  dischargeDateTime: DateTime
  location: Location
  attendingProvider: Provider
}

Event Types

# Patient admit event
type PatientAdmitEvent implements Event {
  id: ID!
  type: EventType!
  timestamp: DateTime!
  source: String!
  sourceFormat: SourceFormat!
  correlationId: String
  patient: Patient!
  encounter: Encounter!
}

# Patient discharge event
type PatientDischargeEvent implements Event {
  id: ID!
  type: EventType!
  timestamp: DateTime!
  source: String!
  sourceFormat: SourceFormat!
  correlationId: String
  patient: Patient!
  encounter: Encounter!
}

# Lab result event
type LabResultEvent implements Event {
  id: ID!
  type: EventType!
  timestamp: DateTime!
  source: String!
  sourceFormat: SourceFormat!
  correlationId: String
  patient: Patient!
  test: LabTest!
  result: LabResult!
  isCritical: Boolean!
  orderingProvider: Provider
}

type LabTest {
  loincCode: String
  localCode: String
  description: String!
  category: String
}

type LabResult {
  value: String!
  unit: String
  referenceRange: String
  interpretation: String
  status: String
}

# Vital sign event
type VitalSignEvent implements Event {
  id: ID!
  type: EventType!
  timestamp: DateTime!
  source: String!
  sourceFormat: SourceFormat!
  correlationId: String
  patient: Patient!
  vitalSign: VitalSign!
}

type VitalSign {
  name: String!
  loincCode: String
  value: String!
  unit: String
  interpretation: String
}

# Condition event
type ConditionEvent implements Event {
  id: ID!
  type: EventType!
  timestamp: DateTime!
  source: String!
  sourceFormat: SourceFormat!
  correlationId: String
  patient: Patient!
  condition: Condition!
  clinicalStatus: String
  onsetDate: String
}

type Condition {
  name: String!
  code: String
  codeSystem: String
  category: String
}

# Appointment event
type AppointmentEvent implements Event {
  id: ID!
  type: EventType!
  timestamp: DateTime!
  source: String!
  sourceFormat: SourceFormat!
  correlationId: String
  patient: Patient!
  appointment: Appointment!
}

type Appointment {
  id: ID!
  status: String!
  startTime: DateTime!
  endTime: DateTime
  location: Location
  provider: Provider
  reason: String
}

# Document event
type DocumentEvent implements Event {
  id: ID!
  type: EventType!
  timestamp: DateTime!
  source: String!
  sourceFormat: SourceFormat!
  correlationId: String
  patient: Patient
  documentType: String!
  title: String
}

Query Types

type Query {
  # Get a single event by ID
  event(id: ID!): Event

  # Query events with filters
  events(
    filter: EventFilter
    first: Int = 100
    after: String
    orderBy: EventOrderBy
  ): EventConnection!

  # Get patient by MRN
  patient(mrn: ID!): Patient

  # Get patients with filter
  patients(
    filter: PatientFilter
    first: Int = 100
    after: String
  ): PatientConnection!

  # Get workflow status
  workflow(name: String!): WorkflowStatus

  # List all workflows
  workflows: [WorkflowStatus!]!

  # Health check
  health: HealthStatus!

  # Get parse result without persisting
  parsePreview(
    format: SourceFormat!
    data: String!
    source: String
  ): ParseResult!
}

# Event filter input
input EventFilter {
  types: [EventType!]
  sources: [String!]
  patientMrn: String
  fromTimestamp: DateTime
  toTimestamp: DateTime
  correlationId: String
}

# Event ordering
input EventOrderBy {
  field: EventOrderField!
  direction: OrderDirection!
}

enum EventOrderField {
  TIMESTAMP
  TYPE
  SOURCE
}

enum OrderDirection {
  ASC
  DESC
}

# Pagination connection types
type EventConnection {
  edges: [EventEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type EventEdge {
  cursor: String!
  node: Event!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type PatientConnection {
  edges: [PatientEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type PatientEdge {
  cursor: String!
  node: Patient!
}

input PatientFilter {
  mrn: String
  familyName: String
  givenName: String
  dateOfBirth: DateTime
}

type WorkflowStatus {
  name: String!
  enabled: Boolean!
  routeCount: Int!
  eventsProcessed: Int!
  lastEventTime: DateTime
  errors: Int!
}

type HealthStatus {
  status: String!
  version: String!
  uptime: Int!
  components: [ComponentHealth!]!
}

type ComponentHealth {
  name: String!
  status: String!
  message: String
}

type ParseResult {
  success: Boolean!
  events: [Event!]!
  warnings: [ParseWarning!]!
  errors: [String!]!
}

type ParseWarning {
  phase: String!
  code: String!
  message: String!
  path: String
}

Mutation Types

type Mutation {
  # Submit a raw message for parsing and processing
  submitMessage(input: SubmitMessageInput!): SubmitResult!

  # Submit a pre-parsed event directly
  submitEvent(input: SubmitEventInput!): SubmitResult!

  # Trigger a workflow manually
  triggerWorkflow(name: String!, event: JSON!): WorkflowResult!

  # Create a FHIR subscription
  createFhirSubscription(input: CreateSubscriptionInput!): FhirSubscription!

  # Delete a FHIR subscription
  deleteFhirSubscription(id: ID!): Boolean!

  # Pause a FHIR subscription
  pauseFhirSubscription(id: ID!): FhirSubscription!

  # Resume a FHIR subscription
  resumeFhirSubscription(id: ID!): FhirSubscription!
}

input SubmitMessageInput {
  format: SourceFormat!
  data: String!
  source: String!
  correlationId: String
}

input SubmitEventInput {
  type: EventType!
  data: JSON!
  source: String!
  correlationId: String
}

type SubmitResult {
  success: Boolean!
  eventId: ID
  warnings: [ParseWarning!]!
  errors: [String!]!
  workflowResults: [WorkflowResult!]!
}

type WorkflowResult {
  workflowName: String!
  routesMatched: Int!
  actionsExecuted: Int!
  errors: [String!]!
  duration: Int! # milliseconds
}

input CreateSubscriptionInput {
  name: String!
  server: String!
  criteria: String!
  endpoint: String!
}

type FhirSubscription {
  id: ID!
  name: String!
  status: String!
  criteria: String!
  server: String!
  endpoint: String!
  createdAt: DateTime!
}

Subscription Types (Real-time)

type Subscription {
  # Stream all events
  eventStream(filter: EventFilter): Event!

  # Stream events for a specific workflow
  workflowEvents(workflowName: String!): WorkflowEventNotification!

  # Stream events for a specific patient
  patientEvents(mrn: ID!): Event!
}

type WorkflowEventNotification {
  event: Event!
  workflow: String!
  routesMatched: [String!]!
  actionsExecuted: [String!]!
  duration: Int!
}

Architecture

Package Structure

internal/api/graphql/
├── schema.go          # Schema definitions using gqlgen
├── resolver.go        # Root resolver
├── resolvers/
│   ├── query.go       # Query resolvers
│   ├── mutation.go    # Mutation resolvers
│   ├── subscription.go # Subscription resolvers
│   └── types.go       # Type resolvers
├── model/
│   └── models.go      # Generated models
├── dataloaders/
│   └── loaders.go     # DataLoader for N+1 prevention
└── server.go          # HTTP/WebSocket server

Implementation Strategy

  1. Schema-First Development: Define GraphQL schema, generate Go code
  2. gqlgen: Use gqlgen for type-safe resolver generation
  3. DataLoaders: Batch and cache lookups to prevent N+1 queries
  4. WebSocket Subscriptions: Use graphql-ws protocol for real-time events

Server Configuration

graphql:
  # Server settings
  enabled: true
  host: '0.0.0.0'
  port: 8081
  path: '/graphql'
  playground: true # Enable GraphQL Playground in dev

  # WebSocket settings
  websocket:
    enabled: true
    path: '/graphql/ws'
    keepalive: 30s

  # Query complexity limits
  complexity:
    max_depth: 10
    max_complexity: 1000

  # Authentication
  auth:
    enabled: false
    type: 'bearer' # bearer, basic, api_key

  # CORS settings
  cors:
    allowed_origins: ['*']
    allowed_methods: ['GET', 'POST', 'OPTIONS']
    allowed_headers: ['Authorization', 'Content-Type']

CLI Integration

# Start GraphQL server
fi-fhir serve --graphql

# Start with specific config
fi-fhir serve --graphql --config config.yaml

# Query via CLI (convenience wrapper)
fi-fhir graphql query '{ health { status version } }'

# Subscribe to events
fi-fhir graphql subscribe 'subscription { eventStream { id type timestamp } }'

Example Queries

Get Recent Lab Results

query RecentLabResults {
  events(
    filter: { types: [LAB_RESULT], patientMrn: "MRN12345" }
    first: 10
    orderBy: { field: TIMESTAMP, direction: DESC }
  ) {
    edges {
      node {
        ... on LabResultEvent {
          id
          timestamp
          test {
            loincCode
            description
          }
          result {
            value
            unit
            interpretation
          }
          isCritical
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Submit HL7v2 Message

mutation SubmitHL7 {
  submitMessage(
    input: {
      format: HL7V2
      data: "MSH|^~\\&|LAB|FACILITY|..."
      source: "lab_interface"
    }
  ) {
    success
    eventId
    warnings {
      code
      message
    }
    workflowResults {
      workflowName
      routesMatched
    }
  }
}

Subscribe to Patient Events

subscription PatientMonitor {
  patientEvents(mrn: "MRN12345") {
    id
    type
    timestamp
    ... on LabResultEvent {
      isCritical
      test {
        description
      }
      result {
        value
        unit
      }
    }
    ... on VitalSignEvent {
      vitalSign {
        name
        value
        unit
      }
    }
  }
}

Implementation Plan

Phase 1: Core API ✅

  • Schema definition with gqlgen - see internal/api/graphql/schema.graphql
  • Query resolvers (events, patients, health) - see internal/api/graphql/resolvers/
  • Basic HTTP server - see internal/api/graphql/server.go

Phase 2: Mutations ✅

  • submitMessage mutation - see schema.resolvers.go
  • submitEvent mutation - see schema.resolvers.go
  • triggerWorkflow mutation - see schema.resolvers.go
  • FHIR subscription CRUD mutations - see schema.resolvers.go

Phase 3: Real-time ✅

  • WebSocket subscriptions - see server.go (graphql-ws protocol)
  • Event stream filtering - see schema.resolvers.go
  • Workflow event notifications - see schema.resolvers.go (pub/sub pattern)

Phase 4: Production ✅

  • Authentication/Authorization - see server.go
  • Query complexity limiting - see server.go
  • Metrics and tracing - integrated with workflow metrics
  • Rate limiting - see server.go

See Also