Skip to content

Commit 3f1069a

Browse files
Merge pull request #377 from samply/feat/305
feat: ast documentation and XOR, NOT handling
2 parents 5738cb2 + 7b99903 commit 3f1069a

File tree

6 files changed

+179
-9
lines changed

6 files changed

+179
-9
lines changed

book/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- [User guide](guide/README.md)
44
- [Creating a new application](guide/new-app.md)
55
- [Overwriting styles](guide/overwriting-styles.md)
6+
- [Query](guide/query.md)
67
- [Translations](guide/translations.md)
78
- [Chart reset button](guide/reset-button.md)
89
- [Development](development/README.md)

book/src/guide/ast.md

-3
This file was deleted.

book/src/guide/query.md

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Query
2+
3+
In `samply/lens`, there are three important structures for building a query:
4+
5+
- Catalogue
6+
- Query Data
7+
- Lens-AST (Abstract Syntax Tree)
8+
9+
---
10+
11+
## Catalogue
12+
13+
The **Catalogue** contains all possible query elements used in your search or exploration application. Lens expects a catalogue to be provided during initialization — even an empty one is valid.
14+
15+
The catalogue can either be:
16+
17+
- a local file included in your project, or
18+
- fetched dynamically via a REST call.
19+
20+
**⚠️Important:** While it is technically possible to retrieve and modify the catalogue at runtime, this is not recommended.
21+
22+
The structure of the catalogue is defined in [schema](https://samply.github.io/lens/docs/types/AstBottomLayerValue.html) and [type](https://samply.github.io/lens/docs/types/Catalogue.html). Valdiating your catalogue can be done within VS Code with the schema, see [here](https://frontaid.io/blog/json-schema-vscode/).
23+
24+
### Subgroups
25+
26+
The catalogue supports the definition of [subgroups](https://samply.github.io/lens/docs/types/Criteria.html#subgroup). For example, you might group all patients with diabetes at the top level, while also distinguishing between different types of diabetes. If a user wants to find patients with _any_ form of diabetes, this can be expressed using subgroups in the catalogue.
27+
28+
Subgroups allow you to structure complex concepts in a way that supports both broad and narrow search criteria.
29+
30+
### Recommended function for fetching:
31+
32+
```ts
33+
/**
34+
* Fetches the catalogue and options file from the given URLs.
35+
* @param catalogueUrl The URL of the catalogue.
36+
* @param optionsUrl The URL or path of the options file.
37+
* @returns A promise that resolves to an object containing the catalogue and options as JSON strings
38+
*/
39+
export const fetchData = async (
40+
catalogueUrl: string,
41+
optionsUrl: string,
42+
): Promise<{ catalogueJSON: string; optionsJSON: string }> => {
43+
const cataloguePromise: string = await fetch(catalogueUrl).then(
44+
(response) => response.text(),
45+
);
46+
47+
const optionsPromise: string = await fetch(optionsUrl).then((response) =>
48+
response.text(),
49+
);
50+
51+
return Promise.all([cataloguePromise, optionsPromise]).then(
52+
([catalogueJSON, optionsJSON]) => {
53+
return { catalogueJSON, optionsJSON };
54+
},
55+
);
56+
};
57+
```
58+
59+
### Svelte integration
60+
61+
If you're using Svelte, we recommend starting with this structure:
62+
63+
```ts
64+
const jsonPromises: Promise<{
65+
catalogueJSON: string;
66+
optionsJSON: string;
67+
}> = fetchData(catalogueUrl, optionsFilePath);
68+
```
69+
70+
```svelte
71+
{#await jsonPromises}
72+
<p>Loading data...</p>
73+
{:then { optionsJSON, catalogueJSON }}
74+
<lens-options {catalogueJSON} {optionsJSON} {measures}></lens-options>
75+
{:catch someError}
76+
System error: {someError.message}
77+
{/await}
78+
```
79+
80+
---
81+
82+
## Query Data
83+
84+
Once a user selects an element from the catalogue, it is added to the query store. Like the catalogue, query elements can also be added programmatically using [setQueryStoreAPI](https://samply.github.io/lens/docs/interfaces/LensDataPasser.html#setquerystoreapi).
85+
86+
Query Data is the internal representation of the user's current query. It contains all necessary information required to construct the final query output.
87+
88+
---
89+
90+
## Lens-AST
91+
92+
To allow external systems (e.g., databases or APIs) to understand the query, the internal Query Data is transformed into the **Lens-AST**.
93+
94+
AST stands for Abstract Syntax Tree. It represents the query in a structured, hierarchical format that is decoupled from the original catalogue.
95+
96+
The root of the AST is an [types](https://samply.github.io/lens/docs/types/AstElement.html). It defines the overall logical structure using one of the following operators:
97+
98+
```
99+
"AND" | "OR" | "XOR" | "NOT"
100+
```
101+
102+
The `children` of an [types](https://samply.github.io/lens/docs/types/AstElement.html) can be either another [types](https://samply.github.io/lens/docs/types/AstElement.html) or an https://samply.github.io/lens/docs/types/AstBottomLayerValue.html. An [`AstBottomLayerValue`](https://samply.github.io/lens/docs/types/AstBottomLayerValue.html) contains the actual filter expressions — for example, `gender = male`.
103+
104+
### Empty Query
105+
106+
Since Lens is designed for exploratory querying, it supports an **empty query**, which returns _all_ available data. In this case, Lens generates the following AST:
107+
108+
```json
109+
{
110+
"operand": "OR",
111+
"children": []
112+
}
113+
```
114+
115+
---
116+
117+
## AST Example
118+
119+
The AST types are located in [types](https://samply.github.io/lens/docs/types/AstElement.html). Here's an example of a more complex query structure:
120+
121+
```json
122+
{
123+
"operand": "OR",
124+
"children": [
125+
{
126+
"operand": "AND",
127+
"children": [
128+
{
129+
"key": "gender",
130+
"operand": "OR",
131+
"children": [
132+
{
133+
"key": "gender",
134+
"type": "EQUALS",
135+
"system": "",
136+
"value": "male"
137+
}
138+
]
139+
}
140+
]
141+
}
142+
]
143+
}
144+
```
145+
146+
This AST includes two nested [AstTopLayer](https://samply.github.io/lens/docs/types/AstTopLayer.html) objects with `OR` and `AND` operators. The inner [AstTopLayer](https://samply.github.io/lens/docs/types/AstTopLayer.html) contains a `key`, indicating that its children are logically grouped under this key — in this case, `gender`.
147+
148+
This layer provides context for the query at the database level. In the deepest `children` array, we see the actual condition: we are searching for patients whose gender is equal to "male".
149+
150+
---
151+
152+
### Converting Query Data to AST
153+
154+
To send a query to a database or external service, you can subscribe to the query store to get the current state, then convert it to an AST using:
155+
156+
```ts
157+
const ast = buildAstFromQueryStore(queryStore);
158+
```
159+
160+
---
161+
162+
### Handling Subgroups in AST
163+
164+
If your catalogue includes subgroups, we recommend expanding them in the query before processing. This can be done easily using:
165+
166+
```ts
167+
const astWithSubCategories = resolveAstSubCategories(ast);
168+
```
169+
170+
This function replaces subgroup references with their actual sub-elements, making the query explicit and ready for processing.

src/components/buttons/SearchButtonComponent.wc.svelte

+3-5
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@
5656
5757
const ast: AstTopLayer = buildAstFromQuery($queryStore);
5858
59-
// The root node of the AST is an OR node that has AND nodes als children - one for each search bar.
60-
// If one of the AND nodes has no children that means the corresponding search bar is empty.
59+
// The root node of the AST is an OR node that has AND nodes as children - one for each search bar.
60+
// If one of the AND nodes has no children, means the corresponding search bar is empty.
6161
if (
6262
ast.children.some(
6363
(child) => isTopLayer(child) && child.children.length === 0,
@@ -69,9 +69,7 @@
6969
console.error(
7070
"There is at least one empty and one non-empty search bar, aborting search",
7171
);
72-
showErrorToast(
73-
"Eine der Suchleisten ist leer. Löschen Sie leere Suchleisten oder fügen Sie Suchkriterien ein.",
74-
);
72+
showErrorToast(translate("search_bar_error"));
7573
return;
7674
}
7775

src/helpers/translations.ts

+4
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,8 @@ const lensTranslations: Texts = {
3939
en: "Search",
4040
de: "Suchen",
4141
},
42+
search_bar_error: {
43+
en: "One of the search fields is empty. Please delete empty search fields or enter search criteria.",
44+
de: "Eine der Suchleisten ist leer. Löschen Sie leere Suchleisten oder fügen Sie Suchkriterien ein.",
45+
},
4246
};

src/types/ast.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const isBottomLayer = (x: AstElement): x is AstBottomLayerValue =>
66
// TODO: Split this into two types, one with mandatory `key` and one without
77
export type AstTopLayer = {
88
key?: string;
9-
operand: "AND" | "OR";
9+
operand: "AND" | "OR" | "XOR" | "NOT";
1010
children: AstElement[];
1111
};
1212

0 commit comments

Comments
 (0)