diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index db1ea38c..937c4ffb 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -21,11 +21,13 @@ services:
spot:
image: samply/rustyspot:main
ports:
- - 8080:8080
+ - 8055:8055
environment:
+ RUST_LOG: "info"
+ CORS_ORIGIN: http://localhost:5173
BEAM_SECRET: "${LOCAL_BEAM_SECRET_SPOT}"
BEAM_PROXY_URL: http://beam-proxy:8081
- BEAM_APP_ID: "spot.${LOCAL_BEAM_ID}.${BROKER_HOST}"
+ BEAM_APP_ID: "focus.${LOCAL_BEAM_ID}.${BROKER_HOST}"
depends_on:
- "beam-proxy"
profiles:
diff --git a/docker-compose.yml b/docker-compose.yml
index 1c06385d..7bf498cd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -55,6 +55,8 @@ services:
spot:
image: samply/rustyspot:main
+ ports:
+ - "8055:8055"
environment:
HTTP_PROXY: ${http_proxy}
HTTPS_PROXY: ${https_proxy}
diff --git a/example.env b/example.env
index 66837680..2e095cf5 100644
--- a/example.env
+++ b/example.env
@@ -11,6 +11,7 @@ GUI_HOST="data.dktk.dkfz.de|demo.lens.samply.de"
BROKER_HOST="broker.ccp-it.dktk.dkfz.de"
LOCAL_BEAM_ID="your-proxy-id"
LOCAL_BEAM_SECRET_SPOT="insert-a-random-passphrase-here"
+LOCAL_BEAM_SECRET_PRISM="insert-a-random-passphrase-here"
# Request your OAUTH client from your oauth provider admin
OAUTH_ISSUER_URL="the-discovery-adress-of-your-oauth-provider"
diff --git a/options_tester.cjs b/options_tester.cjs
index 9fdfc808..9c41892e 100644
--- a/options_tester.cjs
+++ b/options_tester.cjs
@@ -1,17 +1,32 @@
"use strict";
-var __importDefault = (this && this.__importDefault) || function (mod) {
+const __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
+
+console.log(
+ "Checking Lens options for ",
+ process.env.VITE_TARGET_ENVIRONMENT,
+);
+
+let optionsPath = "";
+if (process.env.VITE_TARGET_ENVIRONMENT === "production") {
+ optionsPath = "./packages/demo/public/options-ccp-prod.json";
+} else if (process.env.VITE_TARGET_ENVIRONMENT === "staging") {
+ optionsPath = "./packages/demo/public/options-ccp-demo.json";
+} else {
+ optionsPath = "./packages/demo/public/options-dev.json";
+}
+
Object.defineProperty(exports, "__esModule", { value: true });
-var options_schema_json_1 = __importDefault(require("./packages/lib/src/interfaces/options.schema.json"));
-var schemasafe_1 = require("@exodus/schemasafe");
-var options_json_1 = __importDefault(require("./packages/demo/public/options.json"));
+const options_schema_json_1 = __importDefault(require("./packages/lib/src/types/options.schema.json"));
+const schemasafe_1 = require("@exodus/schemasafe");
+const options_json_1 = __importDefault(require(optionsPath));
console.log("Checking Lens options");
-var parse = (0, schemasafe_1.parser)(options_schema_json_1.default, {
+const parse = (0, schemasafe_1.parser)(options_schema_json_1.default, {
includeErrors: true,
allErrors: true,
});
-var validJSON = parse(JSON.stringify(options_json_1.default));
+const validJSON = parse(JSON.stringify(options_json_1.default));
if (validJSON.valid === true) {
console.log("Options are valid");
}
diff --git a/options_tester.ts b/options_tester.ts
index 296c6d26..d7751695 100644
--- a/options_tester.ts
+++ b/options_tester.ts
@@ -1,9 +1,23 @@
-import optionsSchema from "./packages/lib/src/interfaces/options.schema.json";
+import optionsSchema from "./packages/lib/src/types/options.schema.json";
import { parser } from "@exodus/schemasafe";
-import options from "./packages/demo/public/options.json";
+import devOptions from "./packages/demo/public/options-dev.json";
+import demoOptions from "./packages/demo/public/options-ccp-demo.json";
+import prodOptions from "./packages/demo/public/options-ccp-prod.json";
-console.log("Checking Lens options");
+console.log(
+ "Checking Lens options for ",
+ import.meta.env.VITE_TARGET_ENVIRONMENT,
+);
+
+let options = {};
+if (import.meta.env.VITE_TARGET_ENVIRONMENT === "production") {
+ options = prodOptions;
+} else if (import.meta.env.VITE_TARGET_ENVIRONMENT === "staging") {
+ options = demoOptions;
+} else {
+ options = devOptions;
+}
const parse = parser(optionsSchema, {
includeErrors: true,
diff --git a/package-lock.json b/package-lock.json
index 165a14b0..a424b458 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -720,7 +720,8 @@
"node_modules/@exodus/schemasafe": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz",
- "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="
+ "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==",
+ "license": "MIT"
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
@@ -2985,9 +2986,9 @@
"dev": true
},
"node_modules/follow-redirects": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
- "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
@@ -2995,6 +2996,7 @@
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
+ "license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -5645,10 +5647,11 @@
}
},
"node_modules/vite": {
- "version": "4.5.2",
- "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
- "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
+ "version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
+ "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
diff --git a/packages/demo/public/options-ccp-demo.json b/packages/demo/public/options-ccp-demo.json
new file mode 100644
index 00000000..cc44bc01
--- /dev/null
+++ b/packages/demo/public/options-ccp-demo.json
@@ -0,0 +1,268 @@
+{
+ "iconOptions": {
+ "deleteUrl": "delete_icon.svg",
+ "infoUrl": "info-circle-svgrepo-com.svg",
+ "selectAll": {
+ "text": "Add all"
+ }
+ },
+ "siteMappings": {
+ "berlin": "Berlin",
+ "berlin-test": "Berlin",
+ "bonn": "Bonn",
+ "dresden": "Dresden",
+ "essen": "Essen",
+ "frankfurt": "Frankfurt",
+ "freiburg": "Freiburg",
+ "hannover": "Hannover",
+ "mainz": "Mainz",
+ "muenchen-lmu": "München(LMU)",
+ "muenchen-tum": "München(TUM)",
+ "ulm": "Ulm",
+ "wuerzburg": "Würzburg",
+ "mannheim": "Mannheim",
+ "dktk-test": "DKTK-Test",
+ "hamburg": "Hamburg"
+ },
+ "chartOptions": {
+ "patients": {
+ "legendMapping": {
+ "berlin": "Berlin",
+ "berlin-test": "Berlin",
+ "bonn": "Bonn",
+ "dresden": "Dresden",
+ "essen": "Essen",
+ "frankfurt": "Frankfurt",
+ "freiburg": "Freiburg",
+ "hannover": "Hannover",
+ "mainz": "Mainz",
+ "muenchen-lmu": "München(LMU)",
+ "muenchen-tum": "München(TUM)",
+ "ulm": "Ulm",
+ "wuerzburg": "Würzburg",
+ "mannheim": "Mannheim",
+ "dktk-test": "DKTK-Test",
+ "hamburg": "Hamburg"
+ }
+ },
+ "gender": {
+ "legendMapping": {
+ "male": "Männlich",
+ "female": "Weiblich",
+ "unknown": "Unbekannt",
+ "other": "Divers"
+ }
+ },
+ "diagnosis": {
+ "hintText": [
+ "Bei Patienten mit mehreren onkologischen Diagnosen werden auch Einträge angezeigt, die ggfs. nicht den ausgewählten Suchkriterien entsprechen."
+ ]
+ },
+ "age_at_diagnosis": {
+ "hintText": [
+ "Bei Patienten mit mehreren Erstdiagnosen werden auch Einträge angezeigt, die ggfs. außerhalb der Suchkriterien liegen. "
+ ]
+ },
+ "75186-7": {
+ "hintText": [
+ "\"verstorben\": ein Todesdatum ist dokumentiert oder das aktuelle Lebensalter ist größer 123 Jahre.",
+ "\"lebend\": wird angenommen, wenn kein Todesdatum dokumentiert ist oder das aktuelle Lebensalter nicht 123 Jahre überschritten hat.",
+ "\"unbekannt\": kein Geburtsdatum oder Todesdatum bekannt."
+ ]
+ },
+ "therapy_of_tumor": {
+ "aggregations": [
+ "medicationStatements"
+ ],
+ "tooltips": {
+ "OP": "Operationen",
+ "ST": "Strahlentherapien",
+ "medicationStatements": "Systemische Therapien"
+ }
+ },
+ "medicationStatements": {
+ "hintText": [
+ "Art der systemischen oder abwartenden Therapie (ADT Basisdatensatz Versionen 2014, 2021)"
+ ],
+ "tooltips": {
+ "CH": "Chemotherapie",
+ "HO": "Hormontherapie",
+ "IM": "Immun-/Antikörpertherapie",
+ "KM": "Knochenmarktransplantation",
+ "ZS": "zielgerichtete Substanzen",
+ "CI": "Chemo- + Immun-/Antikörpertherapie",
+ "CZ": "Chemotherapie + zielgerichtete Substanzen",
+ "CIZ": "Chemo- + Immun-/Antikörpertherapie + zielgerichtete Substanzen",
+ "IZ": "Immun-/Antikörpertherapie + zielgerichtete Substanzen",
+ "SZ": "Stammzelltransplantation (inklusive Knochenmarktransplantation)",
+ "AS": "Active Surveillance",
+ "WS": "Wait and see",
+ "WW": "Watchful Waiting",
+ "SO": "Sonstiges"
+ }
+ },
+ "sample_kind": {
+ "hintText": [
+ "Verteilung der Probentypen die mit den identifizierten Patienten verbunden sind."
+ ],
+ "accumulatedValues": [
+ {
+ "name": "ffpe-tissue",
+ "values": [
+ "tissue-ffpe",
+ "tumor-tissue-ffpe",
+ "normal-tissue-ffpe",
+ "other-tissue-ffpe"
+ ]
+ },
+ {
+ "name": "frozen-tissue",
+ "values": [
+ "tissue-frozen",
+ "tumor-tissue-frozen",
+ "normal-tissue-frozen",
+ "other-tissue-frozen"
+ ]
+ }
+ ],
+ "tooltips": {
+ "ffpe-tissue": "Gewebe FFPE",
+ "frozen-tissue": "Gewebe schockgefroren",
+ "tissue-other": "Gewebe, Andere Konservierungsart",
+ "whole-blood": "Vollblut",
+ "blood-serum": "Serum",
+ "blood-plasma": "Plasma",
+ "buffy-coat": "Buffy Coat",
+ "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)",
+ "dried-whole-blood": "Blutkarten",
+ "swab": "Abstrich",
+ "ascites": "Aszites",
+ "stool-faeces": "Stuhl",
+ "urine": "Urin",
+ "csf-liquor": "Liquor",
+ "bone-marrow": "Knochenmark",
+ "saliva": "Speichel",
+ "liquid-other": "Flüssigprobe, Andere",
+ "dna": "DNA",
+ "rna": "RNA",
+ "derivative-other": "Derivat, Andere"
+ },
+ "legendMapping":{
+ "ffpe-tissue": "Gewebe FFPE",
+ "frozen-tissue": "Gewebe schockgefroren",
+ "tissue-other": "Gewebe, Andere Konservierungsart",
+ "whole-blood": "Vollblut",
+ "blood-serum": "Serum",
+ "blood-plasma": "Plasma",
+ "buffy-coat": "Buffy Coat",
+ "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)",
+ "dried-whole-blood": "Blutkarten",
+ "swab": "Abstrich",
+ "ascites": "Aszites",
+ "stool-faeces": "Stuhl",
+ "urine": "Urin",
+ "csf-liquor": "Liquor",
+ "bone-marrow": "Knochenmark",
+ "saliva": "Speichel",
+ "liquid-other": "Flüssigprobe, Andere",
+ "dna": "DNA",
+ "rna": "RNA",
+ "derivative-other": "Derivat, Andere"
+ }
+ }
+ },
+ "tableOptions": {
+ "headerData": [
+ {
+ "title": "Standorte",
+ "dataKey": "site"
+ },
+ {
+ "title": "Patienten",
+ "dataKey": "patients"
+ },
+ {
+ "title": "Bioproben*",
+ "aggregatedDataKeys": [
+ {
+ "groupCode": "specimen"
+ },
+ {
+ "stratifierCode": "Histologies",
+ "stratumCode": "1"
+ }
+ ]
+ }
+ ],
+ "claimedText": "Processing..."
+ },
+ "resultSummaryOptions": {
+ "title": "Ergebnisse",
+ "infoButtonText": "Um eine Re-Identifizierung zu erschweren, werden Standortergebnisse modifiziert und auf Zehnerstellen gerundet. Meldet ein Standort keinen Treffer, wird für diesen null angezeigt.",
+ "dataTypes": [
+ {
+ "title": "Standorte",
+ "dataKey": "collections"
+ },
+ {
+ "title": "Patienten",
+ "dataKey": "patients"
+ }
+ ]
+ },
+ "backends": {
+ "spots": [
+ {
+ "name": "DKTK",
+ "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION",
+ "url": "https://backend.demo.lens.samply.de/prod/",
+ "sites": [
+ "berlin",
+ "dresden",
+ "essen",
+ "frankfurt",
+ "freiburg",
+ "hannover",
+ "mainz",
+ "muenchen-lmu",
+ "muenchen-tum",
+ "ulm",
+ "wuerzburg",
+ "mannheim",
+ "dktk-test",
+ "hamburg"
+ ],
+ "catalogueKeyToResponseKeyMap": [
+ [
+ "gender",
+ "Gender"
+ ],
+ [
+ "age_at_diagnosis",
+ "Age"
+ ],
+ [
+ "diagnosis",
+ "diagnosis"
+ ],
+ [
+ "medicationStatements",
+ "MedicationType"
+ ],
+ [
+ "sample_kind",
+ "sample_kind"
+ ],
+ [
+ "therapy_of_tumor",
+ "ProcedureType"
+ ],
+ [
+ "75186-7",
+ "75186-7"
+ ]
+ ]
+ }
+ ]
+ }
+}
diff --git a/packages/demo/public/options.json b/packages/demo/public/options-ccp-prod.json
similarity index 77%
rename from packages/demo/public/options.json
rename to packages/demo/public/options-ccp-prod.json
index 466d9019..4888e2c4 100644
--- a/packages/demo/public/options.json
+++ b/packages/demo/public/options-ccp-prod.json
@@ -6,6 +6,24 @@
"text": "Add all"
}
},
+ "siteMappings": {
+ "berlin": "Berlin",
+ "berlin-test": "Berlin",
+ "bonn": "Bonn",
+ "dresden": "Dresden",
+ "essen": "Essen",
+ "frankfurt": "Frankfurt",
+ "freiburg": "Freiburg",
+ "hannover": "Hannover",
+ "mainz": "Mainz",
+ "muenchen-lmu": "München(LMU)",
+ "muenchen-tum": "München(TUM)",
+ "ulm": "Ulm",
+ "wuerzburg": "Würzburg",
+ "mannheim": "Mannheim",
+ "dktk-test": "DKTK-Test",
+ "hamburg": "Hamburg"
+ },
"chartOptions": {
"patients": {
"legendMapping": {
@@ -194,5 +212,60 @@
"dataKey": "patients"
}
]
+ },
+ "backends": {
+ "spots": [
+ {
+ "name": "DKTK",
+ "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION",
+ "url": "https://backend.data.dktk.dkfz.de/prod/",
+ "sites": [
+ "berlin",
+ "dresden",
+ "essen",
+ "frankfurt",
+ "freiburg",
+ "hannover",
+ "mainz",
+ "muenchen-lmu",
+ "muenchen-tum",
+ "ulm",
+ "wuerzburg",
+ "mannheim",
+ "dktk-test",
+ "hamburg"
+ ],
+ "catalogueKeyToResponseKeyMap": [
+ [
+ "gender",
+ "Gender"
+ ],
+ [
+ "age_at_diagnosis",
+ "Age"
+ ],
+ [
+ "diagnosis",
+ "diagnosis"
+ ],
+ [
+ "medicationStatements",
+ "MedicationType"
+ ],
+ [
+ "sample_kind",
+ "sample_kind"
+ ],
+ [
+ "therapy_of_tumor",
+ "ProcedureType"
+ ],
+ [
+ "75186-7",
+ "75186-7"
+ ]
+ ]
+ }
+ ]
}
}
diff --git a/packages/demo/public/options-dev.json b/packages/demo/public/options-dev.json
new file mode 100644
index 00000000..e781695f
--- /dev/null
+++ b/packages/demo/public/options-dev.json
@@ -0,0 +1,308 @@
+{
+ "iconOptions": {
+ "deleteUrl": "delete_icon.svg",
+ "infoUrl": "info-circle-svgrepo-com.svg",
+ "selectAll": {
+ "text": "Add all"
+ }
+ },
+ "siteMappings": {
+ "berlin": "Berlin",
+ "berlin-test": "Berlin",
+ "bonn": "Bonn",
+ "dresden": "Dresden",
+ "essen": "Essen",
+ "frankfurt": "Frankfurt",
+ "freiburg": "Freiburg",
+ "hannover": "Hannover",
+ "mainz": "Mainz",
+ "muenchen-lmu": "München(LMU)",
+ "muenchen-tum": "München(TUM)",
+ "ulm": "Ulm",
+ "wuerzburg": "Würzburg",
+ "mannheim": "Mannheim",
+ "dktk-test": "DKTK-Test",
+ "hamburg": "Hamburg"
+ },
+ "chartOptions": {
+ "patients": {
+ "legendMapping": {
+ "berlin": "Berlin",
+ "berlin-test": "Berlin",
+ "bonn": "Bonn",
+ "dresden": "Dresden",
+ "essen": "Essen",
+ "frankfurt": "Frankfurt",
+ "freiburg": "Freiburg",
+ "hannover": "Hannover",
+ "mainz": "Mainz",
+ "muenchen-lmu": "München(LMU)",
+ "muenchen-tum": "München(TUM)",
+ "ulm": "Ulm",
+ "wuerzburg": "Würzburg",
+ "mannheim": "Mannheim",
+ "dktk-test": "DKTK-Test",
+ "hamburg": "Hamburg"
+ }
+ },
+ "gender": {
+ "legendMapping": {
+ "male": "Männlich",
+ "female": "Weiblich",
+ "unknown": "Unbekannt",
+ "other": "Divers"
+ }
+ },
+ "diagnosis": {
+ "hintText": [
+ "Bei Patienten mit mehreren onkologischen Diagnosen werden auch Einträge angezeigt, die ggfs. nicht den ausgewählten Suchkriterien entsprechen."
+ ]
+ },
+ "age_at_diagnosis": {
+ "hintText": [
+ "Bei Patienten mit mehreren Erstdiagnosen werden auch Einträge angezeigt, die ggfs. außerhalb der Suchkriterien liegen. "
+ ]
+ },
+ "75186-7": {
+ "hintText": [
+ "\"verstorben\": ein Todesdatum ist dokumentiert oder das aktuelle Lebensalter ist größer 123 Jahre.",
+ "\"lebend\": wird angenommen, wenn kein Todesdatum dokumentiert ist oder das aktuelle Lebensalter nicht 123 Jahre überschritten hat.",
+ "\"unbekannt\": kein Geburtsdatum oder Todesdatum bekannt."
+ ]
+ },
+ "therapy_of_tumor": {
+ "aggregations": [
+ "medicationStatements"
+ ],
+ "tooltips": {
+ "OP": "Operationen",
+ "ST": "Strahlentherapien",
+ "medicationStatements": "Systemische Therapien"
+ }
+ },
+ "medicationStatements": {
+ "hintText": [
+ "Art der systemischen oder abwartenden Therapie (ADT Basisdatensatz Versionen 2014, 2021)"
+ ],
+ "tooltips": {
+ "CH": "Chemotherapie",
+ "HO": "Hormontherapie",
+ "IM": "Immun-/Antikörpertherapie",
+ "KM": "Knochenmarktransplantation",
+ "ZS": "zielgerichtete Substanzen",
+ "CI": "Chemo- + Immun-/Antikörpertherapie",
+ "CZ": "Chemotherapie + zielgerichtete Substanzen",
+ "CIZ": "Chemo- + Immun-/Antikörpertherapie + zielgerichtete Substanzen",
+ "IZ": "Immun-/Antikörpertherapie + zielgerichtete Substanzen",
+ "SZ": "Stammzelltransplantation (inklusive Knochenmarktransplantation)",
+ "AS": "Active Surveillance",
+ "WS": "Wait and see",
+ "WW": "Watchful Waiting",
+ "SO": "Sonstiges"
+ }
+ },
+ "sample_kind": {
+ "hintText": [
+ "Verteilung der Probentypen die mit den identifizierten Patienten verbunden sind."
+ ],
+ "accumulatedValues": [
+ {
+ "name": "ffpe-tissue",
+ "values": [
+ "tissue-ffpe",
+ "tumor-tissue-ffpe",
+ "normal-tissue-ffpe",
+ "other-tissue-ffpe"
+ ]
+ },
+ {
+ "name": "frozen-tissue",
+ "values": [
+ "tissue-frozen",
+ "tumor-tissue-frozen",
+ "normal-tissue-frozen",
+ "other-tissue-frozen"
+ ]
+ }
+ ],
+ "tooltips": {
+ "ffpe-tissue": "Gewebe FFPE",
+ "frozen-tissue": "Gewebe schockgefroren",
+ "tissue-other": "Gewebe, Andere Konservierungsart",
+ "whole-blood": "Vollblut",
+ "blood-serum": "Serum",
+ "blood-plasma": "Plasma",
+ "buffy-coat": "Buffy Coat",
+ "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)",
+ "dried-whole-blood": "Blutkarten",
+ "swab": "Abstrich",
+ "ascites": "Aszites",
+ "stool-faeces": "Stuhl",
+ "urine": "Urin",
+ "csf-liquor": "Liquor",
+ "bone-marrow": "Knochenmark",
+ "saliva": "Speichel",
+ "liquid-other": "Flüssigprobe, Andere",
+ "dna": "DNA",
+ "rna": "RNA",
+ "derivative-other": "Derivat, Andere"
+ },
+ "legendMapping":{
+ "ffpe-tissue": "Gewebe FFPE",
+ "frozen-tissue": "Gewebe schockgefroren",
+ "tissue-other": "Gewebe, Andere Konservierungsart",
+ "whole-blood": "Vollblut",
+ "blood-serum": "Serum",
+ "blood-plasma": "Plasma",
+ "buffy-coat": "Buffy Coat",
+ "peripheral-blood-cells": "Periphere mononukleäre Blutzellen (PBMC)",
+ "dried-whole-blood": "Blutkarten",
+ "swab": "Abstrich",
+ "ascites": "Aszites",
+ "stool-faeces": "Stuhl",
+ "urine": "Urin",
+ "csf-liquor": "Liquor",
+ "bone-marrow": "Knochenmark",
+ "saliva": "Speichel",
+ "liquid-other": "Flüssigprobe, Andere",
+ "dna": "DNA",
+ "rna": "RNA",
+ "derivative-other": "Derivat, Andere"
+ }
+ }
+ },
+ "tableOptions": {
+ "headerData": [
+ {
+ "title": "Standorte",
+ "dataKey": "site"
+ },
+ {
+ "title": "Patienten",
+ "dataKey": "patients"
+ },
+ {
+ "title": "Bioproben*",
+ "aggregatedDataKeys": [
+ {
+ "groupCode": "specimen"
+ },
+ {
+ "stratifierCode": "Histologies",
+ "stratumCode": "1"
+ }
+ ]
+ }
+ ],
+ "claimedText": "Processing..."
+ },
+ "resultSummaryOptions": {
+ "title": "Ergebnisse",
+ "infoButtonText": "Um eine Re-Identifizierung zu erschweren, werden Standortergebnisse modifiziert und auf Zehnerstellen gerundet. Meldet ein Standort keinen Treffer, wird für diesen null angezeigt.",
+ "dataTypes": [
+ {
+ "title": "Standorte",
+ "dataKey": "collections"
+ },
+ {
+ "title": "Patienten",
+ "dataKey": "patients"
+ }
+ ]
+ },
+ "backends": {
+ "customBackends": [
+ "someUrl"
+ ],
+ "spots": [
+ {
+ "name": "DKTK",
+ "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION",
+ "url": "http://localhost:8055",
+ "sites": [
+ "berlin",
+ "dresden",
+ "essen",
+ "frankfurt",
+ "freiburg",
+ "hannover",
+ "mainz",
+ "muenchen-lmu",
+ "muenchen-tum",
+ "ulm",
+ "wuerzburg",
+ "mannheim",
+ "dktk-test",
+ "hamburg"
+ ],
+ "catalogueKeyToResponseKeyMap": [
+ [
+ "gender",
+ "Gender"
+ ],
+ [
+ "age_at_diagnosis",
+ "Age"
+ ],
+ [
+ "diagnosis",
+ "diagnosis"
+ ],
+ [
+ "medicationStatements",
+ "MedicationType"
+ ],
+ [
+ "sample_kind",
+ "sample_kind"
+ ],
+ [
+ "therapy_of_tumor",
+ "ProcedureType"
+ ],
+ [
+ "75186-7",
+ "75186-7"
+ ]
+ ]
+ }
+ ],
+ "blazes": [
+ {
+ "name": "DKTK",
+ "backendMeasures": "DKTK_STRAT_DEF_IN_INITIAL_POPULATION",
+ "url": "http://localhost:8080",
+ "catalogueKeyToResponseKeyMap": [
+ [
+ "gender",
+ "Gender"
+ ],
+ [
+ "age_at_diagnosis",
+ "Age"
+ ],
+ [
+ "diagnosis",
+ "diagnosis"
+ ],
+ [
+ "medicationStatements",
+ "MedicationType"
+ ],
+ [
+ "sample_kind",
+ "sample_kind"
+ ],
+ [
+ "therapy_of_tumor",
+ "ProcedureType"
+ ],
+ [
+ "75186-7",
+ "75186-7"
+ ]
+ ]
+ }
+ ]
+ }
+}
diff --git a/packages/demo/src/AppCCP.svelte b/packages/demo/src/AppCCP.svelte
index e24d0416..07e48b41 100644
--- a/packages/demo/src/AppCCP.svelte
+++ b/packages/demo/src/AppCCP.svelte
@@ -1,4 +1,9 @@
@@ -147,12 +112,30 @@
noQueryMessage="Leere Suchanfrage: Sucht nach allen Ergebnissen."
showQuery={true}
/>
-
+
+
+
+
+
Suchkriterien
+
+
+
+
@@ -310,4 +293,5 @@
>
-
+
+
diff --git a/packages/demo/src/AppFragmentDevelopment.svelte b/packages/demo/src/AppFragmentDevelopment.svelte
index 9c7e7a80..f14019a5 100644
--- a/packages/demo/src/AppFragmentDevelopment.svelte
+++ b/packages/demo/src/AppFragmentDevelopment.svelte
@@ -1,5 +1,13 @@
@@ -193,11 +193,7 @@
Search Button
-
+
Result Summary Bar
@@ -207,7 +203,7 @@
Result Table
-
+
Result Pie Chart
@@ -242,4 +238,5 @@
-
+
+
diff --git a/packages/demo/src/backends/ast-to-cql-translator.ts b/packages/demo/src/backends/ast-to-cql-translator.ts
new file mode 100644
index 00000000..6b6a8148
--- /dev/null
+++ b/packages/demo/src/backends/ast-to-cql-translator.ts
@@ -0,0 +1,380 @@
+/**
+ * TODO: Document this file. Move to Project
+ */
+
+import type {
+ AstBottomLayerValue,
+ AstElement,
+ AstTopLayer,
+ MeasureItem,
+} from "../../../../dist/types";
+import {
+ alias as aliasMap,
+ cqltemplate,
+ criterionMap,
+} from "./cqlquery-mappings";
+
+let codesystems: string[] = [];
+let criteria: string[];
+
+export const translateAstToCql = (
+ query: AstTopLayer,
+ returnOnlySingeltons: boolean = true,
+ backendMeasures: string,
+ measures: MeasureItem[],
+ criterionList: string[],
+): string => {
+ criteria = criterionList;
+
+ /**
+ * DISCUSS: why is this even an array?
+ * in bbmri there is only concatted to the string
+ */
+ codesystems = [
+ // NOTE: We always need loinc, as the Deceased Stratifier is computed with it!!!
+ "codesystem loinc: 'http://loinc.org'",
+ ];
+
+ const cqlHeader =
+ "library Retrieve\n" +
+ "using FHIR version '4.0.0'\n" +
+ "include FHIRHelpers version '4.0.0'\n" +
+ "\n";
+
+ let singletons: string = "";
+ singletons = backendMeasures;
+ singletons += resolveOperation(query);
+
+ if (query.children.length == 0) {
+ singletons += "\ntrue";
+ }
+
+ if (returnOnlySingeltons) {
+ return singletons;
+ }
+
+ return (
+ cqlHeader +
+ getCodesystems() +
+ "context Patient\n" +
+ measures.map((measureItem: MeasureItem) => measureItem.cql).join("") +
+ singletons
+ );
+};
+
+const resolveOperation = (operation: AstElement): string => {
+ let expression: string = "";
+
+ if ("children" in operation && operation.children.length > 1) {
+ expression += "(";
+ }
+
+ "children" in operation &&
+ operation.children.forEach((element: AstElement, index) => {
+ if ("children" in element) {
+ expression += resolveOperation(element);
+ }
+ if (
+ "key" in element &&
+ "type" in element &&
+ "system" in element &&
+ "value" in element
+ ) {
+ expression += getSingleton(element);
+ }
+ if (index < operation.children.length - 1) {
+ expression +=
+ ")" + ` ${operation.operand.toLowerCase()} ` + "\n(";
+ } else {
+ if (operation.children.length > 1) {
+ expression += ")";
+ }
+ }
+ });
+
+ return expression;
+};
+
+const getSingleton = (criterion: AstBottomLayerValue): string => {
+ let expression: string = "";
+
+ //TODO: Workaround for using the value of "Therapy of Tumor" as key. Need an additional field in catalogue
+ if (criterion.key === "therapy_of_tumor") {
+ criterion.key = criterion.value as string;
+ }
+
+ const myCriterion = criterionMap.get(criterion.key);
+
+ if (myCriterion) {
+ const myCQL = cqltemplate.get(myCriterion.type);
+ if (myCQL) {
+ switch (myCriterion.type) {
+ case "gender":
+ case "BBMRI_gender":
+ case "histology":
+ case "conditionValue":
+ case "BBMRI_conditionValue":
+ case "BBMRI_conditionSampleDiagnosis":
+ case "conditionBodySite":
+ case "conditionLocalization":
+ case "observation":
+ case "uiccstadium":
+ case "observationMetastasis":
+ case "observationMetastasisBodySite":
+ case "procedure":
+ case "procedureResidualstatus":
+ case "medicationStatement":
+ case "specimen":
+ case "BBMRI_specimen":
+ case "BBMRI_hasSpecimen":
+ case "hasSpecimen":
+ case "Organization":
+ case "observationMolecularMarkerName":
+ case "observationMolecularMarkerAminoacidchange":
+ case "observationMolecularMarkerDNAchange":
+ case "observationMolecularMarkerSeqRefNCBI":
+ case "observationMolecularMarkerEnsemblID":
+ case "department":
+ case "TNMp":
+ case "TNMc": {
+ if (typeof criterion.value === "string") {
+ // TODO: Check if we really need to do this or we can somehow tell cql to do that expansion it self
+ if (
+ criterion.value.slice(-1) === "%" &&
+ criterion.value.length == 5
+ ) {
+ const mykey = criterion.value.slice(0, -2);
+ if (criteria != undefined) {
+ const expandedValues = criteria.filter(
+ (value) => value.startsWith(mykey),
+ );
+ expression += getSingleton({
+ key: criterion.key,
+ type: criterion.type,
+ system: criterion.system,
+ value: expandedValues,
+ });
+ }
+ } else if (
+ criterion.value.slice(-1) === "%" &&
+ criterion.value.length == 6
+ ) {
+ const mykey = criterion.value.slice(0, -1);
+ if (criteria != undefined) {
+ const expandedValues = criteria.filter(
+ (value) => value.startsWith(mykey),
+ );
+ expandedValues.push(
+ criterion.value.slice(0, 5),
+ );
+ expression += getSingleton({
+ key: criterion.key,
+ type: criterion.type,
+ system: criterion.system,
+ value: expandedValues,
+ });
+ }
+ } else {
+ expression += substituteCQLExpression(
+ criterion.key,
+ myCriterion.alias,
+ myCQL,
+ criterion.value as string,
+ );
+ }
+ }
+ if (typeof criterion.value === "boolean") {
+ expression += substituteCQLExpression(
+ criterion.key,
+ myCriterion.alias,
+ myCQL,
+ );
+ }
+
+ if (criterion.value instanceof Array) {
+ if (criterion.value.length === 1) {
+ expression += substituteCQLExpression(
+ criterion.key,
+ myCriterion.alias,
+ myCQL,
+ criterion.value[0],
+ );
+ } else {
+ criterion.value.forEach((value: string) => {
+ expression +=
+ "(" +
+ substituteCQLExpression(
+ criterion.key,
+ myCriterion.alias,
+ myCQL,
+ value,
+ ) +
+ ") or\n";
+ });
+ expression = expression.slice(0, -4);
+ }
+ }
+
+ break;
+ }
+
+ case "conditionRangeDate": {
+ expression += substituteRangeCQLExpression(
+ criterion,
+ myCriterion,
+ "condition",
+ "Date",
+ myCQL,
+ );
+ break;
+ }
+
+ case "primaryConditionRangeDate": {
+ expression += substituteRangeCQLExpression(
+ criterion,
+ myCriterion,
+ "primaryCondition",
+ "Date",
+ myCQL,
+ );
+ break;
+ }
+
+ case "conditionRangeAge": {
+ expression += substituteRangeCQLExpression(
+ criterion,
+ myCriterion,
+ "condition",
+ "Age",
+ myCQL,
+ );
+ break;
+ }
+
+ case "primaryConditionRangeAge": {
+ expression += substituteRangeCQLExpression(
+ criterion,
+ myCriterion,
+ "primaryCondition",
+ "Age",
+ myCQL,
+ );
+ break;
+ }
+ }
+ }
+ }
+ return expression;
+};
+
+const substituteRangeCQLExpression = (
+ criterion: AstBottomLayerValue,
+ myCriterion: { type: string; alias?: string[] },
+ criterionPrefix: string,
+ criterionSuffix: string,
+ rangeCQL: string,
+): string => {
+ const input = criterion.value as { min: number; max: number };
+ if (input === null) {
+ console.warn(
+ `Throwing away a ${criterionPrefix}Range${criterionSuffix} criterion, as it is not of type {min: number, max: number}!`,
+ );
+ return "";
+ }
+ if (input.min === 0 && input.max === 0) {
+ console.warn(
+ `Throwing away a ${criterionPrefix}Range${criterionSuffix} criterion, as both dates are undefined!`,
+ );
+ return "";
+ } else if (input.min === 0) {
+ const lowerThanDateTemplate = cqltemplate.get(
+ `${criterionPrefix}LowerThan${criterionSuffix}`,
+ );
+ if (lowerThanDateTemplate)
+ return substituteCQLExpression(
+ criterion.key,
+ myCriterion.alias,
+ lowerThanDateTemplate,
+ "",
+ input.min,
+ input.max,
+ );
+ } else if (input.max === 0) {
+ const greaterThanDateTemplate = cqltemplate.get(
+ `${criterionPrefix}GreaterThan${criterionSuffix}`,
+ );
+ if (greaterThanDateTemplate)
+ return substituteCQLExpression(
+ criterion.key,
+ myCriterion.alias,
+ greaterThanDateTemplate,
+ "",
+ input.min,
+ input.max,
+ );
+ } else {
+ return substituteCQLExpression(
+ criterion.key,
+ myCriterion.alias,
+ rangeCQL,
+ "",
+ input.min,
+ input.max,
+ );
+ }
+ return "";
+};
+
+const substituteCQLExpression = (
+ key: string,
+ alias: string[] | undefined,
+ cql: string,
+ value?: string,
+ min?: number,
+ max?: number,
+): string => {
+ let cqlString: string;
+ if (value) {
+ cqlString = cql.replace(/{{C}}/g, value);
+ } else {
+ cqlString = cql;
+ }
+ cqlString = cqlString.replace(new RegExp("{{K}}"), key);
+ if (alias && alias[0]) {
+ cqlString = cqlString.replace(new RegExp("{{A1}}", "g"), alias[0]);
+ const systemExpression =
+ "codesystem " + alias[0] + ": '" + aliasMap.get(alias[0]) + "'";
+ if (!codesystems.includes(systemExpression)) {
+ codesystems.push(systemExpression);
+ }
+ }
+ if (alias && alias[1]) {
+ cqlString = cqlString.replace(new RegExp("{{A2}}", "g"), alias[1]);
+ const systemExpression =
+ "codesystem " + alias[1] + ": '" + aliasMap.get(alias[1]) + "'";
+ if (!codesystems.includes(systemExpression)) {
+ codesystems.push(systemExpression);
+ }
+ }
+ if (min || min === 0) {
+ cqlString = cqlString.replace(new RegExp("{{D1}}"), min.toString());
+ }
+ if (max || max === 0) {
+ cqlString = cqlString.replace(new RegExp("{{D2}}"), max.toString());
+ }
+ return cqlString;
+};
+
+const getCodesystems = (): string => {
+ let codesystemString: string = "";
+
+ codesystems.forEach((systems) => {
+ codesystemString += systems + "\n";
+ });
+
+ if (codesystems.length > 0) {
+ codesystemString += "\n";
+ }
+
+ return codesystemString;
+};
diff --git a/packages/demo/src/backends/blaze.ts b/packages/demo/src/backends/blaze.ts
new file mode 100644
index 00000000..a475acd7
--- /dev/null
+++ b/packages/demo/src/backends/blaze.ts
@@ -0,0 +1,109 @@
+import { buildLibrary, buildMeasure } from "../helpers/cql-measure";
+import { responseStore } from "../stores/response";
+import type { Site } from "../types/response";
+import { measureStore } from "../stores/measures";
+
+let measureDefinitions;
+
+measureStore.subscribe((store) => {
+ measureDefinitions = store.map((measure) => measure.measure);
+});
+
+export class Blaze {
+ constructor(
+ private url: URL,
+ private name: string,
+ private auth: string = "",
+ ) {}
+
+ /**
+ * sends the query to beam and updates the store with the results
+ * @param cql the query as cql string
+ * @param controller the abort controller to cancel the request
+ */
+ async send(cql: string, controller?: AbortController): Promise {
+ try {
+ responseStore.update((store) => {
+ store.set(this.name, { status: "claimed", data: null });
+ return store;
+ });
+ const libraryResponse = await fetch(
+ new URL(`${this.url}/Library`),
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(buildLibrary(cql)),
+ signal: controller?.signal,
+ },
+ );
+ if (!libraryResponse.ok) {
+ this.handleError(
+ `Couldn't create Library in Blaze`,
+ libraryResponse,
+ );
+ }
+ const library = await libraryResponse.json();
+ const measureResponse = await fetch(
+ new URL(`${this.url}/Measure`),
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(
+ buildMeasure(library.url, measureDefinitions),
+ ),
+ signal: controller.signal,
+ },
+ );
+ if (!measureResponse.ok) {
+ this.handleError(
+ `Couldn't create Measure in Blaze`,
+ measureResponse,
+ );
+ }
+ const measure = await measureResponse.json();
+ const dataResponse = await fetch(
+ new URL(
+ `${this.url}/Measure/$evaluate-measure?measure=${measure.url}&periodStart=2000&periodEnd=2030`,
+ ),
+ {
+ signal: controller.signal,
+ },
+ );
+ if (!dataResponse.ok) {
+ this.handleError(
+ `Couldn't evaluate Measure in Blaze`,
+ dataResponse,
+ );
+ }
+ const blazeResponse: Site = await dataResponse.json();
+ responseStore.update((store) => {
+ store.set(this.name, {
+ status: "succeeded",
+ data: blazeResponse,
+ });
+ return store;
+ });
+ } catch (err) {
+ if (err.name === "AbortError") {
+ console.log(`Aborting former blaze request.`);
+ } else {
+ console.error(err);
+ }
+ }
+ }
+
+ async handleError(message: string, response: Response): Promise {
+ const errorMessage = await response.text();
+ console.debug(
+ `${message}. Received error ${response.status} with message ${errorMessage}`,
+ );
+ responseStore.update((store) => {
+ store.set(this.name, { status: "permfailed", data: null });
+ return store;
+ });
+ }
+}
diff --git a/packages/demo/src/backends/cql-measure.ts b/packages/demo/src/backends/cql-measure.ts
new file mode 100644
index 00000000..21f3b59a
--- /dev/null
+++ b/packages/demo/src/backends/cql-measure.ts
@@ -0,0 +1,92 @@
+import { v4 as uuidv4 } from "uuid";
+import type { Measure } from "../types/backend";
+
+type BuildLibraryReturn = {
+ resourceType: string;
+ url: string;
+ status: string;
+ type: {
+ coding: {
+ system: string;
+ code: string;
+ }[];
+ };
+ content: {
+ contentType: string;
+ data: string;
+ }[];
+};
+
+export const buildLibrary = (cql: string): BuildLibraryReturn => {
+ const libraryId = uuidv4();
+ const encodedQuery = btoa(unescape(encodeURIComponent(cql)));
+ return {
+ resourceType: "Library",
+ url: "urn:uuid:" + libraryId,
+ status: "active",
+ type: {
+ coding: [
+ {
+ system: "http://terminology.hl7.org/CodeSystem/library-type",
+ code: "logic-library",
+ },
+ ],
+ },
+ content: [
+ {
+ contentType: "text/cql",
+ data: encodedQuery,
+ },
+ ],
+ };
+};
+
+type BuildMeasureReturn = {
+ resourceType: string;
+ url: string;
+ status: string;
+ subjectCodeableConcept: {
+ coding: {
+ system: string;
+ code: string;
+ }[];
+ };
+ library: string;
+ scoring: {
+ coding: {
+ system: string;
+ code: string;
+ }[];
+ };
+ group: Measure[];
+};
+
+export const buildMeasure = (
+ libraryUrl: string,
+ measures: Measure[],
+): BuildMeasureReturn => {
+ const measureId = uuidv4();
+ return {
+ resourceType: "Measure",
+ url: "urn:uuid:" + measureId,
+ status: "active",
+ subjectCodeableConcept: {
+ coding: [
+ {
+ system: "http://hl7.org/fhir/resource-types",
+ code: "Patient",
+ },
+ ],
+ },
+ library: libraryUrl,
+ scoring: {
+ coding: [
+ {
+ system: "http://terminology.hl7.org/CodeSystem/measure-scoring",
+ code: "cohort",
+ },
+ ],
+ },
+ group: measures, // configuration.resultRequests.map(request => request.measures)
+ };
+};
diff --git a/packages/demo/src/backends/cqlquery-mappings.ts b/packages/demo/src/backends/cqlquery-mappings.ts
new file mode 100644
index 00000000..12bcac63
--- /dev/null
+++ b/packages/demo/src/backends/cqlquery-mappings.ts
@@ -0,0 +1,429 @@
+export const alias = new Map([
+ ["icd10", "http://fhir.de/CodeSystem/bfarm/icd-10-gm"],
+ ["loinc", "http://loinc.org"],
+ ["gradingcs", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/GradingCS"],
+ ["ops", "http://fhir.de/CodeSystem/bfarm/ops"],
+ ["morph", "urn:oid:2.16.840.1.113883.6.43.1"],
+ ["lokalisation_icd_o_3", "urn:oid:2.16.840.1.113883.6.43.1"],
+ [
+ "bodySite",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/SeitenlokalisationCS",
+ ],
+ [
+ "Therapieart",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/SYSTTherapieartCS",
+ ],
+ ["specimentype", "https://fhir.bbmri.de/CodeSystem/SampleMaterialType"],
+ [
+ "uiccstadiumcs",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/UiccstadiumCS",
+ ],
+ [
+ "lokalebeurteilungresidualstatuscs",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/LokaleBeurteilungResidualstatusCS",
+ ],
+ [
+ "gesamtbeurteilungtumorstatuscs",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/GesamtbeurteilungTumorstatusCS",
+ ],
+ [
+ "verlauflokalertumorstatuscs",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VerlaufLokalerTumorstatusCS",
+ ],
+ [
+ "verlauftumorstatuslymphknotencs",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VerlaufTumorstatusLymphknotenCS",
+ ],
+ [
+ "verlauftumorstatusfernmetastasencs",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VerlaufTumorstatusFernmetastasenCS",
+ ],
+ [
+ "vitalstatuscs",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/VitalstatusCS",
+ ],
+ ["jnucs", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/JNUCS"],
+ [
+ "fmlokalisationcs",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/FMLokalisationCS",
+ ],
+ ["TNMTCS", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMTCS"],
+ ["TNMNCS", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMNCS"],
+ ["TNMMCS", "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMMCS"],
+ [
+ "TNMySymbolCS",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMySymbolCS",
+ ],
+ [
+ "TNMrSymbolCS",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMrSymbolCS",
+ ],
+ [
+ "TNMmSymbolCS",
+ "http://dktk.dkfz.de/fhir/onco/core/CodeSystem/TNMmSymbolCS",
+ ],
+ ["molecularMarker", "http://www.genenames.org"],
+
+ ["BBMRI_icd10", "http://hl7.org/fhir/sid/icd-10"],
+ ["BBMRI_icd10gm", "http://fhir.de/CodeSystem/dimdi/icd-10-gm"],
+ [
+ "BBMRI_SampleMaterialType",
+ "https://fhir.bbmri.de/CodeSystem/SampleMaterialType",
+ ], //specimentype
+ [
+ "BBMRI_StorageTemperature",
+ "https://fhir.bbmri.de/CodeSystem/StorageTemperature",
+ ],
+ [
+ "BBMRI_SmokingStatus",
+ "http://hl7.org/fhir/uv/ips/ValueSet/current-smoking-status-uv-ips",
+ ],
+]);
+
+export const cqltemplate = new Map([
+ ["gender", "Patient.gender = '{{C}}'"],
+ ["conditionValue", "exists [Condition: Code '{{C}}' from {{A1}}]"],
+ [
+ "conditionBodySite",
+ "exists from [Condition] C\nwhere C.bodySite.coding contains Code '{{C}}' from {{A1}}",
+ ],
+ //TODO Revert to first expression if https://github.com/samply/blaze/issues/808 is solved
+ // ["conditionLocalization", "exists from [Condition] C\nwhere C.bodySite.coding contains Code '{{C}}' from {{A1}}"],
+ [
+ "conditionLocalization",
+ "exists from [Condition] C\nwhere C.bodySite.coding.code contains '{{C}}'",
+ ],
+ [
+ "conditionRangeDate",
+ "exists from [Condition] C\nwhere year from C.onset between {{D1}} and {{D2}}",
+ ],
+ [
+ "conditionLowerThanDate",
+ "exists from [Condition] C\nwhere year from C.onset <= {{D2}}",
+ ],
+ [
+ "conditionGreaterThanDate",
+ "exists from [Condition] C\nwhere year from C.onset >= {{D1}}",
+ ],
+ [
+ "conditionRangeAge",
+ "exists [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) between {{D1}} and {{D2}}",
+ ],
+ [
+ "conditionLowerThanAge",
+ "exists [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) <= {{D2}}",
+ ],
+ [
+ "conditionGreaterThanAge",
+ "exists [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) >= {{D1}}",
+ ],
+ [
+ "primaryConditionRangeDate",
+ "year from PrimaryDiagnosis.onset between {{D1}} and {{D2}}",
+ ],
+ [
+ "primaryConditionLowerThanDate",
+ "year from PrimaryDiagnosis.onset <= {{D2}}",
+ ],
+ [
+ "primaryConditionGreaterThanDate",
+ "year from PrimaryDiagnosis.onset >= {{D1}}",
+ ],
+ [
+ "primaryConditionRangeAge",
+ "AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) between {{D1}} and {{D2}}",
+ ],
+ [
+ "primaryConditionLowerThanAge",
+ "AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) <= {{D2}}",
+ ],
+ [
+ "primaryConditionGreaterThanAge",
+ "AgeInYearsAt(FHIRHelpers.ToDateTime(PrimaryDiagnosis.onset)) >= {{D1}}",
+ ],
+ //TODO Revert to first expression if https://github.com/samply/blaze/issues/808 is solved
+ // ["observation", "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value.coding contains Code '{{C}}' from {{A2}}"],
+ [
+ "observation",
+ "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'",
+ ],
+ [
+ "observationMetastasis",
+ "exists from [Observation: Code '21907-1' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'",
+ ],
+ [
+ "observationMetastasisBodySite",
+ "exists from [Observation: Code '21907-1' from {{A1}}] O\nwhere O.bodySite.coding.code contains '{{C}}'",
+ ],
+ [
+ "observationMolecularMarkerName",
+ "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}",
+ ],
+ [
+ "observationMolecularMarkerAminoacidchange",
+ "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'",
+ ], //TODO @ThomasK replace C with S
+ [
+ "observationMolecularMarkerDNAchange",
+ "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'",
+ ],
+ [
+ "observationMolecularMarkerSeqRefNCBI",
+ "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'",
+ ],
+ [
+ "observationMolecularMarkerEnsemblID",
+ "exists from [Observation: Code '69548-6' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value = '{{C}}'",
+ ],
+ ["procedure", "exists [Procedure: category in Code '{{K}}' from {{A1}}]"],
+ [
+ "procedureResidualstatus",
+ "exists from [Procedure: category in Code 'OP' from {{A1}}] P\nwhere P.outcome.coding.code contains '{{C}}'",
+ ],
+ [
+ "medicationStatement",
+ "exists [MedicationStatement: category in Code '{{K}}' from {{A1}}]",
+ ],
+ ["hasSpecimen", "exists [Specimen]"],
+ ["specimen", "exists [Specimen: Code '{{C}}' from {{A1}}]"],
+ [
+ "TNMc",
+ "exists from [Observation: Code '21908-9' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}",
+ ],
+ [
+ "TNMp",
+ "exists from [Observation: Code '21902-2' from {{A1}}] O\nwhere O.component.where(code.coding contains Code '{{K}}' from {{A1}}).value.coding contains Code '{{C}}' from {{A2}}",
+ ],
+ [
+ "Organization",
+ "Patient.managingOrganization.reference = \"Organization Ref\"('Klinisches Krebsregister/ITM')",
+ ],
+ [
+ "department",
+ "exists from [Encounter] I\nwhere I.identifier.value = '{{C}}' ",
+ ],
+ [
+ "uiccstadium",
+ "(exists ([Observation: Code '21908-9' from loinc] O where O.value.coding.code contains '{{C}}')) or (exists ([Observation: Code '21902-2' from loinc] O where O.value.coding.code contains '{{C}}'))",
+ ],
+ ["histology", "exists from [Observation: Code '59847-4' from loinc] O\n"],
+
+ ["BBMRI_gender", "Patient.gender"],
+ [
+ "BBMRI_conditionSampleDiagnosis",
+ "((exists[Condition: Code '{{C}}' from {{A1}}]) or (exists[Condition: Code '{{C}}' from {{A2}}])) or (exists from [Specimen] S where (S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/SampleDiagnosis').value.coding.code contains '{{C}}'))",
+ ],
+ ["BBMRI_conditionValue", "exists [Condition: Code '{{C}}' from {{A1}}]"],
+ [
+ "BBMRI_conditionRangeDate",
+ "exists from [Condition] C\nwhere FHIRHelpers.ToDateTime(C.onset) between {{D1}} and {{D2}}",
+ ],
+ [
+ "BBMRI_conditionRangeAge",
+ "exists from [Condition] C\nwhere AgeInYearsAt(FHIRHelpers.ToDateTime(C.onset)) between Ceiling({{D1}}) and Ceiling({{D2}})",
+ ],
+ ["BBMRI_age", "AgeInYears() between Ceiling({{D1}}) and Ceiling({{D2}})"],
+ [
+ "BBMRI_observation",
+ "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'",
+ ],
+ [
+ "BBMRI_observationSmoker",
+ "exists from [Observation: Code '72166-2' from {{A1}}] O\nwhere O.value.coding.code contains '{{C}}'",
+ ],
+ [
+ "BBMRI_observationRange",
+ "exists from [Observation: Code '{{K}}' from {{A1}}] O\nwhere O.value between {{D1}} and {{D2}}",
+ ],
+ [
+ "BBMRI_observationBodyWeight",
+ "exists from [Observation: Code '29463-7' from {{A1}}] O\nwhere ((O.value as Quantity) < {{D1}} 'kg' and (O.value as Quantity) > {{D2}} 'kg')",
+ ],
+ [
+ "BBMRI_observationBMI",
+ "exists from [Observation: Code '39156-5' from {{A1}}] O\nwhere ((O.value as Quantity) < {{D1}} 'kg/m2' and (O.value as Quantity) > {{D2}} 'kg/m2')",
+ ],
+ ["BBMRI_hasSpecimen", "exists [Specimen]"],
+ ["BBMRI_specimen", "exists [Specimen: Code '{{C}}' from {{A1}}]"],
+ ["BBMRI_retrieveSpecimenByType", "(S.type.coding.code contains '{{C}}')"],
+ [
+ "BBMRI_retrieveSpecimenByTemperature",
+ "(S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/StorageTemperature').value.coding.code contains '{{C}}')",
+ ],
+ [
+ "BBMRI_retrieveSpecimenBySamplingDate",
+ "(FHIRHelpers.ToDateTime(S.collection.collected) between {{D1}} and {{D2}})",
+ ],
+ [
+ "BBMRI_retrieveSpecimenByFastingStatus",
+ "(S.collection.fastingStatus.coding.code contains '{{C}}')",
+ ],
+ [
+ "BBMRI_samplingDate",
+ "exists from [Specimen] S\nwhere FHIRHelpers.ToDateTime(S.collection.collected) between {{D1}} and {{D2}}",
+ ],
+ [
+ "BBMRI_fastingStatus",
+ "exists from [Specimen] S\nwhere S.collection.fastingStatus.coding.code contains '{{C}}'",
+ ],
+ [
+ "BBMRI_storageTemperature",
+ "exists from [Specimen] S where (S.extension.where(url='https://fhir.bbmri.de/StructureDefinition/StorageTemperature').value.coding contains Code '{{C}}' from {{A1}})",
+ ],
+]);
+
+export const criterionMap = new Map(
+ [
+ ["gender", { type: "gender" }],
+ ["histology", { type: "histology", alias: ["loinc"] }],
+ ["diagnosis", { type: "conditionValue", alias: ["icd10"] }],
+ ["bodySite", { type: "conditionBodySite", alias: ["bodySite"] }],
+ [
+ "urn:oid:2.16.840.1.113883.6.43.1",
+ { type: "conditionLocalization", alias: ["lokalisation_icd_o_3"] },
+ ],
+ ["59542-1", { type: "observation", alias: ["loinc", "gradingcs"] }], //grading
+ [
+ "metastases_present",
+ { type: "observationMetastasis", alias: ["loinc", "jnucs"] },
+ ], //Fernmetastasen vorhanden
+ [
+ "localization_metastases",
+ {
+ type: "observationMetastasisBodySite",
+ alias: ["loinc", "fmlokalisationcs"],
+ },
+ ], //Fernmetastasen
+ ["OP", { type: "procedure", alias: ["Therapieart"] }], //Operation
+ ["ST", { type: "procedure", alias: ["Therapieart"] }], //Strahlentherapie
+ ["CH", { type: "medicationStatement", alias: ["Therapieart"] }], //Chemotherapie
+ ["HO", { type: "medicationStatement", alias: ["Therapieart"] }], //Hormontherapie
+ ["IM", { type: "medicationStatement", alias: ["Therapieart"] }], //Immuntherapie
+ ["KM", { type: "medicationStatement", alias: ["Therapieart"] }], //Knochenmarktransplantation
+ ["59847-4", { type: "observation", alias: ["loinc", "morph"] }], //Morphologie
+ ["year_of_diagnosis", { type: "conditionRangeDate" }],
+ ["year_of_primary_diagnosis", { type: "primaryConditionRangeDate" }],
+ ["sample_kind", { type: "specimen", alias: ["specimentype"] }],
+ ["pat_with_samples", { type: "hasSpecimen" }],
+ ["age_at_diagnosis", { type: "conditionRangeAge" }],
+ ["age_at_primary_diagnosis", { type: "primaryConditionRangeAge" }],
+ ["21908-9", { type: "uiccstadium", alias: ["loinc", "uiccstadiumcs"] }],
+ ["21905-5", { type: "TNMc", alias: ["loinc", "TNMTCS"] }], //tnm component
+ ["21906-3", { type: "TNMc", alias: ["loinc", "TNMNCS"] }], //tnm component
+ ["21907-1", { type: "TNMc", alias: ["loinc", "TNMMCS"] }], //tnm component
+ ["42030-7", { type: "TNMc", alias: ["loinc", "TNMmSymbolCS"] }], //tnm component
+ ["59479-6", { type: "TNMc", alias: ["loinc", "TNMySymbolCS"] }], //tnm component
+ ["21983-2", { type: "TNMc", alias: ["loinc", "TNMrSymbolCS"] }], //tnm component
+ ["21899-0", { type: "TNMp", alias: ["loinc", "TNMTCS"] }], //tnm component
+ ["21900-6", { type: "TNMp", alias: ["loinc", "TNMNCS"] }], //tnm component
+ ["21901-4", { type: "TNMp", alias: ["loinc", "TNMMCS"] }], //tnm component
+ ["42030-7", { type: "TNMp", alias: ["loinc", "TNMmSymbolCS"] }], //tnm component
+ ["59479-6", { type: "TNMp", alias: ["loinc", "TNMySymbolCS"] }], //tnm component
+ ["21983-2", { type: "TNMp", alias: ["loinc", "TNMrSymbolCS"] }], //tnm component
+
+ ["Organization", { type: "Organization" }], //organization
+ [
+ "48018-6",
+ {
+ type: "observationMolecularMarkerName",
+ alias: ["loinc", "molecularMarker"],
+ },
+ ], //molecular marker name
+ [
+ "48005-3",
+ {
+ type: "observationMolecularMarkerAminoacidchange",
+ alias: ["loinc"],
+ },
+ ], //molecular marker
+ [
+ "81290-9",
+ { type: "observationMolecularMarkerDNAchange", alias: ["loinc"] },
+ ], //molecular marker
+ [
+ "81248-7",
+ { type: "observationMolecularMarkerSeqRefNCBI", alias: ["loinc"] },
+ ], //molecular marker
+ [
+ "81249-5",
+ { type: "observationMolecularMarkerEnsemblID", alias: ["loinc"] },
+ ], //molecular marker
+
+ [
+ "local_assessment_residual_tumor",
+ {
+ type: "procedureResidualstatus",
+ alias: ["Therapieart", "lokalebeurteilungresidualstatuscs"],
+ },
+ ], //lokalebeurteilungresidualstatuscs
+ [
+ "21976-6",
+ {
+ type: "observation",
+ alias: ["loinc", "gesamtbeurteilungtumorstatuscs"],
+ },
+ ], //GesamtbeurteilungTumorstatus
+ [
+ "LA4583-6",
+ {
+ type: "observation",
+ alias: ["loinc", "verlauflokalertumorstatuscs"],
+ },
+ ], //LokalerTumorstatus
+ [
+ "LA4370-8",
+ {
+ type: "observation",
+ alias: ["loinc", "verlauftumorstatuslymphknotencs"],
+ },
+ ], //TumorstatusLymphknoten
+ [
+ "LA4226-2",
+ {
+ type: "observation",
+ alias: ["loinc", "verlauftumorstatusfernmetastasencs"],
+ },
+ ], //TumorstatusFernmetastasen
+ ["75186-7", { type: "observation", alias: ["loinc", "vitalstatuscs"] }], //Vitalstatus
+ //["Organization", {type: "Organization"}],
+ ["Organization", { type: "department" }],
+
+ ["BBMRI_gender", { type: "BBMRI_gender" }],
+ [
+ "BBMRI_diagnosis",
+ {
+ type: "BBMRI_conditionSampleDiagnosis",
+ alias: ["BBMRI_icd10", "BBMRI_icd10gm"],
+ },
+ ],
+ [
+ "BBMRI_body_weight",
+ { type: "BBMRI_observationBodyWeight", alias: ["loinc"] },
+ ], //Body weight
+ ["BBMRI_bmi", { type: "BBMRI_observationBMI", alias: ["loinc"] }], //BMI
+ [
+ "BBMRI_smoking_status",
+ { type: "BBMRI_observationSmoker", alias: ["loinc"] },
+ ], //Smoking habit
+ ["BBMRI_donor_age", { type: "BBMRI_age" }],
+ ["BBMRI_date_of_diagnosis", { type: "BBMRI_conditionRangeDate" }],
+ [
+ "BBMRI_sample_kind",
+ { type: "BBMRI_specimen", alias: ["BBMRI_SampleMaterialType"] },
+ ],
+ [
+ "BBMRI_storage_temperature",
+ {
+ type: "BBMRI_storageTemperature",
+ alias: ["BBMRI_StorageTemperature"],
+ },
+ ],
+ ["BBMRI_pat_with_samples", { type: "BBMRI_hasSpecimen" }],
+ ["BBMRI_diagnosis_age_donor", { type: "BBMRI_conditionRangeAge" }],
+ [
+ "BBMRI_fasting_status",
+ { type: "BBMRI_fastingStatus", alias: ["loinc"] },
+ ],
+ ["BBMRI_sampling_date", { type: "BBMRI_samplingDate" }],
+ ],
+);
diff --git a/packages/demo/src/backends/spot.ts b/packages/demo/src/backends/spot.ts
new file mode 100644
index 00000000..fa1f0d38
--- /dev/null
+++ b/packages/demo/src/backends/spot.ts
@@ -0,0 +1,107 @@
+/**
+ * TODO: document this class
+ */
+
+import type {
+ ResponseStore,
+ SiteData,
+ Status,
+ BeamResult,
+} from "../../../../dist/types";
+
+export class Spot {
+ private currentTask!: string;
+
+ constructor(
+ private url: URL,
+ private sites: Array,
+ ) {}
+
+ /**
+ * sends the query to beam and updates the store with the results
+ * @param query the query as base64 encoded string
+ * @param updateResponse the function to update the response store
+ * @param controller the abort controller to cancel the request
+ */
+ async send(
+ query: string,
+ updateResponse: (response: ResponseStore) => void,
+ controller: AbortController,
+ ): Promise {
+ try {
+ this.currentTask = crypto.randomUUID();
+ const beamTaskResponse = await fetch(
+ `${this.url}beam?sites=${this.sites.toString()}`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ credentials: import.meta.env.PROD ? "include" : "omit",
+ body: JSON.stringify({
+ id: this.currentTask,
+ sites: this.sites,
+ query: query,
+ }),
+ signal: controller.signal,
+ },
+ );
+ if (!beamTaskResponse.ok) {
+ const error = await beamTaskResponse.text();
+ console.debug(
+ `Received ${beamTaskResponse.status} with message ${error}`,
+ );
+ throw new Error(`Unable to create new beam task.`);
+ }
+
+ console.info(`Created new Beam Task with id ${this.currentTask}`);
+
+ const eventSource = new EventSource(
+ `${this.url.toString()}beam/${this.currentTask}?wait_count=${this.sites.length}`,
+ {
+ withCredentials: true,
+ },
+ );
+
+ /**
+ * Listenes to the new_result event from beam and updates the response store
+ */
+ eventSource.addEventListener("new_result", (message) => {
+ const response: BeamResult = JSON.parse(message.data);
+ if (response.task !== this.currentTask) return;
+ const site: string = response.from.split(".")[1];
+ const status: Status = response.status;
+ const body: SiteData =
+ status === "succeeded"
+ ? JSON.parse(atob(response.body))
+ : null;
+
+ const parsedResponse: ResponseStore = new Map().set(site, {
+ status: status,
+ data: body,
+ });
+ updateResponse(parsedResponse);
+ });
+
+ // read error events from beam
+ eventSource.addEventListener("error", (message) => {
+ console.error(`Beam returned error ${message}`);
+ eventSource.close();
+ });
+
+ // event source in javascript throws an error then the event source is closed by backend
+ eventSource.onerror = () => {
+ console.info(
+ `Querying results from sites for task ${this.currentTask} finished.`,
+ );
+ eventSource.close();
+ };
+ } catch (err) {
+ if (err instanceof Error && err.name === "AbortError") {
+ console.log(`Aborting request ${this.currentTask}`);
+ } else {
+ console.error(err);
+ }
+ }
+ }
+}
diff --git a/packages/demo/src/fragment-development.css b/packages/demo/src/fragment-development.css
index 7ed33451..148490e3 100644
--- a/packages/demo/src/fragment-development.css
+++ b/packages/demo/src/fragment-development.css
@@ -1,4 +1,4 @@
-@import "../../../node_modules/@samply/lens/dist/style.css";
+/* @import "../../../node_modules/@samply/lens/dist/style.css"; */
@import "../../lib/src/styles/index.css";
/**
diff --git a/packages/demo/src/main.ts b/packages/demo/src/main.ts
index 2fc0469d..92f73e87 100644
--- a/packages/demo/src/main.ts
+++ b/packages/demo/src/main.ts
@@ -18,7 +18,7 @@ import App from "./AppCCP.svelte";
// import './gba.css'
const app = new App({
- target: document.getElementById("app"),
+ target: document.getElementById("app") as HTMLElement,
});
export default app;
diff --git a/packages/lib/src/classes/blaze.ts b/packages/lib/src/classes/blaze.ts
index a475acd7..702536b2 100644
--- a/packages/lib/src/classes/blaze.ts
+++ b/packages/lib/src/classes/blaze.ts
@@ -1,18 +1,12 @@
import { buildLibrary, buildMeasure } from "../helpers/cql-measure";
-import { responseStore } from "../stores/response";
-import type { Site } from "../types/response";
-import { measureStore } from "../stores/measures";
-
-let measureDefinitions;
-
-measureStore.subscribe((store) => {
- measureDefinitions = store.map((measure) => measure.measure);
-});
+import type { Site, SiteData } from "../types/response";
+import type { Measure, ResponseStore } from "../types/backend";
export class Blaze {
constructor(
private url: URL,
private name: string,
+ private updateResponse: (response: ResponseStore) => void,
private auth: string = "",
) {}
@@ -20,13 +14,21 @@ export class Blaze {
* sends the query to beam and updates the store with the results
* @param cql the query as cql string
* @param controller the abort controller to cancel the request
+ * @param measureDefinitions the measure definitions to send to blaze
*/
- async send(cql: string, controller?: AbortController): Promise {
+ async send(
+ cql: string,
+ controller: AbortController,
+ measureDefinitions: Measure[],
+ ): Promise {
try {
- responseStore.update((store) => {
- store.set(this.name, { status: "claimed", data: null });
- return store;
- });
+ let response: ResponseStore = new Map().set(
+ this.name,
+ { status: "claimed", data: {} as SiteData },
+ );
+
+ this.updateResponse(response);
+
const libraryResponse = await fetch(
new URL(`${this.url}/Library`),
{
@@ -80,15 +82,15 @@ export class Blaze {
);
}
const blazeResponse: Site = await dataResponse.json();
- responseStore.update((store) => {
- store.set(this.name, {
- status: "succeeded",
- data: blazeResponse,
- });
- return store;
+
+ response = new Map().set(this.name, {
+ status: "succeeded",
+ data: blazeResponse.data,
});
+
+ this.updateResponse(response);
} catch (err) {
- if (err.name === "AbortError") {
+ if (err instanceof Error && err.name === "AbortError") {
console.log(`Aborting former blaze request.`);
} else {
console.error(err);
@@ -101,9 +103,11 @@ export class Blaze {
console.debug(
`${message}. Received error ${response.status} with message ${errorMessage}`,
);
- responseStore.update((store) => {
- store.set(this.name, { status: "permfailed", data: null });
- return store;
- });
+
+ const failedResponse: ResponseStore = new Map().set(
+ this.name,
+ { status: "permfailed", data: null },
+ );
+ this.updateResponse(failedResponse);
}
}
diff --git a/packages/lib/src/classes/spot.ts b/packages/lib/src/classes/spot.ts
index 11c76bf8..e0a312d9 100644
--- a/packages/lib/src/classes/spot.ts
+++ b/packages/lib/src/classes/spot.ts
@@ -2,24 +2,9 @@
* TODO: document this class
*/
-import { responseStore } from "../stores/response";
-import type { ResponseStore } from "../types/backend";
-
import type { SiteData, Status } from "../types/response";
+import type { ResponseStore } from "../types/backend";
-type BeamResult = {
- body: string;
- from: string;
- metadata: string;
- status: Status;
- task: string;
- to: string[];
-};
-
-/**
- * Implements requests to multiple targets through the middleware spot (see: https://github.com/samply/spot).
- * The responses are received via Server Sent Events
- */
export class Spot {
private currentTask!: string;
@@ -31,9 +16,14 @@ export class Spot {
/**
* sends the query to beam and updates the store with the results
* @param query the query as base64 encoded string
+ * @param updateResponse the function to update the response store
* @param controller the abort controller to cancel the request
*/
- async send(query: string, controller?: AbortController): Promise {
+ async send(
+ query: string,
+ updateResponse: (response: ResponseStore) => void,
+ controller: AbortController,
+ ): Promise {
try {
this.currentTask = crypto.randomUUID();
const beamTaskResponse = await fetch(
@@ -62,6 +52,9 @@ export class Spot {
console.info(`Created new Beam Task with id ${this.currentTask}`);
+ /**
+ * Listenes to the new_result event from beam and updates the response store
+ */
const eventSource = new EventSource(
`${this.url.toString()}beam/${this.currentTask}?wait_count=${this.sites.length}`,
{
@@ -78,10 +71,11 @@ export class Spot {
? JSON.parse(atob(response.body))
: null;
- responseStore.update((store: ResponseStore): ResponseStore => {
- store.set(site, { status: status, data: body });
- return store;
+ const parsedResponse: ResponseStore = new Map().set(site, {
+ status: status,
+ data: body,
});
+ updateResponse(parsedResponse);
});
// read error events from beam
diff --git a/packages/lib/src/components/DataPasser.wc.svelte b/packages/lib/src/components/DataPasser.wc.svelte
index 5d9676a4..c321aeb5 100644
--- a/packages/lib/src/components/DataPasser.wc.svelte
+++ b/packages/lib/src/components/DataPasser.wc.svelte
@@ -6,8 +6,8 @@
diff --git a/packages/lib/src/components/Options.wc.svelte b/packages/lib/src/components/Options.wc.svelte
index 5651ff73..600ac086 100644
--- a/packages/lib/src/components/Options.wc.svelte
+++ b/packages/lib/src/components/Options.wc.svelte
@@ -15,20 +15,23 @@
*/
import { lensOptions } from "../stores/options";
import { catalogue } from "../stores/catalogue";
+ import { measureStore } from "../stores/measures";
import { iconStore } from "../stores/icons";
+ import type { MeasureStore } from "../types/backend";
import type { Criteria } from "../types/treeData";
- import optionsSchema from "../interfaces/options.schema.json";
- import catalogueSchema from "../interfaces/catalogue.schema.json";
+ import optionsSchema from "../types/options.schema.json";
+ import catalogueSchema from "../types/catalogue.schema.json";
import { parser } from "@exodus/schemasafe";
import type { LensOptions } from "../types/options";
+ import { uiSiteMappingsStore } from "../stores/mappings";
export let options: LensOptions = {};
export let catalogueData: Criteria[] = [];
+ export let measures: MeasureStore = {} as MeasureStore;
/**
* Validate the options against the schema before passing them to the store
*/
-
$: {
const parse = parser(optionsSchema, {
includeErrors: true,
@@ -60,7 +63,10 @@
);
}
}
-
+ /**
+ * updates the icon store with the options passed in
+ * @param options the Lens options
+ */
const updateIconStore = (options: LensOptions): void => {
iconStore.update((store) => {
if (typeof options === "object" && "iconOptions" in options) {
@@ -103,7 +109,22 @@
});
};
+ /**
+ * watches the backendConfig for changes to populate the uiSiteMappingsStore with a map
+ * web components' props are json, meaning that Maps are not supported
+ * therefore it's a 2d array of strings which is converted to a map
+ */
+ $: uiSiteMappingsStore.update((mappings) => {
+ if (!options?.siteMappings) return mappings;
+ Object.entries(options?.siteMappings)?.forEach((site) => {
+ mappings.set(site[0], site[1]);
+ });
+
+ return mappings;
+ });
+
$: $lensOptions = options;
$: updateIconStore(options);
$: $catalogue = catalogueData;
+ $: $measureStore = measures;
diff --git a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte
index 46d2866c..c84ddf1a 100644
--- a/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte
+++ b/packages/lib/src/components/buttons/SearchButtonComponenet.wc.svelte
@@ -16,57 +16,50 @@
import { translateAstToCql } from "../../cql-translator-service/ast-to-cql-translator";
import { buildLibrary, buildMeasure } from "../../helpers/cql-measure";
import { Spot } from "../../classes/spot";
- import {
- catalogueKeyToResponseKeyMap,
- uiSiteMappingsStore,
- } from "../../stores/mappings";
- import type { Measure, BackendConfig } from "../../types/backend";
- import { responseStore } from "../../stores/response";
+ import { Blaze } from "../../classes/blaze";
+ import { catalogueKeyToResponseKeyMap } from "../../stores/mappings";
+ import { responseStore, updateResponseStore } from "../../stores/response";
+ import { lensOptions } from "../../stores/options";
+ import type {
+ BackendOptions,
+ BlazeOption,
+ Measure,
+ MeasureItem,
+ MeasureOption,
+ SpotOption,
+ } from "../../types/backend";
+ import type { AstTopLayer } from "../../types/ast";
+ import type { Site } from "../../types/response";
export let title: string = "Search";
- export let backendConfig: BackendConfig = {
- url: "http://localhost:8080",
- backends: ["dktk-test", "mannheim"],
- uiSiteMap: [
- ["dktk-test", "DKTK Test"],
- ["mannheim", "Mannheim"],
- ],
- catalogueKeyToResponseKeyMap: [],
- };
export let disabled: boolean = false;
- export let measures: Measure[] = [];
- export let backendMeasures: string = "";
- let controller: AbortController;
- /**
- * watches the backendConfig for changes to populate the uiSiteMappingsStore with a map
- * web components' props are json, meaning that Maps are not supported
- * therefore it's a 2d array of strings which is converted to a map
- */
- $: uiSiteMappingsStore.update((mappings) => {
- backendConfig.uiSiteMap.forEach((site) => {
- mappings.set(site[0], site[1]);
- });
- return mappings;
- });
+ $: options = $lensOptions?.backends as BackendOptions;
+
+ let controller: AbortController = new AbortController();
$: catalogueKeyToResponseKeyMap.update((mappings) => {
- backendConfig.catalogueKeyToResponseKeyMap.forEach((mapping) => {
- mappings.set(mapping[0], mapping[1]);
+ options?.spots?.forEach((spot) => {
+ spot.catalogueKeyToResponseKeyMap.forEach((mapping) => {
+ mappings.set(mapping[0], mapping[1]);
+ });
+ });
+ options?.blazes?.forEach((blaze: BlazeOption) => {
+ blaze.catalogueKeyToResponseKeyMap.forEach((mapping) => {
+ mappings.set(mapping[0], mapping[1]);
+ });
});
return mappings;
});
/**
- * watches the measures for changes to populate the measureStore
+ * Triggers a request to the backend.
+ * Multiple spots and blazes can be configured in lens options.
+ * Emits the ast and the updateResponseStore function to the project
+ * for running the query on other backends as well.
*/
- $: measureStore.set(measures);
-
- /**
- * triggers a request to the backend via the spot class
- */
- const getResultsFromBackend = async (): void => {
+ const getResultsFromBackend = (): void => {
if (controller) {
controller.abort();
}
@@ -75,24 +68,128 @@
controller = new AbortController();
const ast = buildAstFromQuery($queryStore);
- const cql = translateAstToCql(ast, false, backendMeasures);
- const library = buildLibrary(`${cql}`);
- const measure = buildMeasure(
- library.url,
- $measureStore.map((measureItem) => measureItem.measure),
- );
- const query = { lang: "cql", lib: library, measure: measure };
+ options?.spots?.forEach((spot: SpotOption) => {
+ const name = spot.name;
+ const measureItem: MeasureOption | undefined = $measureStore.find(
+ (measureStoreItem: MeasureOption) =>
+ spot.name === measureStoreItem.name,
+ );
+
+ if (measureItem === undefined) {
+ throw new Error(
+ `No measures found for backend ${name}. Please check the measures store.`,
+ );
+ }
+ const measures: Measure[] = measureItem.measures.map(
+ (measureItem: MeasureItem) => measureItem.measure,
+ );
+
+ const cql = translateAstToCql(
+ ast,
+ false,
+ spot.backendMeasures,
+ measureItem.measures,
+ );
+
+ const library = buildLibrary(`${cql}`);
+ const measure = buildMeasure(library.url, measures);
+ const query = { lang: "cql", lib: library, measure: measure };
+
+ const backend = new Spot(new URL(spot.url), spot.sites);
+
+ backend.send(
+ btoa(decodeURI(JSON.stringify(query))),
+ updateResponseStore,
+ controller,
+ );
+ });
+
+ options?.blazes?.forEach((blaze: BlazeOption) => {
+ const {
+ name,
+ url,
+ backendMeasures,
+ }: { name: string; url: string; backendMeasures: string } = blaze;
+
+ const measureItem: MeasureOption | undefined = $measureStore.find(
+ (measureStoreItem: MeasureOption) =>
+ name === measureStoreItem.name,
+ );
+
+ if (measureItem === undefined) {
+ throw new Error(
+ `No measures found for backend ${name}. Please check the measures store.`,
+ );
+ }
- const backend = new Spot(
- new URL(backendConfig.url),
- backendConfig.backends,
- );
+ const measures: Measure[] = measureItem.measures.map(
+ (measureItem: MeasureItem) => measureItem.measure,
+ );
- backend.send(btoa(decodeURI(JSON.stringify(query))), controller);
+ const cql = translateAstToCql(
+ ast,
+ false,
+ backendMeasures,
+ measureItem.measures,
+ );
+
+ const backend = new Blaze(new URL(url), name, updateResponseStore);
+
+ backend.send(cql, controller, measures);
+ });
+
+ options?.customAstBackends?.forEach((customAstBackendUrl: string) => {
+ customBackendCallWithAst(ast, customAstBackendUrl);
+ });
+ emitEvent(ast);
queryModified.set(false);
};
+
+ /**
+ * Sends the ast to a custom backend
+ * @param ast the ast to be sent to the backend
+ * @param url the url of the backend
+ */
+ const customBackendCallWithAst = (ast: AstTopLayer, url: string): void => {
+ fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(ast),
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ updateResponseStore(data);
+ })
+ .catch((error) => {
+ console.error("Error:", error);
+ });
+ };
+
+ interface QueryEvent extends Event {
+ detail: {
+ ast: AstTopLayer;
+ updateResponse: (response: Map) => void;
+ abortController?: AbortController;
+ };
+ }
+ /**
+ * Emits the ast and the updateResponseStore function to the project
+ * @param ast the ast to be emitted
+ */
+ const emitEvent = (ast: AstTopLayer): void => {
+ const event: QueryEvent = new CustomEvent("emit-lens-query", {
+ detail: {
+ ast: ast,
+ updateResponse: updateResponseStore,
+ abortController: controller,
+ },
+ });
+ window.dispatchEvent(event);
+ };