Skip to content

Commit

Permalink
Implements pre-binding with restrictions on the CONSTRUCT query as de…
Browse files Browse the repository at this point in the history
  • Loading branch information
mightymax committed Nov 28, 2023
1 parent 766e662 commit 344d1cf
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 24 deletions.
5 changes: 2 additions & 3 deletions src/lib/Generator.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ export default class Generator {
}

public async loadStatements($this: NamedNode): Promise<ResultStream<Quad>> {
// const values = this.query.values ?? []
// values.push({$this})
// this.query.values = values
// Prebinding, see https://www.w3.org/TR/shacl/#pre-binding
// we know the query is safe to use replacement since we checked it before
const queryString = getSPARQLQueryString(this.query)
.replaceAll(
/[?$]\bthis\b/g,
Expand Down
93 changes: 72 additions & 21 deletions src/utils/getSPARQLQuery.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,82 @@
import chalk from 'chalk';
import fs from 'fs'
import { type SelectQuery, type ConstructQuery, Parser } from 'sparqljs'
import chalk from "chalk";
import fs from "fs";
import {
type SelectQuery,
type ConstructQuery,
Parser,
type Pattern,
} from "sparqljs";

type QueryTypes = "select" | "construct";
type QueryTypes = "select" | "construct";

type QueryType<T> =
T extends "select" ? SelectQuery :
T extends "construct" ? ConstructQuery :
never;
type QueryType<T> = T extends "select"
? SelectQuery
: T extends "construct"
? ConstructQuery
: never;

export default function getSPARQLQuery<T extends QueryTypes>(queryStringOrFile: string, type: T): QueryType<T> {
let query = ''
if (queryStringOrFile.startsWith('file://')) {
const file = queryStringOrFile.replace('file://', '')
export default function getSPARQLQuery<T extends QueryTypes>(
queryStringOrFile: string,
type: T
): QueryType<T> {
let query = "";
if (queryStringOrFile.startsWith("file://")) {
const file = queryStringOrFile.replace("file://", "");
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
throw new Error(`File not found: ${chalk.italic(file)}`);
}
query = fs.readFileSync(file, 'utf-8')
query = fs.readFileSync(file, "utf-8");
} else {
query = queryStringOrFile
query = queryStringOrFile;
}
const parsed = (new Parser()).parse(query)
if (parsed.type !== 'query') {
throw new Error(`Unexpected querytype ${parsed.type}`)
const parsed = new Parser().parse(query);
if (parsed.type !== "query") {
throw new Error(`Unexpected querytype ${parsed.type}`);
}
if (parsed.queryType.toLowerCase() === type) {
const query = parsed as QueryType<T>;
if (query.queryType === "CONSTRUCT") {
checkSPARQLConstructQuery(query.where);
}
return query;
} else throw new Error(`Unexpected querytype ${parsed.queryType}`);
}

/**
* because we use prebinding, our query must follow the rules as specified by
* https://www.w3.org/TR/shacl/#pre-binding:
* - SPARQL queries must not contain a MINUS clause
* - SPARQL queries must not contain a federated query (SERVICE)
* - SPARQL queries must not contain a VALUES clause
* - SPARQL queries must not use the syntax form `AS ?var` for any potentially pre-bound variable
*/
function checkSPARQLConstructQuery(patterns?: Pattern[]): void {
if (patterns === undefined) return;
for (const pattern of patterns) {
if (pattern.type === 'bind') {
if(pattern.variable.value === 'this') {
throw new Error('SPARQL queries must not use the syntax form `AS ?this` because it is a pre-bound variable')
}
}
if (pattern.type === "minus")
throw new Error(
"SPARQL construct queries must not contain a MINUS clause"
);
if (pattern.type === "service")
throw new Error(
"SPARQL construct queries must not contain a SERVICE clause"
);
if (pattern.type === "values")
throw new Error(
"SPARQL construct queries must not contain a VALUES clause"
);
if (
pattern.type === "optional" ||
pattern.type === "union" ||
pattern.type === "group" ||
pattern.type === "graph"
) {
checkSPARQLConstructQuery(pattern.patterns);
}
}
if (parsed.queryType.toLowerCase() === type)
return parsed as QueryType<T>
else
throw new Error(`Unexpected querytype ${parsed.queryType}`)
}

0 comments on commit 344d1cf

Please sign in to comment.