From 344d1cf74f1906ecf2f83cb338124464362f2b8a Mon Sep 17 00:00:00 2001 From: Mark Lindeman Date: Tue, 28 Nov 2023 17:07:03 +0700 Subject: [PATCH] Implements pre-binding with restrictions on the CONSTRUCT query as defined by https://www.w3.org/TR/shacl/#pre-binding --- src/lib/Generator.class.ts | 5 +- src/utils/getSPARQLQuery.ts | 93 ++++++++++++++++++++++++++++--------- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/src/lib/Generator.class.ts b/src/lib/Generator.class.ts index 9a49f8c..76de66d 100644 --- a/src/lib/Generator.class.ts +++ b/src/lib/Generator.class.ts @@ -28,9 +28,8 @@ export default class Generator { } public async loadStatements($this: NamedNode): Promise> { - // 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, diff --git a/src/utils/getSPARQLQuery.ts b/src/utils/getSPARQLQuery.ts index 9fa6ad7..962ffcd 100644 --- a/src/utils/getSPARQLQuery.ts +++ b/src/utils/getSPARQLQuery.ts @@ -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 extends "select" ? SelectQuery : - T extends "construct" ? ConstructQuery : - never; +type QueryType = T extends "select" + ? SelectQuery + : T extends "construct" + ? ConstructQuery + : never; -export default function getSPARQLQuery(queryStringOrFile: string, type: T): QueryType { - let query = '' - if (queryStringOrFile.startsWith('file://')) { - const file = queryStringOrFile.replace('file://', '') +export default function getSPARQLQuery( + queryStringOrFile: string, + type: T +): QueryType { + 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; + 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 - else - throw new Error(`Unexpected querytype ${parsed.queryType}`) }