import { useState , useEffect } from 'react' ; import { Alert } from 'react-native' ; import * as rdf from '@jasonpaulos/rdflib' ; import { EventEmitter } from 'events' ; import { URL } from 'whatwg-url' ; import moment from 'moment' ; import { onWebIdChange , authenticatedFetch } from './auth' ; import { DataPoint , getDailySteps as getGoogleFitSteps , getDailyDistance as getGoogleFitDistance , getHeartRate as getGoogleFitHeartRate , } from './googlefit' ; import { FOAF , RDF , SOLID , FHIR } from './ns' ; export interface Profile { webId : string ; name ? : string ; image ? : string ; friends : string [ ] ; privateTypeIndex ? : string ; } export interface SyncStatus { value : number ; maxValue : number | null ; description : string ; } export type PodDataPoint = DataPoint & { uri : string ; parsedDate : Date ; } ; const syncEvents = new EventEmitter ( ) ; const syncStatus = { value : 0 , maxValue : null , description : 'Setting up...' , } ; let profile = null ; let observationLocation = null ; const fitnessData = { loading : true , heartRate : [ ] , steps : [ ] , distance : [ ] , } ; function setSyncStatus ( desc , value ? , maxValue ? ) { syncStatus . description = desc ; syncStatus . value = value == null || maxValue == null ? 0 : value ; syncStatus . maxValue = value == null || maxValue == null ? null : maxValue ; console . log ( `${ desc } ${ value == null || maxValue == null ? '...' : `${ value }/${ maxValue }` }` ) ; syncEvents . emit ( 'status' , { ... syncStatus } ) ; } export function useSyncStatus ( ) { const [ status , setStatus ] = useState < SyncStatus > ( syncStatus ) ; useEffect ( ( ) => { syncEvents . on ( 'status' , setStatus ) ; return ( ) => { syncEvents . removeListener ( 'status' , setStatus ) ; } ; } , [ ] ) ; return status ; } export function useProfile ( ) { const [ p , setProfile ] = useState < Profile | null > ( profile ) ; useEffect ( ( ) => { syncEvents . on ( 'profile' , setProfile ) ; return ( ) => { syncEvents . removeListener ( 'profile' , setProfile ) ; } ; } , [ ] ) ; return p ; } onWebIdChange ( async ( webId ) => { profile = null ; observationLocation = null ; fitnessData . loading = true ; fitnessData . heartRate = [ ] ; fitnessData . steps = [ ] ; fitnessData . distance = [ ] ; syncEvents . emit ( 'fitnessData' , fitnessData . loading ) ; if ( webId == null ) { fitnessData . loading = false ; syncEvents . emit ( 'profile' , profile ) ; return ; } let success = false ; try { const store = rdf . graph ( ) ; const fetcher = new rdf . Fetcher ( store , { fetch : authenticatedFetch } ) ; setSyncStatus ( 'Loading profile' ) ; profile = await fetchProfile ( store , fetcher , webId ) ; syncEvents . emit ( 'profile' , profile ) ; if ( ! profile . privateTypeIndex ) { throw new Error ( 'No private type index' ) ; } setSyncStatus ( 'Loading private type index' ) ; observationLocation = await fetchTypeIndex ( store , fetcher , webId , profile . privateTypeIndex ) ; setSyncStatus ( 'Loading data from pod' ) ; await fetchObservations ( store , fetcher , webId , observationLocation ) ; success = true ; setSyncStatus ( 'Done' , 1 , 1 ) ; } catch ( err ) { console . warn ( err ) ; setSyncStatus ( 'Could not load profile' , 0 , 1 ) ; Alert . alert ( 'Could not load profile' , err . toString ( ) ) ; } fitnessData . loading = false ; syncEvents . emit ( 'fitnessData' , fitnessData . loading ) ; if ( success ) { const end = moment . utc ( ) . endOf ( 'month' ) ; const start = end . clone ( ) . startOf ( 'month' ) ; syncData ( start , end ) ; } } ) ; async function fetchProfile ( store , fetcher , webId ) { await fetcher . load ( webId ) ; const profile = { webId , friends : [ ] } ; const user = store . sym ( webId ) ; const name = store . any ( user , FOAF ( 'name' ) ) ; if ( name && name . value ) { profile . name = name . value ; } const image = store . any ( user , FOAF ( 'img' ) ) ; if ( image && image . value ) { profile . image = image . value ; } const friends = store . each ( user , FOAF ( 'knows' ) ) ; profile . friends = friends . map ( friend => friend . value ) ; const privateTypeIndex = store . any ( user , SOLID ( 'privateTypeIndex' ) ) ; if ( privateTypeIndex && privateTypeIndex . value ) { profile . privateTypeIndex = privateTypeIndex . value ; } return profile ; } async function fetchTypeIndex ( store , fetcher , webId , typeIndex ) { await fetcher . load ( typeIndex ) ; let observationLocation = null ; const observationTypeRegistration = store . any ( null , SOLID ( 'forClass' ) , FHIR ( 'Observation' ) ) ; if ( observationTypeRegistration && observationTypeRegistration . value ) { const observationTypeFile = store . any ( observationTypeRegistration as rdf . NamedNode , SOLID ( 'instance' ) ) ; const observationTypeDirectory = store . any ( observationTypeRegistration as rdf . NamedNode , SOLID ( 'instanceContainer' ) ) ; if ( observationTypeFile && observationTypeFile . value ) { observationLocation = observationTypeFile . value ; } else if ( observationTypeDirectory && observationTypeDirectory . value ) { observationLocation = new URL ( './fitness.ttl' , observationTypeDirectory . value ) . href ; } } if ( observationLocation == null ) { console . log ( 'Observation location not found, creating' ) ; const defaultLocation = '/private/health/' ; const query = `INSERT DATA {
      <#FHIRObservation> a <http://www.w3.org/ns/solid/terms#TypeRegistration> ;
        <http://www.w3.org/ns/solid/terms#forClass> <http://hl7.org/fhir/Observation> ;
        <http://www.w3.org/ns/solid/terms#instanceContainer> <${ defaultLocation }> .
        <> <http://purl.org/dc/terms/references> <#FHIRObservation> .
      }` ; const ret = await authenticatedFetch ( typeIndex , { method : 'PATCH' , headers : { 'Content-Type' : 'application/sparql-update' } , body : query , credentials : 'include' , } ) ; if ( ! ret . ok ) { throw new Error ( 'Type registration insert unsuccessful: response ' + ret . status ) ; } console . log ( 'Added triple to private type index' ) ; const privateFolder = new URL ( '/private' , typeIndex ) . href ; await authenticatedFetch ( privateFolder , { method : 'POST' , headers : { 'Content-Type' : 'text/turtle' , 'Link' : '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"' , 'Slug' : 'health' , } , body : '<> <http://purl.org/dc/terms/title> "FHIR Health Observations" .' , credentials : 'include' , } ) ; console . log ( 'Created /private/health' ) ; observationLocation = new URL ( defaultLocation + 'fitness.ttl' , typeIndex ) . href ; } return observationLocation ; } async function fetchObservations ( store , fetcher , webId , observationLocation ) { try { await fetcher . load ( observationLocation ) ; } catch ( err ) { await authenticatedFetch ( observationLocation , { method : 'PATCH' , headers : { 'Content-Type' : 'application/sparql-update' } , body : `INSERT DATA {
        <${ webId }> a <http://hl7.org/fhir/Patient> .
        <> a <http://www.w3.org/2002/07/owl#Ontology>;
          <http://www.w3.org/2002/07/owl#imports> <http://hl7.org/fhir/fhir.ttl>.
      }` , credentials : 'include' , } ) ; console . log ( 'Populated ' + observationLocation ) ; } function getSubject ( observation ) { const subject = store . any ( observation , FHIR ( 'Observation.subject' ) ) ; if ( ! subject ) return null ; const patient = store . any ( subject as rdf . BlankNode , FHIR ( 'link' ) ) ; return patient ? patient . value : null ; } function getEffectiveDateTime ( observation ) { const effectiveDateTime = store . any ( observation , FHIR ( 'Observation.effectiveDateTime' ) ) ; if ( ! effectiveDateTime ) return null ; const effectiveDateTimeValue = store . any ( effectiveDateTime as rdf . BlankNode , FHIR ( 'value' ) ) ; if ( ! effectiveDateTimeValue || effectiveDateTimeValue . termType !== 'Literal' ) return null ; const dateTimeLiteral = effectiveDateTimeValue as rdf . Literal ; if ( dateTimeLiteral . datatype . value !== 'http://www.w3.org/2001/XMLSchema#date' ) return null ; return dateTimeLiteral . value ; } function getValue ( observation ) { const valueQuantity = store . any ( observation , FHIR ( 'Observation.valueQuantity' ) ) ; if ( ! valueQuantity ) return null ; const quantityValue = store . any ( valueQuantity as rdf . BlankNode , FHIR ( 'Quantity.value' ) ) ; if ( ! quantityValue ) return null ; const quantityValueValue = store . any ( quantityValue as rdf . BlankNode , FHIR ( 'value' ) ) ; if ( ! quantityValueValue || quantityValueValue . termType !== 'Literal' ) return null ; const quantityValueValueLiteral = quantityValueValue as rdf . Literal ; if ( quantityValueValueLiteral . datatype . value !== 'http://www.w3.org/2001/XMLSchema#decimal' && quantityValueValueLiteral . datatype . value !== 'http://www.w3.org/2001/XMLSchema#integer' ) return null ; const value = parseFloat ( quantityValueValueLiteral . value ) ; return isNaN ( value ) ? null : value ; } function getType ( observation ) { const code = store . any ( observation , FHIR ( 'Observation.code' ) ) ; if ( ! code ) return null ; const coding = store . any ( code as rdf . BlankNode , FHIR ( 'CodeableConcept.coding' ) ) ; if ( ! coding ) return null ; const codingSystem = store . any ( coding as rdf . BlankNode , FHIR ( 'Coding.system' ) ) ; const codingCode = store . any ( coding as rdf . BlankNode , FHIR ( 'Coding.code' ) ) ; if ( ! codingSystem || ! codingCode ) return null ; const codingSytemValue = store . any ( codingSystem as rdf . BlankNode , FHIR ( 'value' ) ) ; const codingCodeValue = store . any ( codingCode as rdf . BlankNode , FHIR ( 'value' ) ) ; if ( ! codingSytemValue || ! codingCodeValue ) return null ; if ( codingSytemValue . value === 'http://loinc.org' && codingCodeValue . value === '8867-4' ) { return 'heartrate' ; } if ( codingSytemValue . value === 'http://loinc.org' && codingCodeValue . value === '55423-8' ) { return 'steps' ; } if ( codingSytemValue . value === 'http://loinc.org' && codingCodeValue . value === '41953-1' ) { return 'distance' ; } return null ; } for ( const observation of store . each ( null , RDF ( 'type' ) , FHIR ( 'Observation' ) ) ) { const subject = getSubject ( observation as rdf . NamedNode ) ; const effectiveDateTime = getEffectiveDateTime ( observation as rdf . NamedNode ) ; const value = getValue ( observation as rdf . NamedNode ) ; const type = getType ( observation as rdf . NamedNode ) ; if ( subject !== webId || effectiveDateTime == null || value == null ) continue ; const dataPoint = { uri : observation . value , parsedDate : new Date ( effectiveDateTime ) , date : effectiveDateTime , value } ; switch ( type ) { case 'heartrate' : fitnessData . heartRate . push ( dataPoint ) ; break ; case 'steps' : fitnessData . steps . push ( dataPoint ) ; break ; case 'distance' : fitnessData . distance . push ( dataPoint ) ; break ; } } } function waitForFitnessData ( ) { return new Promise ( ( resolve , reject ) => { if ( ! fitnessData . loading ) return resolve ( ) ; const fitnessListener = ( ) => { if ( ! fitnessData . loading ) { resolve ( ) ; syncEvents . removeListener ( 'fitnessData' , fitnessListener ) ; } } ; syncEvents . on ( 'fitnessData' , fitnessListener ) ; } ) ; } export async function getHeartRate ( startDate , endDate ) { if ( typeof startDate === 'string' ) { startDate = new Date ( startDate ) ; } else if ( ! ( startDate instanceof Date ) ) { startDate = startDate . toDate ( ) ; } if ( typeof endDate === 'string' ) { endDate = new Date ( endDate ) ; } else if ( ! ( endDate instanceof Date ) ) { endDate = endDate . toDate ( ) ; } await waitForFitnessData ( ) ; return fitnessData . heartRate . filter ( point => ( startDate <= point . parsedDate && point . parsedDate <= endDate ) ) ; } export async function getDailySteps ( startDate , endDate ) { if ( typeof startDate === 'string' ) { startDate = new Date ( startDate ) ; } else if ( ! ( startDate instanceof Date ) ) { startDate = startDate . toDate ( ) ; } if ( typeof endDate === 'string' ) { endDate = new Date ( endDate ) ; } else if ( ! ( endDate instanceof Date ) ) { endDate = endDate . toDate ( ) ; } await waitForFitnessData ( ) ; return fitnessData . steps . filter ( point => ( startDate <= point . parsedDate && point . parsedDate <= endDate ) ) ; } export async function getDailyDistance ( startDate , endDate ) { if ( typeof startDate === 'string' ) { startDate = new Date ( startDate ) ; } else if ( ! ( startDate instanceof Date ) ) { startDate = startDate . toDate ( ) ; } if ( typeof endDate === 'string' ) { endDate = new Date ( endDate ) ; } else if ( ! ( endDate instanceof Date ) ) { endDate = endDate . toDate ( ) ; } await waitForFitnessData ( ) ; return fitnessData . distance . filter ( point => ( startDate <= point . parsedDate && point . parsedDate <= endDate ) ) ; } async function syncData ( start , end ) { const month = end . format ( 'MMMM YYYY' ) ; try { const [ podSteps , podDistance , podHeartRate , googleSteps , googleDistance , googleHeartRate , ] = await Promise . all < PodDataPoint [ ] , PodDataPoint [ ] , PodDataPoint [ ] , DataPoint [ ] , DataPoint [ ] , DataPoint [ ] > ( [ getDailySteps ( start , end ) , getDailyDistance ( start , end ) , getHeartRate ( start , end ) , getGoogleFitSteps ( start . format ( ) , end . format ( ) ) , getGoogleFitDistance ( start . format ( ) , end . format ( ) ) , getGoogleFitHeartRate ( start . format ( ) , end . format ( ) ) , ] ) ; const stepsToUpload = [ ] ; const distanceToUpload = [ ] ; const heartRateToUpload = [ ] ; const pointsToModify = [ ] ; for ( const googlePoint of googleSteps ) { const googleTime = new Date ( googlePoint . date ) . getTime ( ) ; let found = false ; for ( const podPoint of podSteps ) { const podTime = podPoint . parsedDate . getTime ( ) ; if ( googleTime === podTime ) { if ( Math . abs ( googlePoint . value - podPoint . value ) > 0.01 ) { pointsToModify . push ( [ podPoint , googlePoint . value ] ) ; } found = true ; break ; } } if ( ! found ) { stepsToUpload . push ( googlePoint ) ; } } for ( const googlePoint of googleDistance ) { const googleTime = new Date ( googlePoint . date ) . getTime ( ) ; let found = false ; for ( const podPoint of podDistance ) { const podTime = podPoint . parsedDate . getTime ( ) ; if ( googleTime === podTime ) { if ( Math . abs ( googlePoint . value - podPoint . value ) > 0.01 ) { pointsToModify . push ( [ podPoint , googlePoint . value ] ) ; } found = true ; break ; } } if ( ! found ) { distanceToUpload . push ( googlePoint ) ; } } for ( let i = 0 ; i < googleHeartRate . length ; i ++ ) { const googlePoint = googleHeartRate [ i ] ; const googleTime = new Date ( googlePoint . date ) . getTime ( ) ; if ( i > 0 ) { const lastPoint = googleHeartRate [ i - 1 ] ; if ( Math . abs ( googlePoint . value - lastPoint . value ) <= 1 && Math . abs ( googleTime - new Date ( lastPoint . date ) . getTime ( ) ) <= 60000 ) { continue ; } } let found = false ; for ( const podPoint of podHeartRate ) { const podTime = podPoint . parsedDate . getTime ( ) ; if ( googleTime === podTime ) { found = true ; break ; } } if ( ! found ) { heartRateToUpload . push ( googlePoint ) ; } } console . log ( `Points to modify: ${ pointsToModify . length }` ) ; console . log ( `Steps to upload: ${ stepsToUpload . length }` ) ; console . log ( `Distance to upload: ${ distanceToUpload . length }` ) ; console . log ( `Heart rate to upload: ${ heartRateToUpload . length }` ) ; if ( pointsToModify . length !== 0 ) { setSyncStatus ( 'Updating pod data for ' + month , 0 , pointsToModify . length ) ; } for ( let i = 0 ; i < pointsToModify . length ; i ++ ) { const [ point , newValue ] = pointsToModify [ i ] ; const type = Math . floor ( newValue ) === newValue ? 'integer' : 'decimal' ; const query = `DELETE {
        ?s <http://hl7.org/fhir/value> ?o
      } INSERT {
        ?s <http://hl7.org/fhir/value> "${ newValue }"^^<http://www.w3.org/2001/XMLSchema#${ type }>
      } WHERE {
        <${ point . uri }> <http://hl7.org/fhir/Observation.valueQuantity> [
          <http://hl7.org/fhir/Quantity.value> ?s
        ] .
        ?s <http://hl7.org/fhir/value> ?o
      }` ; console . log ( query ) ; const ret = await authenticatedFetch ( observationLocation , { method : 'PATCH' , headers : { 'Content-Type' : 'application/sparql-update' } , body : query , credentials : 'include' , } ) ; if ( ! ret . ok ) { let txt = '' ; try { txt = await ret . text ( ) ; } catch ( _ ) { } throw new Error ( `Invalid status: ${ ret . status } ${ txt }` ) ; } setSyncStatus ( 'Updating pod data for ' + month , i + 1 , pointsToModify . length ) ; } let observationsToUpload = [ ] ; for ( const point of stepsToUpload ) { const { observation , uri } = stepsToObservation ( point ) ; observationsToUpload . push ( observation ) ; fitnessData . steps . push ( { date : point . date , value : point . value , parsedDate : new Date ( point . date ) , uri , } ) ; } for ( const point of distanceToUpload ) { const { observation , uri } = distanceToObservation ( point ) ; observationsToUpload . push ( observation ) ; fitnessData . distance . push ( { date : point . date , value : point . value , parsedDate : new Date ( point . date ) , uri , } ) ; } for ( const point of heartRateToUpload ) { const { observation , uri } = heartRateToObservation ( point ) ; observationsToUpload . push ( observation ) ; fitnessData . heartRate . push ( { date : point . date , value : point . value , parsedDate : new Date ( point . date ) , uri , } ) ; } syncEvents . emit ( 'fitness' , fitnessData . loading ) ; const needToUpload = observationsToUpload . length ; let totalUploaded = 0 ; setSyncStatus ( 'Syncing data to pod for ' + month , totalUploaded , needToUpload ) ; while ( observationsToUpload . length !== 0 ) { let batch ; if ( observationsToUpload . length > 50 ) { batch = observationsToUpload . slice ( 0 , 50 ) ; observationsToUpload = observationsToUpload . slice ( 50 ) ; } else { batch = observationsToUpload ; observationsToUpload = [ ] ; } const query = 'INSERT DATA {\n' + batch . join ( '\n' ) + '\n}' ; const ret = await authenticatedFetch ( observationLocation , { method : 'PATCH' , headers : { 'Content-Type' : 'application/sparql-update' } , body : query , credentials : 'include' , } ) ; if ( ! ret . ok ) { let txt = '' ; try { txt = await ret . text ( ) ; } catch ( _ ) { } throw new Error ( `Invalid status: ${ ret . status } ${ txt }` ) ; } totalUploaded += batch . length ; setSyncStatus ( 'Syncing data to pod for ' + month , totalUploaded , needToUpload ) ; } if ( needToUpload !== 0 ) { syncData ( start . clone ( ) . subtract ( 1 , 'month' ) , end . clone ( ) . subtract ( 1 , 'month' ) ) ; } else { setSyncStatus ( 'Everything up to date' , 1 , 1 ) ; } } catch ( err ) { console . warn ( err ) ; setSyncStatus ( 'Could not sync data' , 0 , 1 ) ; Alert . alert ( 'Could not sync data' , err . toString ( ) ) ; } } function stepsToObservation ( { date , value } ) { const webId = profile ?. webId ; const id = '#steps_' + moment ( date ) . format ( 'YYYYMMDD' ) ; const observation = `<${ id }> a <http://hl7.org/fhir/Observation>;
<http://hl7.org/fhir/nodeRole> <http://hl7.org/fhir/treeRoot>;
<http://hl7.org/fhir/Observation.status> [ <http://hl7.org/fhir/value> "final"];
<http://hl7.org/fhir/Observation.code> [
  <http://hl7.org/fhir/CodeableConcept.coding> [
    <http://hl7.org/fhir/index> 0;
    a <http://loinc.org/rdf#55423-8>;
    <http://hl7.org/fhir/Coding.system> [ <http://hl7.org/fhir/value> "http://loinc.org" ];
    <http://hl7.org/fhir/Coding.code> [ <http://hl7.org/fhir/value> "55423-8" ];
    <http://hl7.org/fhir/Coding.display> [ <http://hl7.org/fhir/value> "Step count" ]
  ];
  <http://hl7.org/fhir/CodeableConcept.text> [ <http://hl7.org/fhir/value> "Step count" ]
];
<http://hl7.org/fhir/Observation.subject> [
  <http://hl7.org/fhir/link> <${ webId }>;
];
<http://hl7.org/fhir/Observation.effectiveDateTime> [ <http://hl7.org/fhir/value> "${ date }"^^<http://www.w3.org/2001/XMLSchema#date> ];
<http://hl7.org/fhir/Observation.valueQuantity> [
  <http://hl7.org/fhir/Quantity.value> [ <http://hl7.org/fhir/value> "${ value }"^^<http://www.w3.org/2001/XMLSchema#integer> ];
  <http://hl7.org/fhir/Quantity.unit> [ <http://hl7.org/fhir/value> "/d" ];
  <http://hl7.org/fhir/Quantity.system> [ <http://hl7.org/fhir/value> "http://unitsofmeasure.org" ];
  <http://hl7.org/fhir/Quantity.code> [ <http://hl7.org/fhir/value> "/d" ]
] .
<> <http://purl.org/dc/terms/references> <${ id }> .` ; return { uri : id , observation } ; } function distanceToObservation ( { date , value } ) { const webId = profile ?. webId ; const id = '#distance_' + moment ( date ) . format ( 'YYYYMMDD' ) ; const observation = `<${ id }> a <http://hl7.org/fhir/Observation>;
<http://hl7.org/fhir/nodeRole> <http://hl7.org/fhir/treeRoot>;
<http://hl7.org/fhir/Observation.status> [ <http://hl7.org/fhir/value> "final"];
<http://hl7.org/fhir/Observation.code> [
  <http://hl7.org/fhir/CodeableConcept.coding> [
    <http://hl7.org/fhir/index> 0;
    a <http://loinc.org/rdf#41953-1>;
    <http://hl7.org/fhir/Coding.system> [ <http://hl7.org/fhir/value> "http://loinc.org" ];
    <http://hl7.org/fhir/Coding.code> [ <http://hl7.org/fhir/value> "41953-1" ];
    <http://hl7.org/fhir/Coding.display> [ <http://hl7.org/fhir/value> "Distanced walked" ]
  ];
  <http://hl7.org/fhir/CodeableConcept.text> [ <http://hl7.org/fhir/value> "Distanced walked" ]
];
<http://hl7.org/fhir/Observation.subject> [
  <http://hl7.org/fhir/link> <${ webId }>;
];
<http://hl7.org/fhir/Observation.effectiveDateTime> [ <http://hl7.org/fhir/value> "${ date }"^^<http://www.w3.org/2001/XMLSchema#date> ];
<http://hl7.org/fhir/Observation.valueQuantity> [
  <http://hl7.org/fhir/Quantity.value> [ <http://hl7.org/fhir/value> "${ value }"^^<http://www.w3.org/2001/XMLSchema#decimal> ];
  <http://hl7.org/fhir/Quantity.unit> [ <http://hl7.org/fhir/value> "m/d" ];
  <http://hl7.org/fhir/Quantity.system> [ <http://hl7.org/fhir/value> "http://unitsofmeasure.org" ];
  <http://hl7.org/fhir/Quantity.code> [ <http://hl7.org/fhir/value> "/d" ]
] .
<> <http://purl.org/dc/terms/references> <${ id }> .` ; return { uri : id , observation } ; } function heartRateToObservation ( { date , value } ) { const webId = profile ?. webId ; const id = '#heartrate_' + moment ( date ) . format ( 'x' ) ; const observation = `<${ id }> a <http://hl7.org/fhir/Observation>;
<http://hl7.org/fhir/nodeRole> <http://hl7.org/fhir/treeRoot>;
<http://hl7.org/fhir/Observation.status> [ <http://hl7.org/fhir/value> "final"];
<http://hl7.org/fhir/Observation.code> [
  <http://hl7.org/fhir/CodeableConcept.coding> [
    <http://hl7.org/fhir/index> 0;
    a <http://loinc.org/rdf#8867-4>;
    <http://hl7.org/fhir/Coding.system> [ <http://hl7.org/fhir/value> "http://loinc.org" ];
    <http://hl7.org/fhir/Coding.code> [ <http://hl7.org/fhir/value> "8867-4" ];
    <http://hl7.org/fhir/Coding.display> [ <http://hl7.org/fhir/value> "Heart rate" ]
  ];
  <http://hl7.org/fhir/CodeableConcept.text> [ <http://hl7.org/fhir/value> "Heart rate" ]
];
<http://hl7.org/fhir/Observation.subject> [
  <http://hl7.org/fhir/link> <${ webId }>;
];
<http://hl7.org/fhir/Observation.effectiveDateTime> [ <http://hl7.org/fhir/value> "${ date }"^^<http://www.w3.org/2001/XMLSchema#date> ];
<http://hl7.org/fhir/Observation.valueQuantity> [
  <http://hl7.org/fhir/Quantity.value> [ <http://hl7.org/fhir/value> "${ value }"^^<http://www.w3.org/2001/XMLSchema#decimal> ];
  <http://hl7.org/fhir/Quantity.unit> [ <http://hl7.org/fhir/value> "beats/min" ];
  <http://hl7.org/fhir/Quantity.system> [ <http://hl7.org/fhir/value> "http://unitsofmeasure.org" ];
  <http://hl7.org/fhir/Quantity.code> [ <http://hl7.org/fhir/value> "/min" ]
] .
<> <http://purl.org/dc/terms/references> <${ id }> .` ; return { uri : id , observation } ; }