Skip to content

Commit

Permalink
Autocomplete is now aware of the SERVICE call the cursor is in. Bump …
Browse files Browse the repository at this point in the history
…to 0.1.8
  • Loading branch information
vemonet committed Sep 25, 2024
1 parent 3a09509 commit f10c293
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 73 deletions.
69 changes: 37 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,15 @@

</div>

A standard web component to easily deploy a user-friendly SPARQL query editor for a specific SPARQL endpoint, based on the popular [YASGUI editor](https://github.com/zazuko/Yasgui) with advanced autocomplete for predicates based on classes.
A standard web component to easily deploy a user-friendly SPARQL query editor for one or more endpoints. Built on the popular [YASGUI editor](https://github.com/zazuko/Yasgui), it provides context-aware autocomplete for classes and predicates based on the content of the endpoints.

The editor retrieves metadata about the endpoint by directly querying the SPARQL endpoint, so all you need to do is to properly document your endpoint. Reducing the need for complex infrastructure, while making your SPARQL endpoints easier to query for users and machines.
The editor retrieves metadata about the endpoints by directly querying them, so all that is needed is to generate and upload some metadata to each endpoints. Reducing the need for complex infrastructure, while making your SPARQL endpoints easier to query for users and machines.

- **Prefixes** are automatically pulled from the endpoint using their definition defined with the [SHACL ontology](https://www.w3.org/TR/shacl/) (`sh:prefix`/`sh:namespace`).
- [x] **Autocomplete possibilities for properties and classes** are automatically pulled from the endpoints based on [VoID description](https://www.w3.org/TR/void/) present in the triplestore (`void:linkPredicate|void:property` and `void:class`). The suggested properties are contextually filtered based on the class of the subject at the cursor's position, and are aware of `SERVICE` clauses, ensuring relevant autocompletion even in federated queries.

The prefixes/namespaces are retrieved with this query:
Checkout the [`void-generator`](https://github.com/JervenBolleman/void-generator) project to automatically generate VoID description for your endpoint. VoID description is retrieved using this SPARQL query:

```SPARQL
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT DISTINCT ?prefix ?namespace
WHERE { [] sh:namespace ?namespace ; sh:prefix ?prefix }
ORDER BY ?prefix
```

- **Example SPARQL queries** defined using the SHACL ontology are automatically pulled from the endpoint (queries are defined with `sh:select|sh:ask|sh:construct|sh:describe`, and their human readable description with `rdfs:comment`). Checkout the [`sparql-examples`](https://github.com/sib-swiss/sparql-examples) project for more details.

The example queries are retrieved with this SPARQL query:

```SPARQL
PREFIX sh: <http://www.w3.org/ns/shacl#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT DISTINCT ?sq ?comment ?query
WHERE {
?sq a sh:SPARQLExecutable ;
rdfs:comment ?comment ;
sh:select|sh:ask|sh:construct|sh:describe ?query .
} ORDER BY ?sq
```

- **Autocomplete possibilities for properties and classes** are automatically pulled from the endpoint based on [VoID description](https://www.w3.org/TR/void/) present in the triplestore (`void:linkPredicate|void:property` and `void:class`). The proposed properties are filtered based on the predicates available for the class of the subject related to where your cursor is 🤯. Checkout the [`void-generator`](https://github.com/JervenBolleman/void-generator) project to automatically generate VoID description for your endpoint.

VoID description is retrieved using this SPARQL query:

```SPARQL
PREFIX up: <http://purl.uniprot.org/core/>
PREFIX void: <http://rdfs.org/ns/void#>
PREFIX void-ext: <http://ldf.fi/void-ext#>
SELECT DISTINCT ?subjectClass ?prop ?objectClass ?objectDatatype
Expand All @@ -67,6 +40,32 @@ The editor retrieves metadata about the endpoint by directly querying the SPARQL
}
```

- [x] **Example SPARQL queries** defined using the SHACL ontology are automatically pulled from the endpoint (queries are defined with `sh:select|sh:ask|sh:construct|sh:describe`, and their human readable description with `rdfs:comment`).

Checkout the [`sparql-examples`](https://github.com/sib-swiss/sparql-examples) project for more details. The example queries are retrieved with this SPARQL query:

```SPARQL
PREFIX sh: <http://www.w3.org/ns/shacl#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT DISTINCT ?sq ?comment ?query
WHERE {
?sq a sh:SPARQLExecutable ;
rdfs:comment ?comment ;
sh:select|sh:ask|sh:construct|sh:describe ?query .
} ORDER BY ?sq
```

- [x] **Prefixes** are automatically pulled from the endpoint using their definition defined with the [SHACL ontology](https://www.w3.org/TR/shacl/) (`sh:prefix`/`sh:namespace`).

The prefixes/namespaces are retrieved with this query:

```SPARQL
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT DISTINCT ?prefix ?namespace
WHERE { [] sh:namespace ?namespace ; sh:prefix ?prefix }
ORDER BY ?prefix
```

👆️ You can **try it** for a few SPARQL endpoints of the SIB, such as UniProt and Bgee, here: **[sib-swiss.github.io/sparql-editor](https://sib-swiss.github.io/sparql-editor)**

![Screenshot gene](demo/screenshot_gene.png)
Expand Down Expand Up @@ -97,9 +96,15 @@ The editor retrieves metadata about the endpoint by directly querying the SPARQL
<sparql-editor endpoint="https://sparql.uniprot.org/sparql/"></sparql-editor>
```

You can also pass a list of endpoints URLs separated by commas to enable users to choose from different endpoints:

```html
<sparql-editor endpoint="https://sparql.uniprot.org/sparql/,https://www.bgee.org/sparql/"></sparql-editor>
```

> [!WARNING]
>
> Metadata are retrieved by a few lightweight queries sent from client-side JavaScript when the editor is initialized, so your SPARQL **endpoint should accept CORS** (either from \*, which is recommended, or just from the URL where the editor is deployed)
> Metadata are retrieved by a few lightweight queries sent from client-side JavaScript when the editor is initialized, so your SPARQL **endpoints should accept CORS** (either from \*, which is recommended, or just from the URL where the editor is deployed)

### ⚙️ Available attributes

Expand Down
52 changes: 27 additions & 25 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,35 @@

<body>
<div>
<h1>💫 SPARQL editors for SIB endpoints</h1>
<ul>
<li><a href="uniprot">UniProt - Protein knowledgebase</a></li>
<li><a href="bgee">Bgee - Gene expression</a></li>
<li><a href="oma">OMA - Ortholog MAtrix</a></li>
<li><a href="swisslipids">SwissLipids - A knowledge resource for lipids and their biology</a></li>
<li>
<a href="rhea"
>Rhea - expert-curated knowledgebase of chemical and transport reactions of biological interest</a
>
</li>
<li><a href="hamap">HAMAP - High-quality Automated and Manual Annotation of Proteins</a></li>
<li>
<a href="metanetx"
>MetaNetX - Automated Model Construction and Genome Annotation for Large-Scale Metabolic Networks</a
>
</li>
<li><a href="dbgi">DBGI - The Digital Botanical Garden Initiative</a></li>
</ul>
<p>
You can check if an endpoint contains the <a href="check">necessary metadata here</a> (query examples and VoID
description).
</p>
<h1 align="center">💫 SPARQL editor for SIB endpoints</h1>

<sparql-editor
endpoint="https://sparql.uniprot.org/sparql/,https://www.bgee.org/sparql/,https://sparql.omabrowser.org/sparql/,https://beta.sparql.swisslipids.org/,https://sparql.rhea-db.org/sparql/,https://biosoda.unil.ch/graphdb/repositories/emi-dbgi,https://hamap.expasy.org/sparql/,https://rdf.metanetx.org/sparql/"
examples-repository="https://github.com/sib-swiss/sparql-examples"
>
<h4>Focused SPARQL editors for SIB endpoints</h4>
<ul>
<li><a href="uniprot">UniProt - protein knowledgebase</a></li>
<li><a href="bgee">Bgee - gene expression</a></li>
<li><a href="oma">OMA - Ortholog MAtrix</a></li>
<li>
<a href="swisslipids">SwissLipids - A knowledge resource for lipids and their biology</a>
</li>
<li>
<a href="rhea">Rhea - expert-curated knowledgebase of chemical and transport reactions of biological interest</a>
</li>
<li><a href="hamap">HAMAP - High-quality Automated and Manual Annotation of Proteins</a></li>
<li><a href="dbgi">DBGI - The Digital Botanical Garden Initiative</a></li>
</ul>
<p>
You can check if an endpoint contains the <a href="check">necessary metadata here</a> (query examples and VoID
description).
</p>
</sparql-editor>
</div>
</body>

<style>
<!-- <style>
body {
display: flex;
justify-content: center;
Expand All @@ -52,5 +54,5 @@ <h1>💫 SPARQL editors for SIB endpoints</h1>
ul {
font-size: 18px;
}
</style>
</style> -->
</html>
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@sib-swiss/sparql-editor",
"version": "0.1.7",
"description": "A standard web components to easily deploy a SPARQL query editor for a specific SPARQL endpoint using the popular YASGUI editor.",
"version": "0.1.8",
"description": "A standard web component to easily deploy a user-friendly SPARQL query editor for one or more endpoints. Built on the popular YASGUI editor, it provides context-aware autocomplete for classes and predicates based on the content of the endpoints.",
"license": "MIT",
"author": {
"name": "Vincent Emonet",
Expand Down
25 changes: 13 additions & 12 deletions src/sparql-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getPrefixes,
getVoidDescription,
ExampleQuery,
getServiceUriForCursorPosition,
} from "./utils";

interface EndpointsMetadata {
Expand Down Expand Up @@ -98,7 +99,6 @@ export class SparqlEditor extends HTMLElement {
${editorCss}
`;
if (endpoints.length === 1) {
console.log("Only one endpoint, hiding the controlbar");
style.textContent += `.yasgui .controlbar {
display: none !important;
}`;
Expand Down Expand Up @@ -189,10 +189,8 @@ export class SparqlEditor extends HTMLElement {
examples: [],
};
}
// console.log("get meta", this.meta[endpoint].prefixes.size);
// if (Object.keys(this.meta[endpoint].prefixes).length < 1) {
if (!this.meta[endpoint].retrievedAt) {
console.log(`Getting metadata for ${endpoint}`);
// console.log(`Getting metadata for ${endpoint}`);
[
this.meta[endpoint].examples,
this.meta[endpoint].prefixes,
Expand Down Expand Up @@ -231,17 +229,16 @@ export class SparqlEditor extends HTMLElement {
copyEndpointOnNewTab: true,
});
await this.loadCurrentEndpoint();
console.log(this.endpointUrl());

// TODO: Not perfect, it is only triggered once for each endpoint, not everytime the endpoint changes
// It's triggered multiple times for same endpoint if you only keep 1 tab, but breaks when opening more tabs
this.yasgui?.getTab()?.on("endpointChange", async () => {
// console.log("Endpoint changed", endpoint, this.currentEndpoint());
await this.loadCurrentEndpoint();
});

this.yasgui?.on("tabSelect", async (yasgui: Yasgui, newTabId: string) => {
// @ts-ignore
this.loadCurrentEndpoint(yasgui.getTab(newTabId).endpointSelect.value);
this.loadCurrentEndpoint(yasgui.getTab(newTabId)?.getEndpoint());
});

// mermaid.initialize({ startOnLoad: false });
Expand Down Expand Up @@ -389,20 +386,24 @@ export class SparqlEditor extends HTMLElement {
voidPropertyCompleter = {
name: "voidProperty",
bulk: false,
get: (yasqe: any, token: any) => {
get: async (yasqe: any, token: any) => {
const cursor = yasqe.getCursor();
const subj = getSubjectForCursorPosition(yasqe.getValue(), cursor.line, cursor.ch);
// TODO: get the URL of the endpoint for SERVICE calls
const subjTypes = extractAllSubjectsAndTypes(yasqe.getValue());
const cursorEndpoint =
getServiceUriForCursorPosition(yasqe.getValue(), cursor.line, cursor.ch) || this.endpointUrl();
// console.log("cursorEndpoint!", cursorEndpoint);
// Make sure the metadata is loaded for the service endpoints
await this.getMetadata(cursorEndpoint);
// console.log("subj, subjTypes, unfiltered hints", subj, subjTypes, hints)
if (subj && subjTypes.has(subj) && Object.keys(this.currentEndpoint().void).length > 0) {
if (subj && subjTypes.has(subj) && Object.keys(this.meta[cursorEndpoint].void).length > 0) {
const types = subjTypes.get(subj);
// console.log("types", types)
if (types) {
const suggestPreds = new Set<string>();
try {
types.forEach(typeCurie => {
Object.keys(this.currentEndpoint().void[this.curieToUri(typeCurie)])
Object.keys(this.meta[cursorEndpoint].void[this.curieToUri(typeCurie)])
.filter(prop => prop.indexOf(token.autocompletionString) === 0)
.forEach(prop => {
suggestPreds.add(prop);
Expand All @@ -415,7 +416,7 @@ export class SparqlEditor extends HTMLElement {
if (suggestPreds.size > 0) return Array.from(suggestPreds).sort();
}
}
return this.currentEndpoint().predicates.filter(iri => iri.indexOf(token.autocompletionString) === 0);
return this.meta[cursorEndpoint].predicates.filter(iri => iri.indexOf(token.autocompletionString) === 0);
},
isValidCompletionPosition: (yasqe: any) => {
const token = yasqe.getCompleteToken();
Expand Down
36 changes: 34 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,8 @@ export function extractAllSubjectsAndTypes(query: string): Map<string, Set<strin
export function getSubjectForCursorPosition(query: string, lineNumber: number, charNumber: number): string | null {
// Extract the subject relevant to the cursor position from a SPARQL query
const lines = query.split("\n");
const currentLine = lines[lineNumber];
// Extract the part of the line up to the cursor position
const partOfLine = currentLine.slice(0, charNumber);
const partOfLine = lines[lineNumber].slice(0, charNumber);
const partialQuery = lines.slice(0, lineNumber).join("\n") + "\n" + partOfLine;
// Put all triple patterns on a single line
const cleanQuery = partialQuery.replace(/;\s*\n/g, "; ").replace(/;\s*$/g, "; ");
Expand All @@ -168,3 +167,36 @@ export function getSubjectForCursorPosition(query: string, lineNumber: number, c
}
return null;
}

export function getServiceUriForCursorPosition(query: string, lineNumber: number, charNumber: number): string | null {
const lines = query.split("\n");
const partOfLine = lines[lineNumber].slice(0, charNumber);
const partialQuery = lines.slice(0, lineNumber).join("\n") + "\n" + partOfLine;
const serviceRegex = /SERVICE\s+<([^>]+)>\s*{/gi;
let match;
// Iterate through all SERVICE blocks in the query
while ((match = serviceRegex.exec(query)) !== null) {
const serviceUri = match[1];
const serviceStart = match.index + match[0].length - 1; // Start of the opening brace '{'
// Find the matching closing brace accounting for nested braces
let braceDepth = 1;
let serviceEnd = serviceStart;
for (let i = serviceStart + 1; i < query.length; i++) {
if (query[i] === "{") {
braceDepth++;
} else if (query[i] === "}") {
braceDepth--;
if (braceDepth === 0) {
serviceEnd = i;
break;
}
}
}
// Check if cursor is inside this SERVICE block
const cursorPosition = partialQuery.length;
if (cursorPosition >= serviceStart && cursorPosition <= serviceEnd) {
return serviceUri;
}
}
return null;
}

0 comments on commit f10c293

Please sign in to comment.