UNPKG

sparnatural

Version:

Visual client-side SPARQL query builder and knowledge graph exploration tool

536 lines (478 loc) 14.4 kB
import { BgpPattern, BindPattern, BlankTerm, FilterPattern, GroupPattern, IriTerm, OptionalPattern, Pattern, PropertyPath, QuadTerm, ServicePattern, Term, Triple, UnionPattern, VariableExpression, VariableTerm, Wildcard } from "sparqljs"; import { Literal, Variable, NamedNode } from "@rdfjs/types"; import { Parser as SparqlParser } from "sparqljs"; import { DataFactory } from 'rdf-data-factory'; const factory = new DataFactory(); export default class SparqlFactory { static sparqlParser = new SparqlParser({ pathOnly: true } as any); /** * @param aggregation The aggregation function to apply * @param aggregatedVar The original variable being aggregated * @param asVar The final variable holding the result of the aggregation function * @returns An aggregation expression, always using a DISTINCT */ static buildAggregateFunctionExpression( aggregation:string, aggregatedVar:Variable, asVar:Variable ):VariableExpression { return { expression: { type: "aggregate", aggregation: aggregation, // always use a DISTINCT, so that we don't count duplicated results // e.g. same result in different named graphs distinct: true, expression: aggregatedVar }, variable : asVar } } static buildBgpPattern(triples: Triple[]): BgpPattern { if(triples.findIndex(t => (t == null)) > -1) { throw new Error("Trying to build a bgp pattern with null triple !"); } return { type: "bgp", triples: triples, }; } static buildGroupPattern(patterns: Pattern[]):GroupPattern { return { type: "group", patterns: patterns }; } static buildUnionPattern(patterns: Pattern[]):UnionPattern { return { type: "union", patterns: patterns }; } static buildServicePattern(patterns:Pattern[],serviceIRI:IriTerm): ServicePattern { return { type:'service', name:serviceIRI, silent:false, patterns: patterns } } static buildSubQuery(patterns: Pattern[]):GroupPattern { return { type: "group", patterns: [ { type:"query", queryType:"SELECT", prefixes:{}, variables: [new Wildcard()], where: patterns } ] }; } static buildNotExistsPattern(groupPattern: GroupPattern): FilterPattern { return { type: "filter", expression: { type: "operation", operator: "notexists", args: [ groupPattern ], }, }; } static buildOptionalPattern(patterns: Pattern[]): OptionalPattern { return { type: "optional", patterns: patterns, }; } static buildFilterRegex(texte: Literal, variable: Variable): FilterPattern { return { type: "filter", expression: { type: "operation", operator: "regex", args: [ { type: "operation", operator: "str", args: [ variable ] }, texte, factory.literal(`i`) ], }, }; } static buildFilterLangEquals(variable: Variable, lang:Literal): FilterPattern { return { type: "filter", expression: { type: "operation", operator: "=", args: [ { type: "operation", operator: "lang", args: [ variable ] }, lang ], }, }; } /** * @param firstVariable First variable in the COALESCE * @param secondvariable Second variable in the COALESCE * @param finalVariable Finale variable of the BIND clause * @returns BIND(COALESCE(?var1, ?var2) AS ?finalVar) */ static buildBindCoalescePattern(firstVariable:Variable, secondvariable:Variable, finalVariable:Variable): BindPattern { return { type: "bind", expression: { type: "operation", operator: "coalesce", args: [ firstVariable, secondvariable ] }, variable:finalVariable }; } static buildFilterStrInOrEquals(values: Literal[], variable: Variable): FilterPattern { if(values.length == 1) { return { type: "filter", expression: { type: "operation", operator: "=", args: [ { type: "operation", operator: "str", args: [ variable ] }, values[0] ], }, }; } else { return { type: "filter", expression: { type: "operation", operator: "in", args: [ { type: "operation", operator: "str", args: [ variable ] }, values ], }, }; } } static buildFilterStringEquals(texte: Literal, variable: Variable): FilterPattern { return { type: "filter", expression: { type: "operation", operator: "=", args: [ { type: "operation", operator: "lcase", args : [ variable ] }, { type: "operation", operator: "lcase", args : [texte] } ] } } ; } static buildFilterRangeDateOrNumber( rangeBegin: Literal|null, rangeEnd: Literal|null, variable: Variable ): Pattern { var filters = new Array ; if (rangeBegin != null) { filters.push( { type: "operation", operator: ">=", args: [ variable, rangeBegin ] }) ; } if (rangeEnd != null) { filters.push( { type: "operation", operator: "<=", args: [ variable, rangeEnd ] }) ; } if (filters.length == 2 ) { return { type: "filter", expression: { type: 'operation', operator: "&&", args: filters } } ; } else { return { type: "filter", expression: filters[0] } ; } } static buildTriple( subject: IriTerm | BlankTerm | VariableTerm | QuadTerm, predicate: IriTerm | VariableTerm | PropertyPath, object: Term ):Triple { return { subject: subject, predicate: predicate, object: object, }; } static buildPropertyPathTriple( subject: IriTerm | BlankTerm | VariableTerm | QuadTerm, predicate: IriTerm | PropertyPath, object: Term ):Triple { return { subject: subject, predicate: { type: 'path', pathType:'*' , items: [predicate] }, object: object, }; } static buildTypeTriple( subject: IriTerm | BlankTerm | VariableTerm | QuadTerm, predicate: IriTerm | PropertyPath, object: Term ): Triple | null { if(!subject?.value || !object?.value) return null return SparqlFactory.buildTriple( subject, predicate, object ); } // It is the intersection between the startclass and endclass chosen. // example: ?person dpedia:birthplace ?country static buildIntersectionTriple( subj: Variable, pred: string, obj: Variable ): Triple | null{ if(!subj?.value || !pred || !obj?.value) return null return { subject: subj as VariableTerm, predicate: factory.namedNode(pred), object: obj as VariableTerm, }; } static parsePropertyPath(path:string): IriTerm | PropertyPath { return this.sparqlParser.parse(path) as unknown as IriTerm | PropertyPath } static buildDateRangePattern( startDate: Literal, endDate: Literal, startClassVar: Variable, beginDatePred: NamedNode, endDatePred: NamedNode, objectVariable: Variable ): Pattern { // we have provided both begin and end date criteria if(startDate != null && endDate != null) { // 1. case where the resource has both start date and end date let firstAlternative:GroupPattern = SparqlFactory.buildGroupPattern([]); let bgp:BgpPattern = SparqlFactory.buildBgpPattern([]); let beginDateVarName = factory.variable(objectVariable.value+`_begin`); let endDateVarName = factory.variable(objectVariable.value+`_end`); bgp.triples.push( SparqlFactory.buildTriple( startClassVar, beginDatePred, beginDateVarName ) ); bgp.triples.push( SparqlFactory.buildTriple( startClassVar, endDatePred, endDateVarName ) ); firstAlternative.patterns.push(bgp); // begin date is before given end date firstAlternative.patterns.push(SparqlFactory.buildFilterRangeDateOrNumber(null, endDate, beginDateVarName)); // end date is after given start date firstAlternative.patterns.push(SparqlFactory.buildFilterRangeDateOrNumber(startDate, null, endDateVarName)); // 2. case where the resource has only a start date let secondAlternative:GroupPattern = SparqlFactory.buildGroupPattern([]); let secondBgp:BgpPattern = SparqlFactory.buildBgpPattern([]); secondBgp.triples.push( SparqlFactory.buildTriple( startClassVar, beginDatePred, beginDateVarName ) ); secondAlternative.patterns.push(secondBgp); let notExistsEndDate = SparqlFactory.buildNotExistsPattern( SparqlFactory.buildGroupPattern( [ SparqlFactory.buildBgpPattern( [ SparqlFactory.buildTriple( startClassVar, endDatePred, endDateVarName ) ] ) ] ) ); secondAlternative.patterns.push(notExistsEndDate); // begin date is before given end date secondAlternative.patterns.push(SparqlFactory.buildFilterRangeDateOrNumber(null, endDate, beginDateVarName)); // 3. case where the resource has only a end date let thirdAlternative:GroupPattern = SparqlFactory.buildGroupPattern([]); let thirdBgp:BgpPattern = SparqlFactory.buildBgpPattern([]); thirdBgp.triples.push( SparqlFactory.buildTriple( startClassVar, endDatePred, endDateVarName ) ); thirdAlternative.patterns.push(thirdBgp); let notExistsBeginDate = SparqlFactory.buildNotExistsPattern( SparqlFactory.buildGroupPattern( [ SparqlFactory.buildBgpPattern( [ SparqlFactory.buildTriple( startClassVar, beginDatePred, beginDateVarName ) ] ) ] ) ); thirdAlternative.patterns.push(notExistsBeginDate); // end date is after given start date thirdAlternative.patterns.push(SparqlFactory.buildFilterRangeDateOrNumber(startDate, null, endDateVarName)); return SparqlFactory.buildUnionPattern([firstAlternative, secondAlternative, thirdAlternative]); // we have provided only a start date } else if(startDate != null && endDate === null) { let endDateVarName = factory.variable(objectVariable.value+"_end"); var bgp = SparqlFactory.buildBgpPattern([ SparqlFactory.buildTriple( startClassVar, endDatePred, endDateVarName ) ]); // end date is after given start date var filter = SparqlFactory.buildFilterRangeDateOrNumber(startDate, null, endDateVarName); // if the resource has no end date, and has only a start date // then it necessarily overlaps with the provided open-ended range // so let's avoid this case for the moment return SparqlFactory.buildGroupPattern([bgp,filter]); // we have provided only a end date } else if(startDate === null && endDate != null) { let beginDateVarName = factory.variable(objectVariable.value+"_begin"); var bgp = SparqlFactory.buildBgpPattern([ SparqlFactory.buildTriple( startClassVar, beginDatePred, beginDateVarName ) ]); // begin date is before given end date var filter = SparqlFactory.buildFilterRangeDateOrNumber(null, endDate, beginDateVarName); return SparqlFactory.buildGroupPattern([bgp,filter]); } }; static buildDateRangeOrExactDatePattern( startDate: Literal, endDate: Literal, startClassVar: Variable, beginDatePred: NamedNode, endDatePred: NamedNode, exactDatePred: NamedNode, objectVariable: Variable ): Pattern{ if(exactDatePred != null) { // first alternative of the union to test exact date let exactDateVarName = factory.variable(objectVariable.value+"_exact"); let firstAlternative = SparqlFactory.buildGroupPattern( [ SparqlFactory.buildBgpPattern( [ SparqlFactory.buildTriple( startClassVar, exactDatePred, exactDateVarName ) ] ), // exact date is within provided date range SparqlFactory.buildFilterRangeDateOrNumber( startDate, endDate, exactDateVarName ) ] ); // second alternative to test date range let secondAlternative = SparqlFactory.buildDateRangePattern( startDate, endDate, startClassVar, beginDatePred, endDatePred, objectVariable ); // return as an array so that caller can have generic forEach loop to all // every element to outer query return SparqlFactory.buildUnionPattern([firstAlternative, secondAlternative]); } else { return SparqlFactory.buildDateRangePattern( startDate, endDate, startClassVar, beginDatePred, endDatePred, objectVariable ); } } }