-
+
-
-
-
-
-
-
-
Suchkriterien
-
+
-
+
+
+
+
+
+
@@ -245,7 +209,7 @@
chartType="bar"
headers={therapyHeaders}
xAxisTitle="Art der Therapie"
- yAxisTitle="Anzahl der Therapien"
+ yAxisTitle="Anzahl der Therapieeinträge"
backgroundColor={JSON.stringify(barChartBackgroundColors)}
/>
@@ -255,7 +219,7 @@
catalogueGroupCode="medicationStatements"
chartType="bar"
xAxisTitle="Art der Therapie"
- yAxisTitle="Anzahl der Therapien"
+ yAxisTitle="Anzahl der Therapieeinträge"
backgroundColor={JSON.stringify(barChartBackgroundColors)}
/>
@@ -279,22 +243,29 @@
Clinical Communication Platform (CCP)
Kontakt
NutzungsvereinbarungNutzungsvereinbarung
DatenschutzDatenschutz
-
ImpressumImpressum
-
+
+
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/ccp.css b/packages/demo/src/ccp.css
index ce2674da..6412fccc 100644
--- a/packages/demo/src/ccp.css
+++ b/packages/demo/src/ccp.css
@@ -69,6 +69,14 @@ button {
}
header {
+ background-color: var(--ghost-white);
+ position: sticky;
+ top: 0px;
+ z-index: 1;
+ padding: var(--gap-xs);
+}
+
+.header-wrapper {
background-color: var(--white);
padding: var(--gap-xs);
display: grid;
@@ -76,6 +84,7 @@ header {
grid-template-columns: 1fr 1fr 1fr;
border-radius: var(--border-radius-small);
border: solid 1px var(--lightest-gray);
+
}
.logo img {
@@ -98,41 +107,52 @@ header h1 {
margin: 0;
}
-main>div,
-header,
+/* .grid,
footer {
margin: var(--gap-xs);
-}
+} */
.search {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto auto;
- grid-gap: var(--gap-s);
- padding: var(--gap-xxs) 0;
+ padding: var(--gap-xs) var(--gap-xs) var(--gap-s);
background-color: var(--ghost-white);
position: sticky;
position: -webkit-sticky;
- top: 0;
+ top: 86px;
z-index: 1;
}
+.search-wrapper {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto auto;
+ grid-gap: var(--gap-s);
+ /* padding: var(--gap-xxs) 0; */
+ background-color: var(--ghost-white);
+}
+
.grid {
position: relative;
display: grid;
grid-template-columns: 400px 1fr;
grid-gap: var(--gap-m);
+ padding: 0 var(--gap-xs) var(--gap-xs);
}
-.catalogue {
- padding: var(--gap-s);
- border-radius: var(--border-radius-small);
+.catalogue-wrapper {
+ background-color: var(--white);
border: solid 1px var(--lightest-gray);
+ border-radius: var(--border-radius-small);
+ }
+
+ .catalogue {
+ border-radius: var(--border-radius-small);
+ padding: var(--gap-s);
background-color: var(--white);
grid-row: 1/-1;
overflow-y: scroll;
- height: 100vh;
+ height: calc(100vh - 226px);
position: sticky;
- top: 40px;
+ top: 153px;
+
}
.catalogue h2 {
@@ -148,6 +168,7 @@ footer {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: var(--gap-xs);
+ grid-column: 2/3;
}
@@ -231,9 +252,6 @@ footer {
background-color: var(--white);
border-radius: var(--border-radius-small);
border: solid 1px var(--lightest-gray);
- position: sticky;
- position: -webkit-sticky;
- bottom: 0;
}
footer a {
@@ -307,15 +325,31 @@ lens-search-bar-multiple::part(info-button-dialogue) {
text-align: left;
}
-lens-search-bar-multiple::part(query-delete-button-item) {
- border-color: var(--white);
+lens-search-bar::part(delete-button-icon-item),
+lens-search-bar-multiple::part(delete-button-icon-item) {
+ filter: invert(41%) sepia(43%) saturate(4610%) hue-rotate(357deg) brightness(96%) contrast(90%);
+ transform: translate(-1px, -1px);
+ width: 20px;
}
-lens-search-bar-multiple::part(query-delete-button-group) {
- background-color: var(--white);
- border-color: var(--white);
+lens-search-bar::part(delete-button-icon-group),
+lens-search-bar-multiple::part(delete-button-icon) {
+ filter: invert(41%) sepia(43%) saturate(4610%) hue-rotate(357deg) brightness(96%) contrast(90%);
+ transform: translate(0px, 2px);
+}
+
+lens-search-bar::part(delete-button-icon),
+lens-search-bar-multiple::part(delete-button-icon-value) {
+ transform: translate(-1px, -1px);
+ width: 20px;
+}
+
+lens-search-bar::part(delete-button-icon):hover,
+lens-search-bar-multiple::part(delete-button-icon-value):hover {
+ filter: invert(38%) sepia(78%) saturate(1321%) hue-rotate(352deg) brightness(92%) contrast(99%);
}
+
lens-search-button::part(lens-search-button) {
background-color: var(--light-blue);
}
@@ -326,7 +360,6 @@ lens-search-button::part(lens-search-button):hover {
lens-search-button::part(lens-search-button):active {
background-color: #003d7e;
- transform: translateX(1px);
}
/**
@@ -337,6 +370,11 @@ lens-catalogue::part(lens-catalogue) {
padding-left: 8px;
}
+lens-catalogue::part(autocomplete-formfield-input) {
+ border: solid 1px var(--dark-gray);
+ border-radius: 0;
+}
+
lens-catalogue::part(autocomplete-formfield-input):focus {
border-color: var(--light-blue);
}
@@ -438,20 +476,6 @@ lens-catalogue::part(info-button-icon):hover {
text-align: left;
}
-lens-info-button::part(info-button-dialogue):hover {background-color: #b8bfb8}
-
-lens-info-button::part(info-button):hover {background-color: #b8bfb8}
-
-lens-info-button::part(info-button):active {
- background-color: #585958;
- transform: translateX(1px);
-}
-
-lens-info-button::part(info-button-dialogue):active {
- background-color: #585958;
- transform: translateX(1px);
-}
-
.result-table-hint-text {
padding-top: 20px;
display: flex;
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 f2bc28fb..600ac086 100644
--- a/packages/lib/src/components/Options.wc.svelte
+++ b/packages/lib/src/components/Options.wc.svelte
@@ -15,13 +15,58 @@
*/
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 "../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,
+ allErrors: true,
+ });
+ const validJSON = parse(JSON.stringify(options));
+ if (validJSON.valid === true) {
+ $lensOptions = options;
+ } else if (typeof options === "object") {
+ console.error(
+ "Lens-Options are not conform with the JSON schema",
+ validJSON.errors,
+ );
+ }
+ }
+
+ $: {
+ const parse = parser(catalogueSchema, {
+ includeErrors: true,
+ allErrors: true,
+ });
+ const validJSON = parse(JSON.stringify(catalogueData));
+ if (validJSON.valid === true) {
+ $catalogue = catalogueData;
+ } else if (typeof catalogueData === "object") {
+ console.error(
+ "Catalogue is not conform with the JSON schema",
+ validJSON.errors,
+ );
+ }
+ }
+ /**
+ * 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) {
@@ -35,6 +80,12 @@
) {
store.set("infoUrl", options.iconOptions.infoUrl);
}
+ if (
+ "deleteUrl" in options.iconOptions &&
+ typeof options.iconOptions["deleteUrl"] === "string"
+ ) {
+ store.set("deleteUrl", options.iconOptions.deleteUrl);
+ }
if (
"selectAll" in options.iconOptions &&
typeof options.iconOptions["selectAll"] === "object" &&
@@ -58,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/InfoButtonComponent.wc.svelte b/packages/lib/src/components/buttons/InfoButtonComponent.wc.svelte
index 446f1d43..9ce8f23a 100644
--- a/packages/lib/src/components/buttons/InfoButtonComponent.wc.svelte
+++ b/packages/lib/src/components/buttons/InfoButtonComponent.wc.svelte
@@ -37,7 +37,8 @@
tooltipOpen = false;
};
- const displayQueryInfo = (queryItem?: QueryItem): void => {
+ const displayQueryInfo = (e: MouseEvent, queryItem?: QueryItem): void => {
+ const target: HTMLElement = e.target as HTMLElement;
if (showQuery) {
if (onlyChildInfo && queryItem !== undefined) {
let childMessage = buildHumanReadableRecursively(
@@ -53,13 +54,19 @@
: [noQueryMessage];
}
}
- tooltipOpen = !tooltipOpen;
+ if (
+ target.getAttribute("part") !== "info-button-dialogue" &&
+ target.getAttribute("part") !== "info-button-dialogue-message"
+ ) {
+ tooltipOpen = !tooltipOpen;
+ }
};