diff --git a/.github/scripts/graphql.sh b/.github/scripts/graphql.sh new file mode 100755 index 000000000..f76ad3a8d --- /dev/null +++ b/.github/scripts/graphql.sh @@ -0,0 +1,11 @@ +#!/bin/bash -e + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +. "$SCRIPT_DIR/util.sh" + +BASE="http://localhost:8080/fhir" +TYPE=$1 +EXPECTED_SIZE=$(curl -s "$BASE/${TYPE}?_summary=count" | jq -r .total) +ACTUAL_SIZE=$(curl -s -H "Content-Type: application/graphql" -d "{ ${TYPE}List { id } }" "$BASE/\$graphql" | jq ".data.${TYPE}List | length") + +test "size" "$ACTUAL_SIZE" "$EXPECTED_SIZE" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6343f94f..c8a9a24c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,6 +59,7 @@ jobs: - luid - metrics - openid-auth + - operation-graphql - operation-measure-evaluate-measure - page-store - page-store-cassandra @@ -531,6 +532,12 @@ jobs: - name: Conditional Update If-None-Match run: .github/scripts/conditional-update-if-none-match.sh + - name: GraphQL Patient + run: .github/scripts/graphql.sh Patient + + - name: GraphQL Observation + run: .github/scripts/graphql.sh Observation + not-enforcing-referential-integrity-test: needs: build runs-on: ubuntu-22.04 @@ -1238,6 +1245,12 @@ jobs: - name: Conditional Update If-None-Match run: .github/scripts/conditional-update-if-none-match.sh + - name: GraphQL Patient + run: .github/scripts/graphql.sh Patient + + - name: GraphQL Observation + run: .github/scripts/graphql.sh Observation + - name: Docker Stats run: docker stats --no-stream diff --git a/deps.edn b/deps.edn index 0952670f1..f84f104ec 100644 --- a/deps.edn +++ b/deps.edn @@ -10,6 +10,9 @@ blaze/interaction {:local/root "modules/interaction"} + blaze.operation/graphql + {:local/root "modules/operation-graphql"} + blaze.operation/measure-evaluate-measure {:local/root "modules/operation-measure-evaluate-measure"} @@ -80,7 +83,7 @@ :outdated {:replace-deps {com.github.liquidz/antq - {:mvn/version "2.2.992"} + {:mvn/version "2.2.999"} org.slf4j/slf4j-nop {:mvn/version "2.0.6"}} @@ -114,6 +117,7 @@ "-d" "modules/metrics" "-d" "modules/module-base" "-d" "modules/openid-auth" + "-d" "modules/operation-graphql" "-d" "modules/operation-measure-evaluate-measure" "-d" "modules/page-store" "-d" "modules/page-store-cassandra" @@ -127,6 +131,8 @@ "-d" "modules/test-util" "-d" "modules/thread-pool-executor-collector" "--exclude" "com.taoensso/timbre" + "--exclude" "org.antlr/antlr4" "--exclude" "org.eclipse.jetty/jetty-server" "--exclude" "org.clojure/alpha.spec" + "--exclude" "com.walmartlabs/lacinia" "--exclude" "lambdaisland/kaocha"]}}} diff --git a/docs/performance.md b/docs/performance.md index 82887f884..609eaf0cc 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -4,6 +4,10 @@ A section about FHIR Search performance can be found [here](performance/fhir-search.md). +## GraphQL + +A section about GraphQL performance can be found [here](performance/graphql.md). + ## Transaction Bundle Upload - Summary | CPU | # Cores | RAM (GB) | Xmx | MBJ³ | -c¹ | # Resources | Disk Util.² | Duration (s) | Resources/s | diff --git a/docs/performance/graphql.md b/docs/performance/graphql.md new file mode 100644 index 000000000..10430cd4b --- /dev/null +++ b/docs/performance/graphql.md @@ -0,0 +1,32 @@ +# GraphQL + +## Simple Code Search + +In this section, GraphQL for selecting Observation resources with a certain code is used. + +### Download of Resources + +All measurements are done after Blaze is in a steady state with all resources to download in it's resource cache in order to cancel out resource load times from disk or file system cache. + +Download is done using the following `curl` command: + +```sh +curl -s -H "Content-Type: application/graphql" -d "{ ObservationList { subject { reference } } }" "http://localhost:8080/\$graphql" > /dev/null" +``` + +| CPU | Heap Mem | Block Cache | # Res. ¹ | # Obs. ² | Code | # Hits | Time (s) | T / 1M ³ | +|------------|---------:|------------:|---------:|---------:|---------|-------:|---------:|---------:| +| EPYC 7543P | 8 GB | 1 GB | 29 M | 28 M | 17861-6 | 171 k | 1.045 | 6.11 | +| EPYC 7543P | 8 GB | 1 GB | 29 M | 28 M | 39156-5 | 967 k | 5.740 | 5.94 | +| EPYC 7543P | 8 GB | 1 GB | 29 M | 28 M | 29463-7 | 1.3 M | 8.057 | 6.20 | +| EPYC 7543P | 30 GB | 10 GB | 292 M | 278 M | 17861-6 | 1.7 M | 10.744 | 6.32 | +| EPYC 7543P | 30 GB | 10 GB | 292 M | 278 M | 39156-5 | 9.7 M | 70.122 | 7.23 | +| EPYC 7543P | 30 GB | 10 GB | 292 M | 278 M | 29463-7 | 13 M | 96.735 | 7.44 | + +¹ Number of Resources, ² Number of Observations, ³ Time in seconds per 1 million resources, The amount of system memory was 128 GB in all cases. + +According to the measurements, the time needed by Blaze to deliver Observations containing only the subject reference is about **twice as fast** as returning the same information via [Subsetted FHIR Search](fhir-search.md#download-of-resources-with-subsetting) and **4 times as fast** as downloading the whole Observation Resources using [FHIR Search](fhir-search.md#download-of-resources). + +## Used Dataset + +The dataset was the same as in [FHIR Search](fhir-search.md) performance tests. diff --git a/modules/interaction/.clj-kondo/config.edn b/modules/interaction/.clj-kondo/config.edn index 78125243f..a5d4f5356 100644 --- a/modules/interaction/.clj-kondo/config.edn +++ b/modules/interaction/.clj-kondo/config.edn @@ -4,27 +4,16 @@ blaze.async.comp/do-sync clojure.core/let blaze.db.api-stub/with-system-data clojure.core/with-open blaze.interaction.create-test/with-handler clojure.core/fn - blaze.interaction.create-test/with-handler-data clojure.core/fn blaze.interaction.delete-test/with-handler clojure.core/fn - blaze.interaction.delete-test/with-handler-data clojure.core/fn blaze.interaction.history.instance-test/with-handler clojure.core/fn - blaze.interaction.history.instance-test/with-handler-data clojure.core/fn blaze.interaction.history.system-test/with-handler clojure.core/fn - blaze.interaction.history.system-test/with-handler-data clojure.core/fn blaze.interaction.history.type-test/with-handler clojure.core/fn - blaze.interaction.history.type-test/with-handler-data clojure.core/fn blaze.interaction.read-test/with-handler clojure.core/fn - blaze.interaction.read-test/with-handler-data clojure.core/fn blaze.interaction.search-compartment-test/with-handler clojure.core/fn - blaze.interaction.search-compartment-test/with-handler-data clojure.core/fn blaze.interaction.search-system-test/with-handler clojure.core/fn - blaze.interaction.search-system-test/with-handler-data clojure.core/fn blaze.interaction.search-type-test/with-handler clojure.core/fn - blaze.interaction.search-type-test/with-handler-data clojure.core/fn blaze.interaction.transaction-test/with-handler clojure.core/fn - blaze.interaction.transaction-test/with-handler-data clojure.core/fn blaze.interaction.update-test/with-handler clojure.core/fn - blaze.interaction.update-test/with-handler-data clojure.core/fn blaze.test-util/with-system clojure.core/with-open} :linters diff --git a/modules/interaction/test/blaze/interaction/create_test.clj b/modules/interaction/test/blaze/interaction/create_test.clj index 6da134eba..de1104d37 100644 --- a/modules/interaction/test/blaze/interaction/create_test.clj +++ b/modules/interaction/test/blaze/interaction/create_test.clj @@ -12,7 +12,7 @@ [blaze.fhir.response.create-spec] [blaze.fhir.spec.type] [blaze.interaction.create] - [blaze.interaction.test-util :as itu :refer [wrap-error]] + [blaze.interaction.test-util :refer [wrap-error]] [blaze.interaction.util-spec] [blaze.test-util :as tu :refer [given-thrown with-system]] [clojure.spec.alpha :as s] @@ -95,7 +95,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{handler# :blaze.interaction/create} system] ~txs (let [~handler-binding (-> handler# wrap-defaults wrap-error)] diff --git a/modules/interaction/test/blaze/interaction/delete_test.clj b/modules/interaction/test/blaze/interaction/delete_test.clj index 198612d6b..762e65435 100644 --- a/modules/interaction/test/blaze/interaction/delete_test.clj +++ b/modules/interaction/test/blaze/interaction/delete_test.clj @@ -6,7 +6,6 @@ [blaze.db.api-stub :refer [mem-node-system with-system-data]] [blaze.executors :as ex] [blaze.interaction.delete] - [blaze.interaction.test-util :as itu] [blaze.test-util :as tu :refer [given-thrown]] [clojure.spec.alpha :as s] [clojure.spec.test.alpha :as st] @@ -55,7 +54,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{handler# :blaze.interaction/delete} system] ~txs (let [~handler-binding handler#] diff --git a/modules/interaction/test/blaze/interaction/history/instance_test.clj b/modules/interaction/test/blaze/interaction/history/instance_test.clj index caf2fd3ba..a611ee2ba 100644 --- a/modules/interaction/test/blaze/interaction/history/instance_test.clj +++ b/modules/interaction/test/blaze/interaction/history/instance_test.clj @@ -9,7 +9,7 @@ [blaze.db.api-stub :refer [mem-node-system with-system-data]] [blaze.interaction.history.instance] [blaze.interaction.history.util-spec] - [blaze.interaction.test-util :as itu :refer [wrap-error]] + [blaze.interaction.test-util :refer [wrap-error]] [blaze.middleware.fhir.db :refer [wrap-db]] [blaze.middleware.fhir.db-spec] [blaze.test-util :as tu :refer [given-thrown]] @@ -91,7 +91,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{node# :blaze.db/node handler# :blaze.interaction.history/instance} system] ~txs diff --git a/modules/interaction/test/blaze/interaction/history/system_test.clj b/modules/interaction/test/blaze/interaction/history/system_test.clj index 896f8620f..9730fdb78 100644 --- a/modules/interaction/test/blaze/interaction/history/system_test.clj +++ b/modules/interaction/test/blaze/interaction/history/system_test.clj @@ -8,7 +8,6 @@ [blaze.db.api-stub :refer [mem-node-system with-system-data]] [blaze.interaction.history.system] [blaze.interaction.history.util-spec] - [blaze.interaction.test-util :as itu] [blaze.middleware.fhir.db :refer [wrap-db]] [blaze.middleware.fhir.db-spec] [blaze.test-util :as tu :refer [given-thrown]] @@ -93,7 +92,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{node# :blaze.db/node handler# :blaze.interaction.history/system} system] ~txs diff --git a/modules/interaction/test/blaze/interaction/history/type_test.clj b/modules/interaction/test/blaze/interaction/history/type_test.clj index 719510464..14172afc7 100644 --- a/modules/interaction/test/blaze/interaction/history/type_test.clj +++ b/modules/interaction/test/blaze/interaction/history/type_test.clj @@ -8,7 +8,6 @@ [blaze.db.api-stub :refer [mem-node-system with-system-data]] [blaze.interaction.history.type] [blaze.interaction.history.util-spec] - [blaze.interaction.test-util :as itu] [blaze.middleware.fhir.db :refer [wrap-db]] [blaze.middleware.fhir.db-spec] [blaze.test-util :as tu :refer [given-thrown]] @@ -94,7 +93,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{node# :blaze.db/node handler# :blaze.interaction.history/type} system] ~txs diff --git a/modules/interaction/test/blaze/interaction/read_test.clj b/modules/interaction/test/blaze/interaction/read_test.clj index 4f82d2982..83576f32c 100644 --- a/modules/interaction/test/blaze/interaction/read_test.clj +++ b/modules/interaction/test/blaze/interaction/read_test.clj @@ -9,7 +9,7 @@ [blaze.db.api-stub :refer [mem-node-system with-system-data]] [blaze.db.spec] [blaze.interaction.read] - [blaze.interaction.test-util :as itu :refer [wrap-error]] + [blaze.interaction.test-util :refer [wrap-error]] [blaze.middleware.fhir.db :refer [wrap-db]] [blaze.middleware.fhir.db-spec] [blaze.test-util :as tu] @@ -43,7 +43,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{node# :blaze.db/node handler# :blaze.interaction/read} system] ~txs diff --git a/modules/interaction/test/blaze/interaction/search_compartment_test.clj b/modules/interaction/test/blaze/interaction/search_compartment_test.clj index 58975055b..fe431c64c 100644 --- a/modules/interaction/test/blaze/interaction/search_compartment_test.clj +++ b/modules/interaction/test/blaze/interaction/search_compartment_test.clj @@ -9,7 +9,7 @@ [blaze.interaction.search.nav-spec] [blaze.interaction.search.params-spec] [blaze.interaction.search.util-spec] - [blaze.interaction.test-util :as itu :refer [wrap-error]] + [blaze.interaction.test-util :refer [wrap-error]] [blaze.middleware.fhir.db :refer [wrap-db]] [blaze.middleware.fhir.db-spec] [blaze.page-store-spec] @@ -99,7 +99,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{node# :blaze.db/node handler# :blaze.interaction/search-compartment} system] ~txs diff --git a/modules/interaction/test/blaze/interaction/search_system_test.clj b/modules/interaction/test/blaze/interaction/search_system_test.clj index 3e73bd47b..03fc3e7bf 100644 --- a/modules/interaction/test/blaze/interaction/search_system_test.clj +++ b/modules/interaction/test/blaze/interaction/search_system_test.clj @@ -8,7 +8,7 @@ [blaze.interaction.search.nav-spec] [blaze.interaction.search.params-spec] [blaze.interaction.search.util-spec] - [blaze.interaction.test-util :as itu :refer [wrap-error]] + [blaze.interaction.test-util :refer [wrap-error]] [blaze.middleware.fhir.db :refer [wrap-db]] [blaze.middleware.fhir.db-spec] [blaze.page-store-spec] @@ -101,7 +101,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{node# :blaze.db/node handler# :blaze.interaction/search-system} system] ~txs diff --git a/modules/interaction/test/blaze/interaction/search_type_test.clj b/modules/interaction/test/blaze/interaction/search_type_test.clj index 9682a8e04..20dfa2bfc 100644 --- a/modules/interaction/test/blaze/interaction/search_type_test.clj +++ b/modules/interaction/test/blaze/interaction/search_type_test.clj @@ -10,7 +10,7 @@ [blaze.interaction.search.nav-spec] [blaze.interaction.search.params-spec] [blaze.interaction.search.util-spec] - [blaze.interaction.test-util :as itu :refer [wrap-error]] + [blaze.interaction.test-util :refer [wrap-error]] [blaze.middleware.fhir.db :refer [wrap-db]] [blaze.middleware.fhir.db-spec] [blaze.page-store-spec] @@ -171,7 +171,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{node# :blaze.db/node handler# :blaze.interaction/search-type} system] ~txs diff --git a/modules/interaction/test/blaze/interaction/test_util.clj b/modules/interaction/test/blaze/interaction/test_util.clj index a85e2062e..562381cf3 100644 --- a/modules/interaction/test/blaze/interaction/test_util.clj +++ b/modules/interaction/test/blaze/interaction/test_util.clj @@ -8,9 +8,3 @@ (fn [request] (-> (handler request) (ac/exceptionally handler-util/error-response)))) - - -(defn extract-txs-body [more] - (if (vector? (first more)) - [(first more) (next more)] - [[] more])) diff --git a/modules/interaction/test/blaze/interaction/transaction_test.clj b/modules/interaction/test/blaze/interaction/transaction_test.clj index 0f2d7fba9..e2ff17cf4 100644 --- a/modules/interaction/test/blaze/interaction/transaction_test.clj +++ b/modules/interaction/test/blaze/interaction/transaction_test.clj @@ -14,7 +14,7 @@ [blaze.interaction.delete] [blaze.interaction.read] [blaze.interaction.search-type] - [blaze.interaction.test-util :as itu :refer [wrap-error]] + [blaze.interaction.test-util :refer [wrap-error]] [blaze.interaction.transaction] [blaze.interaction.update] [blaze.interaction.util-spec] @@ -177,7 +177,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{handler# :blaze.interaction/transaction router# ::router} system] ~txs diff --git a/modules/interaction/test/blaze/interaction/update_test.clj b/modules/interaction/test/blaze/interaction/update_test.clj index b9b985b38..533bff091 100644 --- a/modules/interaction/test/blaze/interaction/update_test.clj +++ b/modules/interaction/test/blaze/interaction/update_test.clj @@ -11,7 +11,7 @@ [blaze.executors :as ex] [blaze.fhir.response.create-spec] [blaze.fhir.spec.type] - [blaze.interaction.test-util :as itu :refer [wrap-error]] + [blaze.interaction.test-util :refer [wrap-error]] [blaze.interaction.update] [blaze.test-util :as tu :refer [given-thrown with-system]] [clojure.spec.alpha :as s] @@ -98,7 +98,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (itu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{handler# :blaze.interaction/update} system] ~txs (let [~handler-binding (-> handler# wrap-defaults wrap-error)] diff --git a/modules/operation-graphql/.clj-kondo/config.edn b/modules/operation-graphql/.clj-kondo/config.edn new file mode 100644 index 000000000..2660210af --- /dev/null +++ b/modules/operation-graphql/.clj-kondo/config.edn @@ -0,0 +1,22 @@ +{:lint-as + {blaze.anomaly/when-ok clojure.core/let + blaze.fhir.operation.graphql-test/with-handler clojure.core/fn + blaze.test-util/with-system clojure.core/with-open} + + :linters + {:unsorted-required-namespaces + {:level :error} + + :single-key-in + {:level :warning} + + :keyword-binding + {:level :error} + + :reduce-without-init + {:level :warning} + + :warn-on-reflection + {:level :warning :warn-only-on-interop true}} + + :skip-comments true} diff --git a/modules/operation-graphql/Makefile b/modules/operation-graphql/Makefile new file mode 100644 index 000000000..e4ae9e7ce --- /dev/null +++ b/modules/operation-graphql/Makefile @@ -0,0 +1,19 @@ +lint: + clj-kondo --lint src test deps.edn + +prep: + clojure -X:deps prep + +test: prep + clojure -M:test:kaocha --profile :ci + +test-coverage: prep + clojure -M:test:coverage + +clean: + rm -rf .clj-kondo/.cache .cpcache target + +deps-tree: + clojure -X:deps tree + +.PHONY: lint prep test test-coverage clean deps-tree diff --git a/modules/operation-graphql/deps.edn b/modules/operation-graphql/deps.edn new file mode 100644 index 000000000..123fc0ad6 --- /dev/null +++ b/modules/operation-graphql/deps.edn @@ -0,0 +1,42 @@ +{:deps + {blaze/module-base + {:local/root "../module-base"} + + blaze/rest-util + {:local/root "../rest-util"} + + blaze/spec + {:local/root "../spec"} + + com.walmartlabs/lacinia + {:git/url "https://github.com/alexanderkiel/lacinia.git" + :git/sha "d3aa6a7a4b452521f5f311683cc996e782f1f59d"} + + org.antlr/antlr4 + {:mvn/version "4.10.1" + :exclusions + [com.ibm.icu/icu4j + org.abego.treelayout/org.abego.treelayout.core + org.glassfish/javax.json]}} + + :aliases + {:test + {:extra-paths ["test"] + + :extra-deps + {blaze/db-stub + {:local/root "../db-stub"}}} + + :kaocha + {:extra-deps + {lambdaisland/kaocha + {:mvn/version "1.71.1119"}} + + :main-opts ["-m" "kaocha.runner"]} + + :coverage + {:extra-deps + {cloverage/cloverage + {:mvn/version "1.2.4"}} + + :main-opts ["-m" "cloverage.coverage" "--codecov" "-p" "src" "-s" "test"]}}} diff --git a/modules/operation-graphql/src/blaze/fhir/operation/graphql.clj b/modules/operation-graphql/src/blaze/fhir/operation/graphql.clj new file mode 100644 index 000000000..487e7f37c --- /dev/null +++ b/modules/operation-graphql/src/blaze/fhir/operation/graphql.clj @@ -0,0 +1,123 @@ +(ns blaze.fhir.operation.graphql + "Main entry point into the $graphql operation." + (:require + [blaze.async.comp :as ac] + [blaze.db.api :as d] + [blaze.executors :as ex] + [blaze.fhir.operation.graphql.spec] + [blaze.middleware.fhir.metrics :refer [wrap-observe-request-duration]] + [clojure.spec.alpha :as s] + [com.walmartlabs.lacinia :as lacinia] + [com.walmartlabs.lacinia.resolve :as resolve] + [com.walmartlabs.lacinia.schema :as ls] + [com.walmartlabs.lacinia.util :as lu] + [integrant.core :as ig] + [ring.util.response :as ring] + [taoensso.timbre :as log]) + (:import + [java.util.concurrent TimeUnit])) + + +(def schema + {:objects + {:Patient + {:fields + {:id {:type 'String} + :gender {:type 'String}}} + + :Observation + {:fields + {:id {:type 'String} + :subject {:type :Reference}}} + + :Reference + {:fields + {:reference {:type 'String}}} + + :Query + {:fields + {:PatientList + {:type '(list :Patient) + :args {:gender {:type 'String}}} + + :ObservationList + {:type '(list :Observation) + :args {:code {:type 'String}}}}}}}) + + +(defn- clauses [args] + (mapv (fn [[key value]] [(name key) value]) args)) + + +(defn- type-query [db type args] + (if (seq args) + (d/type-query db type (clauses args)) + (d/type-list db type))) + + +(defn- resolve-type-list [type {:blaze/keys [db]} args _] + (log/trace (format "execute %sList query" type)) + (let [result (resolve/resolve-promise)] + (-> (d/pull-many db (type-query db type args)) + (ac/when-complete (partial resolve/deliver! result))) + result)) + + +(defn- compile-schema [options] + (-> schema + (lu/inject-resolvers + {:Query/PatientList (partial resolve-type-list "Patient") + :Query/ObservationList (partial resolve-type-list "Observation")}) + (ls/compile options))) + + +(defn- execute-query [schema db {:keys [request-method] :as request}] + (if (= :post request-method) + (let [{{:keys [query variables]} :body} request] + (lacinia/execute schema query variables {:blaze/db db})) + (lacinia/execute schema (get (:params request) "query") nil {:blaze/db db}))) + + +(defn- handler [{:keys [executor] :as context}] + (let [schema (compile-schema context)] + (fn [{:blaze/keys [db] :as request}] + (ac/supply-async + #(ring/response (execute-query schema db request)) + executor)))) + + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req-un [:blaze.db/node ::executor])) + + +(defmethod ig/init-key ::handler [_ context] + (log/info "Init FHIR $graphql operation handler") + (-> (handler context) + (wrap-observe-request-duration "operation-graphql"))) + + + +(defmethod ig/pre-init-spec ::executor [_] + (s/keys :opt-un [::num-threads])) + + +(defn- executor-init-msg [num-threads] + (format "Init $graphql operation executor with %d threads" num-threads)) + + +(defmethod ig/init-key ::executor + [_ {:keys [num-threads] :or {num-threads 4}}] + (log/info (executor-init-msg num-threads)) + (ex/io-pool num-threads "operation-graphql-%d")) + + +(defmethod ig/halt-key! ::executor + [_ executor] + (log/info "Stopping $graphql operation executor...") + (ex/shutdown! executor) + (if (ex/await-termination executor 10 TimeUnit/SECONDS) + (log/info "$graphql operation executor was stopped successfully") + (log/warn "Got timeout while stopping the $graphql operation executor"))) + + +(derive ::executor :blaze.metrics/thread-pool-executor) diff --git a/modules/operation-graphql/src/blaze/fhir/operation/graphql/middleware.clj b/modules/operation-graphql/src/blaze/fhir/operation/graphql/middleware.clj new file mode 100644 index 000000000..9e8229e92 --- /dev/null +++ b/modules/operation-graphql/src/blaze/fhir/operation/graphql/middleware.clj @@ -0,0 +1,9 @@ +(ns blaze.fhir.operation.graphql.middleware + (:require + [blaze.fhir.operation.graphql.middleware.query :refer [wrap-query]] + [integrant.core :as ig])) + + +(defmethod ig/init-key ::query [_ _] + {:name ::query + :wrap wrap-query}) diff --git a/modules/operation-graphql/src/blaze/fhir/operation/graphql/middleware/query.clj b/modules/operation-graphql/src/blaze/fhir/operation/graphql/middleware/query.clj new file mode 100644 index 000000000..30ba516f3 --- /dev/null +++ b/modules/operation-graphql/src/blaze/fhir/operation/graphql/middleware/query.clj @@ -0,0 +1,81 @@ +(ns blaze.fhir.operation.graphql.middleware.query + (:require + [blaze.anomaly :as ba :refer [if-ok when-ok]] + [blaze.async.comp :as ac] + [clojure.string :as str] + [cognitect.anomalies :as anom] + [jsonista.core :as j] + [ring.util.request :as request])) + + +(defn- query-request-graphql [{:keys [body] :as request}] + (if body + (assoc request :body {:query (slurp body)}) + (ba/incorrect "Missing HTTP body."))) + + +(def ^:private object-mapper + (j/object-mapper + {:decode-key-fn true})) + + +(defn- parse-json [body] + (ba/try-all ::anom/incorrect (j/read-value body object-mapper))) + + +(defn- conform-json [json] + (if (map? json) + (select-keys json [:query]) + (ba/incorrect + "Expect a JSON object." + :fhir/issue "structure" + :fhir/operation-outcome "MSG_JSON_OBJECT"))) + + +(defn- query-request-json [{:keys [body] :as request}] + (if body + (when-ok [x (parse-json body) + query (conform-json x)] + (assoc request :body query)) + (ba/incorrect "Missing HTTP body."))) + + +(defn- unsupported-media-type-msg [media-type] + (format "Unsupported media type `%s` expect one of `application/graphql` or `application/json`." + media-type)) + + +(defn- query-request [request] + (if-let [content-type (request/content-type request)] + (cond + (str/starts-with? content-type "application/graphql") + (query-request-graphql request) + + (str/starts-with? content-type "application/json") + (query-request-json request) + + :else + (ba/incorrect (unsupported-media-type-msg content-type) + :http/status 415)) + (ba/incorrect "Content-Type header expected, but is missing."))) + + +(defn wrap-query + "Middleware to slurp a GraphQL query from the body according the content-type + header. + + Updates the :body key in the request map with a map consisting of at least a + :query key. If the content-type is `application/graphql` the body string is + the value of the query key. If the content-type is `application/json` the map + is the parsed JSON body. + + Returns an OperationOutcome in the internal format, skipping the handler, with + an appropriate error when a content-type other than `application/graphql` or + `application/json` was specified. + + See also: https://graphql.org/learn/serving-over-http/" + [handler] + (fn [request] + (if-ok [request (query-request request)] + (handler request) + ac/completed-future))) diff --git a/modules/operation-graphql/src/blaze/fhir/operation/graphql/spec.clj b/modules/operation-graphql/src/blaze/fhir/operation/graphql/spec.clj new file mode 100644 index 000000000..3ee178622 --- /dev/null +++ b/modules/operation-graphql/src/blaze/fhir/operation/graphql/spec.clj @@ -0,0 +1,13 @@ +(ns blaze.fhir.operation.graphql.spec + (:require + [blaze.executors :as ex] + [blaze.fhir.operation.graphql :as-alias graphql] + [clojure.spec.alpha :as s])) + + +(s/def ::graphql/executor + ex/executor?) + + +(s/def ::graphql/num-threads + pos-int?) diff --git a/modules/operation-graphql/test/blaze/fhir/operation/graphql/middleware/query_test.clj b/modules/operation-graphql/test/blaze/fhir/operation/graphql/middleware/query_test.clj new file mode 100644 index 000000000..c2d01588a --- /dev/null +++ b/modules/operation-graphql/test/blaze/fhir/operation/graphql/middleware/query_test.clj @@ -0,0 +1,117 @@ +(ns blaze.fhir.operation.graphql.middleware.query-test + (:require + [blaze.async.comp :as ac] + [blaze.fhir.operation.graphql.middleware.query :refer [wrap-query]] + [blaze.fhir.spec :as fhir-spec] + [blaze.handler.util :as handler-util] + [blaze.test-util :as tu] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest is testing]] + [juxt.iota :refer [given]] + [taoensso.timbre :as log]) + (:import + [java.io ByteArrayInputStream] + [java.nio.charset StandardCharsets])) + + +(set! *warn-on-reflection* true) +(st/instrument) +(log/set-level! :trace) + + +(test/use-fixtures :each tu/fixture) + + +(defn wrap-error [handler] + (fn [request] + (-> (handler request) + (ac/exceptionally handler-util/error-response)))) + + +(def handler + "A handler which just returns the :body from the request." + (-> (comp ac/completed-future :body) + wrap-query + wrap-error)) + + +(defn input-stream + ([^String s] + (ByteArrayInputStream. (.getBytes s StandardCharsets/UTF_8))) + ([^String s closed?] + (proxy [ByteArrayInputStream] [(.getBytes s StandardCharsets/UTF_8)] + (close [] + (reset! closed? true))))) + + +(deftest wrap-query-test + (testing "application/graphql" + (let [closed? (atom false)] + (given @(handler + {:headers {"content-type" "application/graphql"} + :body (input-stream "query-160125" closed?)}) + :query := "query-160125") + (is (true? @closed?)))) + + (testing "application/json" + (let [closed? (atom false)] + (given @(handler + {:headers {"content-type" "application/json"} + :body (input-stream "{\"query\": \"query-155956\"}" closed?)}) + :query := "query-155956") + (is (true? @closed?))) + + (testing "unknown keys are ignored" + (given @(handler + {:headers {"content-type" "application/json"} + :body (input-stream "{\"query\": \"query-155956\", \"foo\": \"bar\"}")}) + :query := "query-155956" + :foo :? nil?))) + + (testing "body with invalid JSON" + (given @(handler + {:headers {"content-type" "application/json"} + :body (input-stream "x")}) + :status := 400 + [:body fhir-spec/fhir-type] := :fhir/OperationOutcome + [:body :issue 0 :severity] := #fhir/code"error" + [:body :issue 0 :code] := #fhir/code"invalid" + [:body :issue 0 :diagnostics] := "Unrecognized token 'x': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\n at [Source: (ByteArrayInputStream); line: 1, column: 2]")) + + (testing "body with no JSON object" + (given @(handler + {:headers {"content-type" "application/json"} + :body (input-stream "1")}) + :status := 400 + [:body fhir-spec/fhir-type] := :fhir/OperationOutcome + [:body :issue 0 :severity] := #fhir/code"error" + [:body :issue 0 :code] := #fhir/code"structure" + [:body :issue 0 :details :coding 0 :system] := #fhir/uri"http://terminology.hl7.org/CodeSystem/operation-outcome" + [:body :issue 0 :details :coding 0 :code] := #fhir/code"MSG_JSON_OBJECT" + [:body :issue 0 :diagnostics] := "Expect a JSON object.")) + + (testing "other content is invalid" + (testing "without content-type header" + (given @(handler {}) + :status := 400 + [:body fhir-spec/fhir-type] := :fhir/OperationOutcome + [:body :issue 0 :severity] := #fhir/code"error" + [:body :issue 0 :code] := #fhir/code"invalid" + [:body :issue 0 :diagnostics] := "Content-Type header expected, but is missing.")) + + (testing "with unknown content-type header" + (given @(handler {:headers {"content-type" "text/plain"} :body (input-stream "")}) + :status := 415 + [:body fhir-spec/fhir-type] := :fhir/OperationOutcome + [:body :issue 0 :severity] := #fhir/code"error" + [:body :issue 0 :code] := #fhir/code"invalid" + [:body :issue 0 :diagnostics] := "Unsupported media type `text/plain` expect one of `application/graphql` or `application/json`.")) + + (testing "missing body" + (doseq [content-type ["application/graphql" "application/json"]] + (given @(handler + {:headers {"content-type" content-type}}) + [:body fhir-spec/fhir-type] := :fhir/OperationOutcome + [:body :issue 0 :severity] := #fhir/code"error" + [:body :issue 0 :code] := #fhir/code"invalid" + [:body :issue 0 :diagnostics] := "Missing HTTP body."))))) diff --git a/modules/operation-graphql/test/blaze/fhir/operation/graphql/middleware_test.clj b/modules/operation-graphql/test/blaze/fhir/operation/graphql/middleware_test.clj new file mode 100644 index 000000000..7be908d06 --- /dev/null +++ b/modules/operation-graphql/test/blaze/fhir/operation/graphql/middleware_test.clj @@ -0,0 +1,24 @@ +(ns blaze.fhir.operation.graphql.middleware-test + (:require + [blaze.fhir.operation.graphql.middleware :as middleware] + [blaze.log] + [blaze.middleware.fhir.db-spec] + [blaze.test-util :as tu] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest]] + [integrant.core :as ig] + [juxt.iota :refer [given]] + [taoensso.timbre :as log])) + + +(st/instrument) +(log/set-level! :trace) + + +(test/use-fixtures :each tu/fixture) + + +(deftest init-test + (given (ig/init {::middleware/query {}}) + [::middleware/query :name] := ::middleware/query + [::middleware/query :wrap] :? fn?)) diff --git a/modules/operation-graphql/test/blaze/fhir/operation/graphql/test_util.clj b/modules/operation-graphql/test/blaze/fhir/operation/graphql/test_util.clj new file mode 100644 index 000000000..a75ca2548 --- /dev/null +++ b/modules/operation-graphql/test/blaze/fhir/operation/graphql/test_util.clj @@ -0,0 +1,10 @@ +(ns blaze.fhir.operation.graphql.test-util + (:require + [blaze.async.comp :as ac] + [blaze.handler.util :as handler-util])) + + +(defn wrap-error [handler] + (fn [request] + (-> (handler request) + (ac/exceptionally handler-util/error-response)))) diff --git a/modules/operation-graphql/test/blaze/fhir/operation/graphql_test.clj b/modules/operation-graphql/test/blaze/fhir/operation/graphql_test.clj new file mode 100644 index 000000000..615d7cb39 --- /dev/null +++ b/modules/operation-graphql/test/blaze/fhir/operation/graphql_test.clj @@ -0,0 +1,220 @@ +(ns blaze.fhir.operation.graphql-test + (:require + [blaze.db.api-stub :refer [mem-node-system with-system-data]] + [blaze.executors :as ex] + [blaze.fhir.operation.graphql :as graphql] + [blaze.fhir.operation.graphql.test-util :refer [wrap-error]] + [blaze.log] + [blaze.middleware.fhir.db :refer [wrap-db]] + [blaze.middleware.fhir.db-spec] + [blaze.test-util :as tu :refer [given-thrown with-system]] + [clojure.spec.alpha :as s] + [clojure.spec.test.alpha :as st] + [clojure.test :as test :refer [deftest is testing]] + [integrant.core :as ig] + [juxt.iota :refer [given]] + [taoensso.timbre :as log])) + + +(st/instrument) +(log/set-level! :trace) + + +(test/use-fixtures :each tu/fixture) + + +(deftest init-test + (testing "nil config" + (given-thrown (ig/init {::graphql/handler nil}) + :key := ::graphql/handler + :reason := ::ig/build-failed-spec + [:explain ::s/problems 0 :pred] := `map?)) + + (testing "missing config" + (given-thrown (ig/init {::graphql/handler {}}) + :key := ::graphql/handler + :reason := ::ig/build-failed-spec + [:explain ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node)) + [:explain ::s/problems 1 :pred] := `(fn ~'[%] (contains? ~'% :executor)))) + + (testing "invalid executor" + (given-thrown (ig/init {::graphql/handler {:executor ::invalid}}) + :key := ::graphql/handler + :reason := ::ig/build-failed-spec + [:explain ::s/problems 0 :pred] := `(fn ~'[%] (contains? ~'% :node)) + [:explain ::s/problems 1 :pred] := `ex/executor? + [:explain ::s/problems 1 :val] := ::invalid))) + + +(deftest executor-init-test + (testing "nil config" + (given-thrown (ig/init {::graphql/executor nil}) + :key := ::graphql/executor + :reason := ::ig/build-failed-spec + [:explain ::s/problems 0 :pred] := `map?)) + + (testing "invalid num-threads" + (given-thrown (ig/init {::graphql/executor {:num-threads ::invalid}}) + :key := ::graphql/executor + :reason := ::ig/build-failed-spec + [:explain ::s/problems 0 :pred] := `pos-int? + [:explain ::s/problems 0 :val] := ::invalid)) + + (testing "with default num-threads" + (with-system [{::graphql/keys [executor]} + {::graphql/executor {}}] + (is (ex/executor? executor))))) + + +(def system + (assoc mem-node-system + ::graphql/handler + {:node (ig/ref :blaze.db/node) + :executor (ig/ref :blaze.test/executor)} + :blaze.test/executor {})) + + +(defmacro with-handler [[handler-binding] & more] + (let [[txs body] (tu/extract-txs-body more)] + `(with-system-data [{node# :blaze.db/node + handler# ::graphql/handler} system] + ~txs + (let [~handler-binding (-> handler# (wrap-db node#) wrap-error)] + ~@body)))) + + +(deftest execute-query-test + (testing "query param" + (testing "invalid query" + (testing "via query param" + (with-handler [handler] + (let [{:keys [status body]} + @(handler + {:request-method :get + :params {"query" "{"}})] + + (is (= 200 status)) + + (given body + [:errors 0 :message] := "Failed to parse GraphQL query.")))) + + (testing "via body" + (with-handler [handler] + (let [{:keys [status body]} + @(handler + {:request-method :post + :body {:query "{"}})] + + (is (= 200 status)) + + (given body + [:errors 0 :message] := "Failed to parse GraphQL query."))))) + + (testing "success" + (testing "Patient" + (testing "empty result" + (testing "via query param" + (with-handler [handler] + (let [{:keys [status body]} + @(handler + {:request-method :get + :params {"query" "{ PatientList { gender } }"}})] + + (is (= 200 status)) + + (given body + [:data :PatientList] :? empty? + [:errors] :? empty?)))) + + (testing "via body" + (with-handler [handler] + (let [{:keys [status body]} + @(handler + {:request-method :post + :body {:query "{ PatientList { gender } }"}})] + + (is (= 200 status)) + + (given body + [:data :PatientList] :? empty? + [:errors] :? empty?))))) + + (testing "one Patient" + (with-handler [handler] + [[[:put {:fhir/type :fhir/Patient :id "0" + :gender #fhir/code"male"}]]] + + (let [{:keys [status body]} + @(handler + {:request-method :get + :params {"query" "{ PatientList { id gender } }"}})] + + (is (= 200 status)) + + (given body + [:data :PatientList 0 :id] := "0" + [:data :PatientList 0 :gender] := "male" + [:errors] :? empty?))))) + + (testing "Observation" + (testing "empty result" + (with-handler [handler] + (let [{:keys [status body]} + @(handler + {:request-method :get + :params {"query" "{ ObservationList { subject { reference } } }"}})] + + (is (= 200 status)) + + (given body + [:data :ObservationList] :? empty? + [:errors] :? empty?)))) + + (testing "one Observation" + (with-handler [handler] + [[[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :subject #fhir/Reference{:reference "Patient/0"}}]]] + + (let [{:keys [status body]} + @(handler + {:request-method :get + :params {"query" "{ ObservationList { subject { reference } } }"}})] + + (is (= 200 status)) + + (given body + [:data :ObservationList 0 :subject :reference] := "Patient/0" + [:errors] :? empty?)))) + + (testing "one Observation with code" + (with-handler [handler] + [[[:put {:fhir/type :fhir/Patient :id "0"}] + [:put {:fhir/type :fhir/Observation :id "0" + :code + #fhir/CodeableConcept + {:coding + [#fhir/Coding + {:system #fhir/uri"http://loinc.org" + :code #fhir/code"39156-5"}]} + :subject #fhir/Reference{:reference "Patient/0"}}] + [:put {:fhir/type :fhir/Observation :id "1" + :code + #fhir/CodeableConcept + {:coding + [#fhir/Coding + {:system #fhir/uri"http://loinc.org" + :code #fhir/code"29463-7"}]} + :subject #fhir/Reference{:reference "Patient/0"}}]]] + + (let [{:keys [status body]} + @(handler + {:request-method :get + :params {"query" "{ ObservationList(code: \"39156-5\") { subject { reference } } }"}})] + + (is (= 200 status)) + + (given body + [:data :ObservationList count] := 1 + [:data :ObservationList 0 :subject :reference] := "Patient/0" + [:errors] :? empty?)))))))) diff --git a/modules/operation-graphql/tests.edn b/modules/operation-graphql/tests.edn new file mode 100644 index 000000000..94fe5636c --- /dev/null +++ b/modules/operation-graphql/tests.edn @@ -0,0 +1,5 @@ +#kaocha/v1 + #merge + [{} + #profile {:ci {:reporter kaocha.report/documentation + :color? false}}] diff --git a/modules/operation-measure-evaluate-measure/.clj-kondo/config.edn b/modules/operation-measure-evaluate-measure/.clj-kondo/config.edn index 809ba05c7..0b290a0ed 100644 --- a/modules/operation-measure-evaluate-measure/.clj-kondo/config.edn +++ b/modules/operation-measure-evaluate-measure/.clj-kondo/config.edn @@ -3,7 +3,6 @@ blaze.anomaly/when-ok clojure.core/let blaze.db.api-stub/with-system-data clojure.core/with-open blaze.fhir.operation.evaluate-measure-test/with-handler clojure.core/fn - blaze.fhir.operation.evaluate-measure-test/with-handler-data clojure.core/fn blaze.test-util/with-system clojure.core/with-open prometheus.alpha/defcounter clojure.core/def prometheus.alpha/defhistogram clojure.core/def} diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/test_util.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/test_util.clj index 6644eaf96..e5dd5ed68 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/test_util.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure/test_util.clj @@ -8,9 +8,3 @@ (fn [request] (-> (handler request) (ac/exceptionally handler-util/error-response)))) - - -(defn extract-txs-body [more] - (if (vector? (first more)) - [(first more) (next more)] - [[] more])) diff --git a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj index 33e8d87f6..4367b0c4e 100644 --- a/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj +++ b/modules/operation-measure-evaluate-measure/test/blaze/fhir/operation/evaluate_measure_test.clj @@ -17,9 +17,7 @@ [java-time.api :as time] [juxt.iota :refer [given]] [reitit.core :as reitit] - [taoensso.timbre :as log]) - (:import - [java.util.concurrent ExecutorService])) + [taoensso.timbre :as log])) (set! *warn-on-reflection* true) @@ -141,7 +139,7 @@ (testing "with default num-threads" (with-system [{::evaluate-measure/keys [executor]} {::evaluate-measure/executor {}}] - (is (instance? ExecutorService executor))))) + (is (ex/executor? executor))))) (deftest compile-duration-seconds-collector-init-test @@ -176,7 +174,7 @@ (defmacro with-handler [[handler-binding] & more] - (let [[txs body] (etu/extract-txs-body more)] + (let [[txs body] (tu/extract-txs-body more)] `(with-system-data [{node# :blaze.db/node handler# ::evaluate-measure/handler} system] ~txs diff --git a/modules/rest-api/src/blaze/rest_api/middleware/output.clj b/modules/rest-api/src/blaze/rest_api/middleware/output.clj index be9e1c1cf..691ddab12 100644 --- a/modules/rest-api/src/blaze/rest_api/middleware/output.clj +++ b/modules/rest-api/src/blaze/rest_api/middleware/output.clj @@ -4,6 +4,7 @@ [blaze.fhir.spec :as fhir-spec] [clojure.data.xml :as xml] [clojure.java.io :as io] + [jsonista.core :as j] [muuntaja.parse :as parse] [prometheus.alpha :as prom] [ring.util.response :as ring] @@ -100,3 +101,15 @@ ([handler opts] (fn [request respond raise] (handler request #(respond (handle-response opts request %)) raise)))) + + +(defn- handle-json-response [response] + (-> (update response :body j/write-value-as-bytes) + (ring/content-type "application/json;charset=utf-8"))) + + +(defn wrap-json-output + "Middleware to output data (not resources) in JSON" + [handler] + (fn [request respond raise] + (handler request #(respond (handle-json-response %)) raise))) diff --git a/modules/rest-api/src/blaze/rest_api/middleware/resource.clj b/modules/rest-api/src/blaze/rest_api/middleware/resource.clj index 1ccb3ccd2..903c55a3e 100644 --- a/modules/rest-api/src/blaze/rest_api/middleware/resource.clj +++ b/modules/rest-api/src/blaze/rest_api/middleware/resource.clj @@ -105,7 +105,7 @@ (defn- unsupported-media-type-msg [media-type] - (format "Unsupported Media Type `%s` expect one of `application/fhir+json` or `application/fhir+xml`." + (format "Unsupported media type `%s` expect one of `application/fhir+json` or `application/fhir+xml`." media-type)) diff --git a/modules/rest-api/src/blaze/rest_api/routes.clj b/modules/rest-api/src/blaze/rest_api/routes.clj index 8780df22c..d4742d1f0 100644 --- a/modules/rest-api/src/blaze/rest_api/routes.clj +++ b/modules/rest-api/src/blaze/rest_api/routes.clj @@ -68,7 +68,10 @@ (def ^:private wrap-output {:name :output - :wrap output/wrap-output}) + :compile (fn [{:keys [response-type]} _] + (if (= :json response-type) + output/wrap-json-output + output/wrap-output))}) (def ^:private wrap-error @@ -169,13 +172,18 @@ (defn- operation-system-handler-route [{:keys [node db-sync-timeout]} - {:blaze.rest-api.operation/keys [code system-handler]}] + {:blaze.rest-api.operation/keys + [code response-type post-middleware system-handler]}] (when system-handler [[(str "/$" code) - {:middleware [[wrap-db node db-sync-timeout]] - :get system-handler - :post {:middleware [wrap-resource] - :handler system-handler}}]])) + (cond-> {:middleware [[wrap-db node db-sync-timeout]] + :get system-handler + :post {:middleware [(if post-middleware + post-middleware + wrap-resource)] + :handler system-handler}} + response-type + (assoc :response-type response-type))]])) (defn operation-type-handler-route diff --git a/modules/rest-api/test/blaze/rest_api/middleware/resource_test.clj b/modules/rest-api/test/blaze/rest_api/middleware/resource_test.clj index 2eeee418f..a7d951502 100644 --- a/modules/rest-api/test/blaze/rest_api/middleware/resource_test.clj +++ b/modules/rest-api/test/blaze/rest_api/middleware/resource_test.clj @@ -163,7 +163,7 @@ [:body fhir-spec/fhir-type] := :fhir/OperationOutcome [:body :issue 0 :severity] := #fhir/code"error" [:body :issue 0 :code] := #fhir/code"invalid" - [:body :issue 0 :diagnostics] := "Unsupported Media Type `text/plain` expect one of `application/fhir+json` or `application/fhir+xml`.")) + [:body :issue 0 :diagnostics] := "Unsupported media type `text/plain` expect one of `application/fhir+json` or `application/fhir+xml`.")) (testing "missing body" (doseq [content-type ["application/fhir+json" "application/fhir+xml"]] diff --git a/modules/test-util/src/blaze/test_util.clj b/modules/test-util/src/blaze/test_util.clj index 99223740f..53f6d5225 100644 --- a/modules/test-util/src/blaze/test_util.clj +++ b/modules/test-util/src/blaze/test_util.clj @@ -104,3 +104,9 @@ (st/instrument) (f) (st/unstrument)) + + +(defn extract-txs-body [more] + (if (vector? (first more)) + [(first more) (next more)] + [[] more])) diff --git a/resources/blaze.edn b/resources/blaze.edn index cefedce07..63d98339b 100644 --- a/resources/blaze.edn +++ b/resources/blaze.edn @@ -69,7 +69,14 @@ :def-uri "http://hl7.org/fhir/OperationDefinition/Measure-evaluate-measure" :resource-types ["Measure"] :type-handler #blaze/ref :blaze.fhir.operation.evaluate-measure/handler - :instance-handler #blaze/ref :blaze.fhir.operation.evaluate-measure/handler}] + :instance-handler #blaze/ref :blaze.fhir.operation.evaluate-measure/handler} + #:blaze.rest-api.operation + {:code "graphql" + :def-uri "http://hl7.org/fhir/OperationDefinition/Resource-graphql" + :resource-types ["Resource"] + :post-middleware #blaze/ref :blaze.fhir.operation.graphql.middleware/query + :response-type :json + :system-handler #blaze/ref :blaze.fhir.operation.graphql/handler}] :enforce-referential-integrity #blaze/cfg ["ENFORCE_REFERENTIAL_INTEGRITY" boolean? true]} ;; @@ -156,6 +163,18 @@ :blaze.fhir.operation.evaluate-measure/compile-duration-seconds {} :blaze.fhir.operation.evaluate-measure/evaluate-duration-seconds {} + ;; + ;; FHIR Operation GraphQL + ;; + :blaze.fhir.operation.graphql/handler + {:node #blaze/ref :blaze.db/node + :executor #blaze/ref :blaze.fhir.operation.graphql/executor} + + :blaze.fhir.operation.graphql/executor + {:num-threads #blaze/cfg ["FHIR_OPERATION_GRAPHQL_THREADS" pos-int? 4]} + + :blaze.fhir.operation.graphql.middleware/query {} + ;; ;; Database Node ;;