diff --git a/.gitignore b/.gitignore index 6735e4eb..80805a1e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ coverage lib module node_modules +src/**.js diff --git a/notes.md b/notes.md new file mode 100644 index 00000000..bf05fd88 --- /dev/null +++ b/notes.md @@ -0,0 +1,8 @@ +1. Should be able to have supports / requires on each handler to indicate, type-wise, when other handlers are required +2. Should work in terms of SPARQLALGEBRAJS (or something similar) +3. extendPath take, as a parameter, a function that mutates the algebra. +4. The SPARQL builder that we make needs to be immutable as this is what enables branched paths. + + + +TO break down we need to go into JISON \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7f5c0fd7..6dbd505a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "license": "MIT", "dependencies": { "@rdfjs/data-model": "^1.3.4", + "@rdfjs/types": "^1.1.0", "jsonld-context-parser": "^2.1.5", - "sparqlalgebrajs": "^4.0.1" + "sparqlalgebrajs": "^4.0.1", + "sparqljs": "^3.5.2" }, "devDependencies": { "@babel/cli": "^7.16.0", @@ -21,12 +23,14 @@ "@babel/preset-env": "^7.16.4", "@comunica/actor-init-sparql-file": "^2.0.1", "@ldflex/comunica": "^4.0.0", + "@types/sparqljs": "^3.1.3", "eslint": "^8.4.0", "eslint-plugin-jest": "^26.0.0", "husky": "^8.0.1", "jest": "^28.0.3", "n3": "^1.12.2", - "semantic-release": "^19.0.2" + "semantic-release": "^19.0.2", + "typescript": "^4.7.3" } }, "node_modules/@ampproject/remapping": { @@ -7755,9 +7759,9 @@ } }, "node_modules/@rdfjs/types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-1.0.1.tgz", - "integrity": "sha512-YxVkH0XrCNG3MWeZxfg596GFe+oorTVusmNxRP6ZHTsGczZ8AGvG3UchRNkg3Fy4MyysI7vBAA5YZbESL+VmHQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-1.1.0.tgz", + "integrity": "sha512-5zm8bN2/CC634dTcn/0AhTRLaQRjXDZs3QfcAsQKNturHT7XVWcKy/8p3P5gXl+YkZTAmy7T5M/LyiT/jbkENw==", "dependencies": { "@types/node": "*" } @@ -21085,11 +21089,10 @@ } }, "node_modules/typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -27589,9 +27592,9 @@ } }, "@rdfjs/types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-1.0.1.tgz", - "integrity": "sha512-YxVkH0XrCNG3MWeZxfg596GFe+oorTVusmNxRP6ZHTsGczZ8AGvG3UchRNkg3Fy4MyysI7vBAA5YZbESL+VmHQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-1.1.0.tgz", + "integrity": "sha512-5zm8bN2/CC634dTcn/0AhTRLaQRjXDZs3QfcAsQKNturHT7XVWcKy/8p3P5gXl+YkZTAmy7T5M/LyiT/jbkENw==", "requires": { "@types/node": "*" } @@ -37768,11 +37771,10 @@ } }, "typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", - "dev": true, - "peer": true + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", + "dev": true }, "uglify-js": { "version": "3.14.5", diff --git a/package.json b/package.json index 02fc3f18..983c453f 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,10 @@ ], "dependencies": { "@rdfjs/data-model": "^1.3.4", + "@rdfjs/types": "^1.1.0", "jsonld-context-parser": "^2.1.5", - "sparqlalgebrajs": "^4.0.1" + "sparqlalgebrajs": "^4.0.1", + "sparqljs": "^3.5.2" }, "devDependencies": { "@babel/cli": "^7.16.0", @@ -33,12 +35,14 @@ "@babel/preset-env": "^7.16.4", "@comunica/actor-init-sparql-file": "^2.0.1", "@ldflex/comunica": "^4.0.0", + "@types/sparqljs": "^3.1.3", "eslint": "^8.4.0", "eslint-plugin-jest": "^26.0.0", "husky": "^8.0.1", "jest": "^28.0.3", "n3": "^1.12.2", - "semantic-release": "^19.0.2" + "semantic-release": "^19.0.2", + "typescript": "^4.7.3" }, "scripts": { "build": "npm run build:lib && npm run build:module", diff --git a/sparqlBuilder/Untitled-1.ts b/sparqlBuilder/Untitled-1.ts new file mode 100644 index 00000000..e07bd7b9 --- /dev/null +++ b/sparqlBuilder/Untitled-1.ts @@ -0,0 +1,466 @@ +import { namedNode } from '@rdfjs/data-model'; +import { Data, order } from '../types'; +import { variable } from '@rdfjs/data-model' +import * as RDF from 'rdf-js' +import { toSparql, Algebra, translate, Factory } from 'sparqlalgebrajs' + + +const factory = new Factory() + +enum order { + ASC = 'asc', + DESC = 'desc' +} + +function makeVariable(input: string | RDF.Variable): RDF.Variable +function makeVariable(input: string | RDF.Term): RDF.Term { + return typeof input === 'string' ? variable(input) : input +} + +class OperationBuilder { + private expressions: Algebra.Expression[] = [] + //private filter = [] + //private limit = [] + //private offset = [] + private patterns: Algebra.Pattern[] = [] + private variables: RDF.Variable[] = [] + distinct: boolean = false; + + algebra(): Algebra.Operation { + const bgp = factory.createBgp(this.patterns) + const project = factory.createProject(factory.createOrderBy(bgp, this.expressions), this.variables) + return translate(toSparql(this.distinct ? factory.createDistinct(project) : project)) + } + + sparql() { + return toSparql(this.algebra()) + } + // @ts-ignore + addPattern(subject: RDF.Quad_Subject, predicate: RDF.Quad_Predicate, object: RDF.Quad_Object, graph?: RDF.Quad_Graph, reverse: boolean = false) { + this.patterns.push(factory.createPattern( + reverse ? object : subject, + predicate, + reverse ? subject : object, + graph + ) + ) + } + + addVariable(variable: string | RDF.Variable) { + this.variables.push(makeVariable(variable)) + } + + addOrderBy(variable: string | RDF.Variable, ordering: order = order.ASC) { + // @ts-ignore + this.expressions.push( + ordering === order.DESC ? + // @ts-ignore + factory.createOperatorExpression(ordering, [factory.createTermExpression(makeVariable(variable))]) : + // @ts-ignore + factory.createTermExpression(makeVariable(variable)) + ) + } + // @ts-ignore + addFilter(filter, expression) { + // @ts-ignore + this.expressions.push(factory.createFilter( + filter, + expression + )) + } + +} + + + +// class SparqlBuilder { +// private _algebra: Algebra.Operation +// constructor(query: string | Algebra.Operation) { +// this._algebra = typeof query === 'string' ? translate(query) : query +// } + +// sparql() { +// return toSparql(this._algebra) +// } + +// algebra() { +// return this._algebra +// } + +// // } +// const factory = new Factory() + +// function makeVariable(input: string | RDF.Variable) { +// return typeof input === 'string' ? variable(input) : input +// } + +// class OperationBuilder { +// private expressions: Algebra.Expression[] = [] +// //private filter = [] +// //private limit = [] +// //private offset = [] +// private patterns: Algebra.Pattern[] = [] +// private variables: RDF.Variable[] = [] + + +// algebra(): Algebra.Operation { +// const bgp = factory.createBgp(this.patterns) +// // const filter = factory.createFilter(null, bgp) +// const orderBy = factory.createOrderBy(bgp, this.expressions) +// } + +// sparql() { +// toSparql(this.algebra()) +// } + +// addPattern(subject: RDF.Quad_Subject, predicate: RDF.Quad_Predicate, object: RDF.Quad_Object, graph?: RDF.Quad_Graph) { +// this.patterns.push(factory.createPattern(subject, predicate, object, graph)) +// } + +// addVariables(variable: string | RDF.Variable) { +// this.variables.push(makeVariable(variable)) +// } + +// addOrderBy(variable: string | RDF.Variable, ordering: order = order.ASC) { +// this.expressions.push(factory.createOperatorExpression(ordering, [factory.createTermExpression(makeVariable(variable))]) +// } +// } + + +// class OperationsHanlder { +// constructor(private operation: Algebra.Operation) {} + +// addExpression({ +// type, expression, operator, args +// }: { +// expression: Algebra.Expression +// }) { +// Algebra.expressionTypes +// } + +// } + + + +const NEEDS_ESCAPE = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/, + ESCAPE_ALL = /["\\\t\n\r\b\f\u0000-\u0019]|[\ud800-\udbff][\udc00-\udfff]/g, + ESCAPED_CHARS = { + '\\': '\\\\', '"': '\\"', '\t': '\\t', + '\n': '\\n', '\r': '\\r', '\b': '\\b', '\f': '\\f', + }; + +/** + * Expresses a path or mutation as a SPARQL query. + * + * Requires: + * - a mutationExpressions or pathExpression property on the path proxy + */ +export default class SparqlHandler { + async handle(pathData: Data, path: Data) { + // First check if we have a mutation expression + const mutationExpressions = await path.mutationExpressions; + if (Array.isArray(mutationExpressions) && mutationExpressions.length) + // Remove empty results to prevent dangling semicolons + return mutationExpressions.map(e => this.mutationExpressionToQuery(e)).filter(Boolean).join('\n;\n'); + + // Otherwise, fall back to checking for a path expression + const pathExpression = await path.pathExpression; + if (!Array.isArray(pathExpression)) + throw new Error(`${pathData} has no pathExpression property`); + return this.pathExpressionToQuery(pathData, path, pathExpression); + } + + pathExpressionToQuery(pathData: Data, path: Data, pathExpression) { + const builder = new OperationBuilder() + let queryVar = '?subject' + + if (pathExpression.length < 2 && !pathData.finalClause) + throw new Error(`${pathData} should at least contain a subject and a predicate`); + + // Create triple patterns + if (pathExpression.length > 1) { + queryVar = this.createVar(pathData.property); + const lastIndex = pathExpression.length - 1; + let object = skolemize(root.subject); + let queryVar = object; + let allowValues = false; + pathExpression.forEach(({ predicate, reverse, sort, values }, index) => { + // Obtain components and generate triple pattern + const subject = object; + + // Use fixed object values values if they were specified + let objects; + if (values && values.length > 0) { + if (!allowValues) + throw new Error('Specifying fixed values is not allowed here'); + objects.forEach(obj => { builder.addPattern(subject, predicate, obj, null, reverse) }) + + objects = values.map(this.termToString); + allowValues = false; // disallow subsequent fixed values for this predicate + } + // Otherwise, use a variable subject + else { + object = index < lastIndex ? this.createVar(`v${index}`, scope) : lastVar; + objects = [object]; + allowValues = true; + } + + + // If the sort option was not set, use this object as a query variable + if (!sort) + queryVar = object; + // If sort was set, use this object as a sorting variable + else { + // TODO: handle when an object is used for sorting, and later also for querying + builder.addOrderBy(object, sort) + // TODO: use a descriptive lastVar in case of sorting + object = queryVar; + } + }); + } + + builder.addVariable(pathData.select ?? queryVar) + builder.distinct = pathData.distinct ?? false + + + + let queryVar = '?subject', sorts = [], clauses = []; + if (pathExpression.length > 1) { + queryVar = this.createVar(pathData.property); + ({ queryVar, sorts, clauses } = this.expressionToTriplePatterns(pathExpression, queryVar)); + } + if (pathData.finalClause) + clauses.push(pathData.finalClause(queryVar)); + + builder.addVariable() + + + + const factory = new Factory() + + factory.createBgp([ + factory.createPattern() + ]) + + + + + + factory.createOrderBy({ + + }) + + factory.createTermExpression(RDF.variable()) + + let algebra: Algebra.Operation = { + type: Algebra.types.PROJECT, + variables: [pathData.select ?? queryVar], + input: { + type: Algebra.types.ORDER_BY, + input: { + type: Algebra.types.BGP, + + }, + expressions: sorts.map(({order, variable}) => factory.createOrderBy({ + type: Algebra.types., + + }, [{ + expressionType: Algebra.expressionTypes.TERM, + type: Algebra.types.EXPRESSION + }]) + + + // ({ + // type: Algebra.types.EXPRESSION, + // expressionType: Algebra.expressionTypes.OPERATOR, + // operator: order, + // args: [{ + // type: Algebra.types.EXPRESSION, + // expressionType: Algebra.expressionTypes.TERM, + // term: variable + // }] + // }) + + + ) } + } + + + + algebra = pathData.distinct ? {type: Algebra.types.DISTINCT, input: algebra } : algebra + + + // Create SPARQL query body + const distinct = pathData.distinct ? 'DISTINCT ' : ''; + const select = `SELECT ${distinct}${pathData.select ? pathData.select : queryVar}`; + const where = ` WHERE {\n ${clauses.join('\n ')}\n}`; + const orderClauses = sorts.map(({ order, variable }) => `${order}(${variable})`); + const orderBy = orderClauses.length === 0 ? '' : `\nORDER BY ${orderClauses.join(' ')}`; + return `${select}${where}${orderBy}`; + } + + mutationExpressionToQuery({ mutationType, conditions, predicateObjects }) { + // If there are no mutations, there is no query + if (!mutationType || !conditions || predicateObjects && predicateObjects.length === 0) + return ''; + + // Create the WHERE clauses + const scope = {}; + let subject, where; + // If the only condition is a subject, we need no WHERE clause + if (conditions.length === 1) { + subject = this.termToString(conditions[0].subject); + where = []; + } + // Otherwise, create a WHERE clause from all conditions + else { + const lastPredicate = conditions[conditions.length - 1].predicate; + subject = this.createVar(lastPredicate.value, scope); + ({ queryVar: subject, clauses: where } = + this.expressionToTriplePatterns(conditions, subject, scope)); + } + + // Create the mutation clauses + const mutations = []; + for (const { predicate, reverse, objects } of predicateObjects) { + // Mutate either only the specified objects, or all of them + const objectStrings = objects ? + objects.map(o => this.termToString(o)) : + [this.createVar(predicate.value, scope)]; + // Generate a triple pattern for all subjects + mutations.push(...this.triplePatterns(subject, predicate, objectStrings, reverse)); + } + const mutationClauses = `{\n ${mutations.join('\n ')}\n}`; + + // Join clauses into a SPARQL query + return where.length === 0 ? + // If there are no WHERE clauses, just mutate raw data + `${mutationType} DATA ${mutationClauses}` : + // Otherwise, return a DELETE/INSERT ... WHERE ... query + `${mutationType} ${mutationClauses} WHERE {\n ${where.join('\n ')}\n}`; + } + + expressionToTriplePatterns([root, ...pathExpression], lastVar, scope = {}) { + const lastIndex = pathExpression.length - 1; + const clauses = []; + const sorts = []; + let object = this.termToString(skolemize(root.subject)); + let queryVar = object; + let allowValues = false; + pathExpression.forEach((segment, index) => { + // Obtain components and generate triple pattern + const subject = object; + const { predicate, reverse, sort, values } = segment; + + // Use fixed object values values if they were specified + let objects; + if (values && values.length > 0) { + if (!allowValues) + throw new Error('Specifying fixed values is not allowed here'); + objects = values.map(this.termToString); + allowValues = false; // disallow subsequent fixed values for this predicate + } + // Otherwise, use a variable subject + else { + object = index < lastIndex ? this.createVar(`v${index}`, scope) : lastVar; + objects = [object]; + allowValues = true; + } + clauses.push(...this.triplePatterns(subject, predicate, objects, reverse)); + + // If the sort option was not set, use this object as a query variable + if (!sort) { + queryVar = object; + } + // If sort was set, use this object as a sorting variable + else { + // TODO: handle when an object is used for sorting, and later also for querying + sorts.push({ variable: object, order: sort }); + // TODO: use a descriptive lastVar in case of sorting + object = queryVar; + } + }); + return { queryVar, sorts, clauses }; + } + + // Creates a unique query variable within the given scope, based on the suggestion + createVar(suggestion = '', scope?) { + let counter = 0; + let label = `?${suggestion.match(/[a-z0-9]*$/i)[0] || 'result'}`; + if (scope) { + suggestion = label; + while (scope[label]) + label = `${suggestion}_${counter++}`; + scope[label] = true; + } + return label; + } + + // Converts an RDFJS term to a string that we can use in a query + termToString(term) { + // Determine escaped value + let { value } = term; + if (NEEDS_ESCAPE.test(value)) + value = value.replace(ESCAPE_ALL, escapeCharacter); + + switch (term.termType) { + case 'NamedNode': + return `<${value}>`; + + case 'BlankNode': + return `_:${value}`; + + case 'Literal': + // Determine optional language or datatype + let suffix = ''; + if (term.language) + suffix = `@${term.language}`; + else if (term.datatype.value !== 'http://www.w3.org/2001/XMLSchema#string') + suffix = `^^<${term.datatype.value}>`; + return `"${value}"${suffix}`; + + default: + throw new Error(`Could not convert a term of type ${term.termType}`); + } + } + + // Creates triple patterns for the given subject, predicate, and objects + triplePatterns(subjectString, predicateTerm, objectStrings, reverse = false) { + let subjectStrings = [subjectString]; + if (reverse) + [subjectStrings, objectStrings] = [objectStrings, subjectStrings]; + const objects = objectStrings.join(', '); + return subjectStrings.map(s => `${s} <${predicateTerm.value}> ${objects}.`); + } +} + +// Replaces a character by its escaped version +// (borrowed from https://www.npmjs.com/package/n3) +function escapeCharacter(character) { + // Replace a single character by its escaped version + let result = ESCAPED_CHARS[character]; + if (result === undefined) { + // Replace a single character with its 4-bit unicode escape sequence + if (character.length === 1) { + result = character.charCodeAt(0).toString(16); + result = '\\u0000'.substr(0, 6 - result.length) + result; + } + // Replace a surrogate pair with its 8-bit unicode escape sequence + else { + result = ((character.charCodeAt(0) - 0xD800) * 0x400 + + character.charCodeAt(1) + 0x2400).toString(16); + result = '\\U00000000'.substr(0, 10 - result.length) + result; + } + } + return result; +} + +// Skolemizes the given term if it is a blank node +let skolemId = 0; +function skolemize(term: RDF.Term): RDF.Term { + if (term.termType !== 'BlankNode') + return term; + if (!term.skolemized) + term.skolemized = namedNode(`urn:ldflex:sk${skolemId++}`); + return term.skolemized; +} \ No newline at end of file diff --git a/sparqlBuilder/index.ts b/sparqlBuilder/index.ts new file mode 100644 index 00000000..208b46d9 --- /dev/null +++ b/sparqlBuilder/index.ts @@ -0,0 +1,61 @@ +import { toSparql, Algebra, translate, Factory } from 'sparqlalgebrajs' +import * as RDF from '@rdfjs/types'; +const { createSeq, createPath, createInv } = new Factory; + +const seq = createSeq( + [createSeq([]), createSeq([]), createInv(createSeq([]))], +) + +const path = createPath( + +) + + +path.subject + +class SparqlBuilder { + private _variables?: RDF.Variable[]; + private _template?: Algebra.Pattern[]; + + distinct = false; + + constructor(private factory = new Factory()) { + } + + get variables(): RDF.Variable[] { + if (this._variables) + return this._variables; + + // TODO: Lazily calculate variables using the + // this.operation or this.pattern + throw new Error('Not Implemented'); + } + + get template(): Algebra.Pattern[] { + if (this._template) + return this._template; + + const { operation } = this; + if (operation.type === Algebra.types.BGP) { + return operation.patterns; + } + + throw new Error('Not Implemented'); + } + + get operation(): Algebra.Operation { + throw new Error('Not Implemented'); + } + + get select(): Algebra.Project { + return this.factory.createProject(this.operation, this.variables) + } + + get construct(): Algebra.Construct { + return this.factory.createConstruct(this.operation, this.template); + } + + get ask() { + return this.factory.createAsk(this.operation); + } +} diff --git a/src/AbstractPathResolver.js b/src/AbstractPathResolver.ts similarity index 55% rename from src/AbstractPathResolver.js rename to src/AbstractPathResolver.ts index 31b52149..51f54164 100644 --- a/src/AbstractPathResolver.js +++ b/src/AbstractPathResolver.ts @@ -1,28 +1,39 @@ import ContextProvider from './ContextProvider'; import { lazyThenable } from './promiseUtils'; import { valueToTerm } from './valueUtils'; +import type { MaybePromise, PathData, Resolver } from './types' +import { IExpandOptions, JsonLdContext } from 'jsonld-context-parser' +import * as RDF from '@rdfjs/types'; +import { Parser } from 'sparqljs'; +const parser = new Parser() + +parser.parse /** * Resolves property names of a path * to their corresponding IRIs through a JSON-LD context. * @abstract */ -export default class AbstractPathResolver { - _contextProvider = new ContextProvider(); +export default abstract class AbstractPathResolver implements Resolver { + private _contextProvider = new ContextProvider(); + + expandTerm(term: string, expandVocab?: boolean, options?: IExpandOptions) { + return this._contextProvider.expandTerm(term, expandVocab, options); + } - get _context() { - return this._contextProvider._context; + getContextRaw() { + return this._contextProvider.getContextRaw(); } - async extendContext(...contexts) { - await this._contextProvider.extendContext(...contexts); + extendContext(...contexts: JsonLdContext[]) { + return this._contextProvider.extendContext(...contexts); } /** * Creates a new resolver for the given context(s). * @param arg Either a context provider *or* a context */ - constructor(arg, ...contexts) { + constructor(arg: JsonLdContext | ContextProvider, ...contexts: JsonLdContext[]) { if (arg instanceof ContextProvider) { this._contextProvider = arg; this.extendContext(...contexts); @@ -35,7 +46,7 @@ export default class AbstractPathResolver { /** * The JSON-LD resolver supports all string properties. */ - supports(property) { + supports(property: any): boolean { return typeof property === 'string'; } @@ -45,10 +56,10 @@ export default class AbstractPathResolver { * * Example usage: person.friends.firstName */ - resolve(property, pathData) { + resolve(property: string, pathData: PathData) { const predicate = lazyThenable(() => this.expandProperty(property)); - const reverse = lazyThenable(() => this._context.then(({ contextRaw }) => - contextRaw[property] && contextRaw[property]['@reverse'])); + const reverse = this.getContextRaw().then(context => context[property]?.['@reverse']); + // const reverse = lazyThenable(() => this.getContextRaw().then(context => context[property]?.['@reverse'])) const resultsCache = this.getResultsCache(pathData, predicate, reverse); const newData = { property, predicate, resultsCache, reverse, apply: this.apply }; return pathData.extendPath(newData); @@ -60,7 +71,7 @@ export default class AbstractPathResolver { * * Example usage: person.friends.location(place).firstName */ - apply(args, pathData, path) { + apply(args, pathData: PathData, path) { if (args.length === 0) { const { property } = pathData; throw new Error(`Specify at least one term when calling .${property}() on a path`); @@ -70,22 +81,26 @@ export default class AbstractPathResolver { return path; } - async expandProperty(property) { + abstract lookupProperty(property: string): Promise; + + expandProperty(property: string): Promise { // JavaScript requires keys containing colons to be quoted, // so prefixed names would need to written as path['foaf:knows']. // We thus allow writing path.foaf_knows or path.foaf$knows instead. + // TODO: Make sure this can be captured by the types system that we develop - or not return this.lookupProperty(property.replace(/^([a-z][a-z0-9]*)[_$]/i, '$1:')); } /** * Gets the results cache for the given predicate. + * TODO: If anything results cache's should be per SPARQL algebra + * rather than per predicate - furthermore this, in general, is better + * handled by the query engine. */ - getResultsCache(pathData, predicate, reverse) { - let { propertyCache } = pathData; + getResultsCache({ propertyCache }: PathData, predicate: MaybePromise, reverse: MaybePromise) { return propertyCache && lazyThenable(async () => { // Preloading does not work with reversed predicates - propertyCache = !(await reverse) && await propertyCache; - return propertyCache && propertyCache[(await predicate).value]; + return !(await reverse) && (await propertyCache)?.[(await predicate).value]; }); } } diff --git a/src/AsyncIteratorHandler.js b/src/AsyncIteratorHandler.ts similarity index 74% rename from src/AsyncIteratorHandler.js rename to src/AsyncIteratorHandler.ts index a246d811..17115120 100644 --- a/src/AsyncIteratorHandler.js +++ b/src/AsyncIteratorHandler.ts @@ -1,4 +1,5 @@ import { iteratorFor } from './iterableUtils'; +import type { Handler, PathData } from './types' /** * AsyncIterator handler that yields either the subject or all results. @@ -8,8 +9,8 @@ import { iteratorFor } from './iterableUtils'; * - (optional) a subject on the path proxy * - (optional) results on the path proxy */ -export default class AsyncIteratorHandler { - handle({ subject }, pathProxy) { +export default class AsyncIteratorHandler implements Handler { + handle({ subject }: PathData, pathProxy: PathData['proxy']) { // Return a one-item iterator of the subject if present; // otherwise, return all results of this path return subject ? diff --git a/src/CollectionsHandler.js b/src/CollectionsHandler.ts similarity index 91% rename from src/CollectionsHandler.js rename to src/CollectionsHandler.ts index 9eb42103..28d9d6b7 100644 --- a/src/CollectionsHandler.js +++ b/src/CollectionsHandler.ts @@ -9,6 +9,7 @@ export function listHandler() { return handler((_, path) => async () => { let _path = await path; const list = []; + // TODO: Should also be doing a NamedNode termType check here while (_path && _path.value !== `${RDF}nil`) { list.push(_path[`${RDF}first`]); _path = await _path[`${RDF}rest`]; @@ -21,7 +22,7 @@ export function listHandler() { * @param {Boolean} set Emits set if True, array otherwise * @returns An handler that returns an RDF collection as an array or set */ -export function containerHandler(set) { +export function containerHandler(set?: boolean) { return handler((_, path) => async () => { let container = []; let elem; @@ -41,6 +42,7 @@ export function containerHandler(set) { export function collectionHandler() { return handler((pathData, path) => async () => { // TODO: Handle cases where multiple classes may be present (e.g. if inferencing is on) + // can probably be done via. and ask query switch ((await path[`${RDF}type`])?.value) { case `${RDF}List`: return listHandler().handle(pathData, path)(); diff --git a/src/ComplexPathResolver.js b/src/ComplexPathResolver.ts similarity index 76% rename from src/ComplexPathResolver.js rename to src/ComplexPathResolver.ts index 3ee9cd73..e2a9edf1 100644 --- a/src/ComplexPathResolver.js +++ b/src/ComplexPathResolver.ts @@ -1,16 +1,20 @@ -import { translate, toSparql } from 'sparqlalgebrajs'; +import { translate, toSparql, Algebra, Factory } from 'sparqlalgebrajs'; import AbstractPathResolver from './AbstractPathResolver'; import { namedNode } from '@rdfjs/data-model'; +import { IJsonLdContextNormalizedRaw } from 'jsonld-context-parser'; +import { Resolver } from './types'; +import * as RDF from '@rdfjs/types'; +const { } = new Factory(); /** * Writes SPARQL algebra a complex SPARQL path */ -function writePathAlgebra(algebra) { - if (algebra.type === 'join') +function writePathAlgebra(algebra: Algebra.Join | Algebra.Bgp | Algebra.Operation | Algebra.Path): string { + if (algebra.type === Algebra.types.JOIN) return algebra.input.map(x => writePathAlgebra(x)).join('/'); // The algebra library turns sequential path expressions like // foaf:friend/foaf:givenName into a bgp token rather than a path token - if (algebra.type === 'bgp' && + if (algebra.type === Algebra.types.BGP && algebra.patterns.every(quad => quad.predicate.termType === 'NamedNode') && algebra.patterns.length >= 0) { let lastObject = 's'; @@ -24,17 +28,17 @@ function writePathAlgebra(algebra) { return predicate; }).join('/'); } - if (algebra.type === 'path') { + if (algebra.type === Algebra.types.PATH) { // Note - this could be made cleaner if sparqlalgebrajs exported // the translatePathComponent function - let query = toSparql({ type: 'project', input: algebra }); + let query = toSparql(factory.createProject(algebra, [])); query = query.replace(/^SELECT WHERE \{ \?[0-9a-z]+ \(|\) \?[0-9a-z]+\. \}$/ig, ''); return query; } throw new Error(`Unhandled algebra ${algebra.type}`); } -export default class ComplexPathResolver extends AbstractPathResolver { +export default class ComplexPathResolver extends AbstractPathResolver implements Resolver { /** * Supports all strings that contain path modifiers. The regular * expression is testing for 4 main properties: @@ -48,7 +52,7 @@ export default class ComplexPathResolver extends AbstractPathResolver { * 4. /((^[(<])|([)>]$))/ * Tests for '(', '<', at the start of a string and ')', '>' at the end of a string */ - supports(property) { + supports(property: any): boolean { return super.supports(property) && (/((^|[/|])[\^])|(([a-z:>)])[*+?])|([)>*+?]|[a-z]*[:][a-z]*)[|/]([<(^]|[a-z]*[:][a-z]*)|(((^[(<])|([)>]$)))/i) .test(property); @@ -57,13 +61,13 @@ export default class ComplexPathResolver extends AbstractPathResolver { /** * Takes string and resolves it to a predicate or SPARQL path */ - async lookupProperty(property) { + async lookupProperty(property: string): Promise { // Expand the property to a full IRI - const context = await this._context; - const prefixes = {}; - for (const key in context.contextRaw) { - if (typeof context.contextRaw[key] === 'string') - prefixes[key] = context.contextRaw[key]; + const context = await this.getContextRaw(); + const prefixes: Record = {}; + for (const key in context) { + if (typeof context[key] === 'string') + prefixes[key] = context[key]; } // Wrap inside try/catch as 'translate' throws error on invalid paths let algebra; @@ -76,7 +80,7 @@ export default class ComplexPathResolver extends AbstractPathResolver { throw new Error(`The Complex Path Resolver cannot expand the '${property}' path`); } - if (algebra.input.type === 'bgp' && + if (algebra.input.type === Algebra.types.BGP && algebra.input.patterns.length === 1 && algebra.input.patterns[0].predicate.termType === 'NamedNode' && // Test to make sure the path is not an inverse path diff --git a/src/ContextProvider.js b/src/ContextProvider.js deleted file mode 100644 index 5424d51a..00000000 --- a/src/ContextProvider.js +++ /dev/null @@ -1,23 +0,0 @@ -import { ContextParser } from 'jsonld-context-parser'; - -/** - * Used to share context between multiple resolvers - */ -export default class ContextProvider { - _context = Promise.resolve({}); - - /** - * Creates a new resolver for the given context(s). - */ - constructor(...contexts) { - this.extendContext(...contexts); - } - - /** - * Extends the current context with the given context(s). - */ - async extendContext(...contexts) { - await (this._context = this._context.then(({ contextRaw }) => - new ContextParser().parse([contextRaw, ...contexts]))); - } -} diff --git a/src/ContextProvider.ts b/src/ContextProvider.ts new file mode 100644 index 00000000..7db9cd66 --- /dev/null +++ b/src/ContextProvider.ts @@ -0,0 +1,34 @@ +import { ContextParser, IExpandOptions, IJsonLdContextNormalizedRaw, JsonLdContext, JsonLdContextNormalized } from 'jsonld-context-parser'; + +/** + * Used to share context between multiple resolvers + */ +export default class ContextProvider { + private _context: Promise; + private parser = new ContextParser(); + + /** + * Creates a new resolver for the given context(s). + */ + constructor(...contexts: JsonLdContext[]) { + this._context = this.parser.parse([...contexts]); + } + + /** + * Extends the current context with the given context(s). + */ + async extendContext(...contexts: JsonLdContext[]) { + this._context = this._context.then(context => this.parser.parse([context.getContextRaw(), ...contexts])) + } + + /** + * @return The raw inner context + */ + async getContextRaw(): Promise { + return (await this._context).getContextRaw(); + } + + async expandTerm(term: string, expandVocab?: boolean, options?: IExpandOptions) { + return (await this._context).expandTerm(term, expandVocab, options) + } +} diff --git a/src/DataHandler.js b/src/DataHandler.ts similarity index 57% rename from src/DataHandler.js rename to src/DataHandler.ts index 8f3b043b..81faa775 100644 --- a/src/DataHandler.js +++ b/src/DataHandler.ts @@ -1,3 +1,10 @@ +import { Handler } from "./types"; + +interface Options { + async?: boolean; + function?: boolean; +} + /** * Resolves to the given item in the path data. * For example, new DataHandler({}, 'foo', 'bar') @@ -6,35 +13,39 @@ * Resolution can optionally be async, * and/or be behind a function call. */ -export default class DataHandler { - constructor(options, ...dataProperties) { - this._isAsync = options.async; - this._isFunction = options.function; +export default class DataHandler implements Handler { + private _isAsync: boolean; + private _isFunction: boolean; + private _dataProperties: string[] + + constructor(options: Options, ...dataProperties: string[]) { + this._isAsync = Boolean(options.async); + this._isFunction = Boolean(options.function); this._dataProperties = dataProperties; } - static sync(...dataProperties) { + static sync(...dataProperties: string[]) { return new DataHandler({ async: false }, ...dataProperties); } - static syncFunction(...dataProperties) { + static syncFunction(...dataProperties: string[]) { return new DataHandler({ async: false, function: true }, ...dataProperties); } - static async(...dataProperties) { + static async(...dataProperties: string[]) { return new DataHandler({ async: true }, ...dataProperties); } - static asyncFunction(...dataProperties) { + static asyncFunction(...dataProperties: string[]) { return new DataHandler({ async: true, function: true }, ...dataProperties); } // Resolves the data path, or returns a function that does so handle(pathData) { - return !this._isFunction ? - this._resolveDataPath(pathData) : - () => this._resolveDataPath(pathData); + return this._isFunction ? + () => this._resolveDataPath(pathData) : + this._resolveDataPath(pathData); } // Resolves the data path @@ -46,15 +57,16 @@ export default class DataHandler { // Resolves synchronous property access _resolveSyncDataPath(data) { + return this._dataProperties.reduce(prop => data?.[prop]) for (const property of this._dataProperties) - data = data && data[property]; + data &&= data[property]; return data; } // Resolves asynchronous property access async _resolveAsyncDataPath(data) { for (const property of this._dataProperties) - data = data && await data[property]; + data &&= await data[property]; return data; } } diff --git a/src/DeleteFunctionHandler.js b/src/DeleteFunctionHandler.ts similarity index 77% rename from src/DeleteFunctionHandler.js rename to src/DeleteFunctionHandler.ts index af7f7581..15f934b4 100644 --- a/src/DeleteFunctionHandler.js +++ b/src/DeleteFunctionHandler.ts @@ -1,9 +1,10 @@ import MutationFunctionHandler from './MutationFunctionHandler'; +import { Handler } from './types'; /** * A MutationFunctionHandler for deletions. */ -export default class DeleteFunctionHandler extends MutationFunctionHandler { +export default class DeleteFunctionHandler extends MutationFunctionHandler implements Handler { constructor() { super('DELETE', true); } diff --git a/src/ExecuteQueryHandler.js b/src/ExecuteQueryHandler.ts similarity index 54% rename from src/ExecuteQueryHandler.js rename to src/ExecuteQueryHandler.ts index c949b647..96b6e121 100644 --- a/src/ExecuteQueryHandler.js +++ b/src/ExecuteQueryHandler.ts @@ -1,3 +1,7 @@ +import { Bindings, Term } from '@rdfjs/types'; +import { streamToAsyncIterable } from './iterableUtils'; +import { Handler, PathData } from './types'; + /** * Executes the query represented by a path. * @@ -6,8 +10,8 @@ * - a sparql property on the path proxy * - (optional) a resultsCache property on the path data */ -export default class ExecuteQueryHandler { - async *handle(pathData, path) { +export default class ExecuteQueryHandler implements Handler { + async *handle(pathData: PathData, path) { // Try to retrieve the result from cache const resultsCache = await pathData.resultsCache; if (resultsCache) { @@ -28,20 +32,18 @@ export default class ExecuteQueryHandler { return; // Extract the term from every query result - for await (const bindings of queryEngine.execute(query)) - yield this.extractTerm(bindings, pathData); - } + const resultsStream = streamToAsyncIterable(await queryEngine.queryBindings(query)); + + for await (const bindings of resultsStream) + yield pathData.extendPath({ subject: getSingleTerm(bindings) }, null); } + +} - /** - * Extracts the first term from a query result binding as a new path. - */ - extractTerm(binding, pathData) { - // Extract the first term from the binding map - if (binding.size !== 1) - throw new Error('Only single-variable queries are supported'); - const subject = binding.values().next().value; +function getSingleTerm(binding: Bindings): Term { + // Extract the first term from the binding map + if (binding.size === 1) + for (const subject of binding.values()) + return subject; - // Each result is a new path that starts from the term as subject - return pathData.extendPath({ subject }, null); - } + throw new Error('Only single-variable queries are supported'); } diff --git a/src/GetFunctionHandler.js b/src/GetFunctionHandler.ts similarity index 87% rename from src/GetFunctionHandler.js rename to src/GetFunctionHandler.ts index aa772f1a..28d1c261 100644 --- a/src/GetFunctionHandler.js +++ b/src/GetFunctionHandler.ts @@ -1,5 +1,6 @@ import { isPlainObject, isAsyncIterable } from './valueUtils'; import { iterableToArray } from './iterableUtils'; +import { Handler, PathData } from './types'; /** * Returns a function that requests the values of multiple properties. @@ -12,13 +13,13 @@ import { iterableToArray } from './iterableUtils'; * - fn({ p1: null, p2: null }) returns { p1: path[p1], p2: path[p2] } * Combinations of the above are possible by passing them in arrays. */ -export default class GetFunctionHandler { - handle(pathData, path) { +export default class GetFunctionHandler implements Handler { + handle(pathData: PathData, path) { return (...args) => this.readProperties(path, args.length === 1 ? args[0] : args, true); } - async readProperties(path, properties, wrapSingleValues = false) { + async readProperties(path, properties: string | AsyncIterable | string[], wrapSingleValues = false) { // Convert an async iterable to an array if (isAsyncIterable(properties)) properties = await iterableToArray(properties); diff --git a/src/InsertFunctionHandler.js b/src/InsertFunctionHandler.ts similarity index 77% rename from src/InsertFunctionHandler.js rename to src/InsertFunctionHandler.ts index 4ec5c533..d423f220 100644 --- a/src/InsertFunctionHandler.js +++ b/src/InsertFunctionHandler.ts @@ -1,9 +1,10 @@ import MutationFunctionHandler from './MutationFunctionHandler'; +import { Handler } from './types'; /** * A MutationFunctionHandler for insertions. */ -export default class InsertFunctionHandler extends MutationFunctionHandler { +export default class InsertFunctionHandler extends MutationFunctionHandler implements Handler { constructor() { super('INSERT', false); } diff --git a/src/JSONLDResolver.js b/src/JSONLDResolver.ts similarity index 67% rename from src/JSONLDResolver.js rename to src/JSONLDResolver.ts index 31ec17b2..6b77a97a 100644 --- a/src/JSONLDResolver.js +++ b/src/JSONLDResolver.ts @@ -1,19 +1,19 @@ import { Util as ContextUtil } from 'jsonld-context-parser'; import { namedNode } from '@rdfjs/data-model'; import AbstractPathResolver from './AbstractPathResolver'; +import { Resolver } from './types'; /** * Resolves property names of a path * to their corresponding IRIs through a JSON-LD context. */ -export default class JSONLDResolver extends AbstractPathResolver { +export default class JSONLDResolver extends AbstractPathResolver implements Resolver { /** * Expands a JSON property key into a full IRI. */ - async lookupProperty(property) { - const context = await this._context; - const expandedProperty = context.expandTerm(property, true); - if (!ContextUtil.isValidIri(expandedProperty)) + async lookupProperty(property: string) { + const expandedProperty = await this.expandTerm(property, true); + if (expandedProperty === null || !ContextUtil.isValidIri(expandedProperty)) throw new Error(`The JSON-LD context cannot expand the '${property}' property`); return namedNode(expandedProperty); } diff --git a/src/MutationExpressionsHandler.js b/src/MutationExpressionsHandler.ts similarity index 79% rename from src/MutationExpressionsHandler.js rename to src/MutationExpressionsHandler.ts index 3bfbbae2..33922b32 100644 --- a/src/MutationExpressionsHandler.js +++ b/src/MutationExpressionsHandler.ts @@ -1,3 +1,5 @@ +import { Handler, PathData } from "./types"; + /** * Traverses a path to collect mutationExpressions into an expression. * This is needed because mutations can be chained. @@ -5,8 +7,8 @@ * Requires: * - a mutationExpressions property on the path proxy */ -export default class MutationExpressionsHandler { - async handle(pathData) { +export default class MutationExpressionsHandler implements Handler { + async handle(pathData: PathData) { const mutationExpressions = []; // Add all mutationExpressions to the path diff --git a/src/MutationFunctionHandler.js b/src/MutationFunctionHandler.ts similarity index 76% rename from src/MutationFunctionHandler.js rename to src/MutationFunctionHandler.ts index 4472159c..7bbec80c 100644 --- a/src/MutationFunctionHandler.js +++ b/src/MutationFunctionHandler.ts @@ -4,6 +4,7 @@ import { ensureArray, joinArrays, valueToTerm, hasPlainObjectArgs, isAsyncIterable, } from './valueUtils'; +import { Handler, MaybePromise, PathData } from './types'; /** * Returns a function that, when called with arguments, @@ -20,17 +21,18 @@ import { * Requires: * - a pathExpression property on the path proxy and all non-raw arguments. */ -export default class MutationFunctionHandler { - constructor(mutationType, allowZeroArgs) { - this._mutationType = mutationType; - this._allowZeroArgs = allowZeroArgs; +export default class MutationFunctionHandler implements Handler { + constructor( + private mutationType?: 'INSERT' | 'DELETE', + private allowZeroArgs?: boolean + ) { } // Creates a function that performs a mutation - handle(pathData, path) { + handle(pathData: PathData, path) { return (...args) => { // Check if the given arguments are valid - if (!this._allowZeroArgs && !args.length) + if (!this.allowZeroArgs && !args.length) throw new Error('Mutation cannot be invoked without arguments'); // Create a lazy Promise to the mutation expressions @@ -75,14 +77,14 @@ export default class MutationFunctionHandler { // Create a mutation, unless no objects are affected (`null` means all) return objects !== null && objects.length === 0 ? {} : { - mutationType: this._mutationType, + mutationType: this.mutationType, conditions: conditions.slice(0, -1), predicateObjects: [{ predicate, reverse, objects }], }; } // Extracts individual objects from a set of values passed to a mutation function - async extractObjects(pathData, path, values) { + async extractObjects(pathData: PathData, path, values) { // If no specific values are specified, match all (represented by `null`) if (values.length === 0) return null; @@ -91,7 +93,7 @@ export default class MutationFunctionHandler { const objects = []; for (const value of values) { if (!isAsyncIterable(value)) - // Add a (promise to) a single value + // Add a (promise to) a single value objects.push(await value); // Add multiple values from a path else @@ -100,3 +102,30 @@ export default class MutationFunctionHandler { return objects.map(valueToTerm); } } + +async function extract(values: (MaybePromise> | MaybePromise)[]): Promise { + // If no specific values are specified, match all (represented by `null`) + if (values.length === 0) + return null; + + // Otherwise, expand singular values, promises, and paths + const objects: T[] = []; + for (const value of values) { + if (!isAsyncIterable(value)) + // Add a (promise to) a single value + objects.push(await value); + // Add multiple values from a path + else + objects.push(...(await iterableToArray(value))); + } + + return objects; +} + + +export async function iterableToArray(iterable: AsyncIterable): Promise { + const items: T[] = []; + for await (const item of iterable) + items.push(item); + return items; +} \ No newline at end of file diff --git a/src/PathExpressionHandler.js b/src/PathExpressionHandler.ts similarity index 88% rename from src/PathExpressionHandler.js rename to src/PathExpressionHandler.ts index b664b07f..4d8f1ae7 100644 --- a/src/PathExpressionHandler.js +++ b/src/PathExpressionHandler.ts @@ -1,7 +1,9 @@ +import { Handler } from "./types"; + /** * Traverses a path to collect links and nodes into an expression. */ -export default class PathExpressionHandler { +export default class PathExpressionHandler implements Handler { async handle(pathData) { const segments = []; let current = pathData; diff --git a/src/PathFactory.js b/src/PathFactory.ts similarity index 83% rename from src/PathFactory.js rename to src/PathFactory.ts index ffdc67e8..60698efc 100644 --- a/src/PathFactory.js +++ b/src/PathFactory.ts @@ -4,12 +4,18 @@ import ComplexPathResolver from './ComplexPathResolver'; import defaultHandlers from './defaultHandlers'; import { ContextParser } from 'jsonld-context-parser'; import ContextProvider from './ContextProvider'; +import { handler } from './handlerUtil' +import { Handler, HandlerFunction, Settings } from './types'; /** * A PathFactory creates paths with default settings. */ export default class PathFactory { - constructor(settings, data) { + private _jsonldResolver?: JSONLDResolver; + private _settings: Settings; + public static defaultHandlers = defaultHandlers; + + constructor(settings?: Partial, data) { // Store settings and data this._settings = settings = { ...settings }; this._data = data = { ...data }; @@ -27,8 +33,8 @@ export default class PathFactory { const contextProvider = new ContextProvider(settings.context); resolvers.push(new ComplexPathResolver(contextProvider)); resolvers.push(this._jsonldResolver = new JSONLDResolver(contextProvider)); - settings.parsedContext = new ContextParser().parse(settings.context) - .then(({ contextRaw }) => contextRaw); + // TODO: See why this isn't dealy with by the context provider + settings.parsedContext = new ContextParser().parse(settings.context).then(context => context.getContextRaw()); } else { settings.context = settings.parsedContext = {}; @@ -69,13 +75,12 @@ export default class PathFactory { return this._pathProxy.createPath({ ...this._settings, ...settings }, _data); } } -PathFactory.defaultHandlers = defaultHandlers; /** * Converts a handler function into a handler object. */ -export function toHandler(handle) { - return typeof handle.handle === 'function' ? handle : { handle }; +export function toHandler(handle: Handler | HandlerFunction): Handler { + return typeof handle.handle === 'function' ? handle : handler(handle); } /** diff --git a/src/PathProxy.js b/src/PathProxy.ts similarity index 85% rename from src/PathProxy.js rename to src/PathProxy.ts index 6cf8dd2b..8f2ec79f 100644 --- a/src/PathProxy.js +++ b/src/PathProxy.ts @@ -1,3 +1,5 @@ +import { Handlers, PathData, Resolvers, Settings } from "./types"; + const EMPTY = Object.create(null); /** @@ -22,6 +24,9 @@ const EMPTY = Object.create(null); * - extendPath, a method to create a child path with this path as parent */ export default class PathProxy { + private _handlers: Handlers; + private _resolvers: Resolvers; + constructor({ handlers = EMPTY, resolvers = [] } = {}) { this._handlers = handlers; this._resolvers = resolvers; @@ -30,7 +35,7 @@ export default class PathProxy { /** * Creates a path Proxy with the given settings and internal data fields. */ - createPath(settings = {}, data) { + createPath(settings: Settings = {}, data): PathData { // The settings parameter is optional if (data === undefined) [data, settings] = [settings, {}]; @@ -48,7 +53,7 @@ export default class PathProxy { // Add an extendPath method to create child paths if (!path.extendPath) { const pathProxy = this; - path.extendPath = function extendPath(newData, parent = this) { + path.extendPath = function extendPath(newData: PathData, parent = this) { return pathProxy.createPath(settings, { parent, extendPath, ...newData }); }; } @@ -60,7 +65,7 @@ export default class PathProxy { /** * Handles access to a property */ - get(pathData, property) { + get(pathData: PathData, property: string) { // Handlers provide functionality for a specific property, // so check if we find a handler first const handler = this._handlers[property]; @@ -69,6 +74,9 @@ export default class PathProxy { // Resolvers provide functionality for arbitrary properties, // so find a resolver that can handle this property + return this._resolvers.find(resolver => resolver.supports(property)) + ?.resolve(property, pathData, pathData.proxy) + for (const resolver of this._resolvers) { if (resolver.supports(property)) return resolver.resolve(property, pathData, pathData.proxy); diff --git a/src/PredicateHandler.js b/src/PredicateHandler.ts similarity index 54% rename from src/PredicateHandler.js rename to src/PredicateHandler.ts index 9e937fa7..22b64723 100644 --- a/src/PredicateHandler.js +++ b/src/PredicateHandler.ts @@ -1,13 +1,21 @@ +import { Handler } from "./types"; + /** * Returns a new path starting from the predicate of the current path. * * Requires: * - (optional) a predicate property on the path data */ -export default class PredicateHandler { +export default class PredicateHandler implements Handler { handle(pathData) { const { predicate } = pathData; return !predicate ? undefined : Promise.resolve(predicate) .then(subject => pathData.extendPath({ subject }, null)); + + + // TODO: See if we can make it this + // const { predicate } = pathData; + // if (!predicate) return undefined; + // return pathData.extendPath({ subject: await predicate }, null); } } diff --git a/src/PredicatesHandler.js b/src/PredicatesHandler.ts similarity index 64% rename from src/PredicatesHandler.js rename to src/PredicatesHandler.ts index d660dc21..e18833cf 100644 --- a/src/PredicatesHandler.js +++ b/src/PredicatesHandler.ts @@ -1,8 +1,10 @@ +import { Handler, PathData } from "./types"; + /** * Queries for all predicates of a path subject */ -export default class PredicatesHandler { - handle(pathData) { +export default class PredicatesHandler implements Handler { + handle(pathData: PathData) { return pathData.extendPath({ distinct: true, select: '?predicate', diff --git a/src/PreloadHandler.js b/src/PreloadHandler.ts similarity index 92% rename from src/PreloadHandler.js rename to src/PreloadHandler.ts index bb8a3b92..623441c4 100644 --- a/src/PreloadHandler.js +++ b/src/PreloadHandler.ts @@ -1,3 +1,5 @@ +import { Handler, PathData } from "./types"; + const VARIABLE = /(SELECT\s+)(\?\S+)/; const QUERY_TAIL = /\}[^}]*$/; @@ -12,7 +14,7 @@ const QUERY_TAIL = /\}[^}]*$/; * Creates: * - a resultsCache property on the path data */ -export default class PreloadHandler { +export default class PreloadHandler implements Handler { /** * Creates a preload function. */ @@ -35,11 +37,11 @@ export default class PreloadHandler { * Creates a cache for the results of * resolving the given predicates against the path. */ - async createResultsCache(predicates, pathData, path) { + async createResultsCache(predicates, pathData: PathData, path) { // Execute the preloading query const { query, vars, resultVar } = await this.createQuery(predicates, path); const { settings: { queryEngine } } = pathData; - const bindings = queryEngine.execute(query); + const bindings = await queryEngine.queryBindings(query); // Extract all results and their preloaded property values const resultsCache = {}; @@ -73,7 +75,7 @@ export default class PreloadHandler { /** * Creates the query for preloading the given predicates on the path */ - async createQuery(predicates, path) { + async createQuery(predicates, path: PathData['proxy']) { // Obtain the query for the current path, and its main variable const parentQuery = await path.sparql; const variableMatch = VARIABLE.exec(parentQuery); diff --git a/src/PropertiesHandler.js b/src/PropertiesHandler.ts similarity index 73% rename from src/PropertiesHandler.js rename to src/PropertiesHandler.ts index 37612e91..326481c4 100644 --- a/src/PropertiesHandler.js +++ b/src/PropertiesHandler.ts @@ -3,14 +3,16 @@ */ import { JsonLdContextNormalized } from 'jsonld-context-parser'; import { toIterablePromise } from './promiseUtils'; +import { Handler } from './types'; -export default class PropertiesHandler { +export default class PropertiesHandler implements Handler { handle(pathData, path) { return toIterablePromise(this._handle(pathData, path)); } async* _handle(pathData, path) { const contextRaw = (await pathData.settings.parsedContext) || {}; + // TODO: See if we can just use the normalized context. This seems like unecessarily repeated behvaior const context = new JsonLdContextNormalized(contextRaw); for await (const predicate of path.predicates) yield context.compactIri(`${await predicate}`, true); diff --git a/src/ReplaceFunctionHandler.js b/src/ReplaceFunctionHandler.ts similarity index 59% rename from src/ReplaceFunctionHandler.js rename to src/ReplaceFunctionHandler.ts index b0afaee2..d115bfd9 100644 --- a/src/ReplaceFunctionHandler.js +++ b/src/ReplaceFunctionHandler.ts @@ -1,3 +1,5 @@ +import { Handler, PathData } from "./types"; + /** * Returns a function that deletes the given value * for the path, and then adds the given values to the path. @@ -6,8 +8,8 @@ * - a delete function on the path proxy. * - an add function on the path proxy. */ -export default class ReplaceFunctionHandler { - handle(pathData, path) { +export default class ReplaceFunctionHandler implements Handler { + handle(pathData: PathData, path) { return function (oldValue, ...newValues) { if (!oldValue || !newValues.length) throw new Error('Replacing values requires at least two arguments, old value followed by all new values'); @@ -15,3 +17,7 @@ export default class ReplaceFunctionHandler { }; } } + +// TODO: Implement the concept of a "dependent" set of handlers. +// in the case of this handler this means that the 'ADD' and 'DELETE' +// handlers would be the dependent handlers diff --git a/src/SetFunctionHandler.js b/src/SetFunctionHandler.ts similarity index 93% rename from src/SetFunctionHandler.js rename to src/SetFunctionHandler.ts index b618809b..d6b64c19 100644 --- a/src/SetFunctionHandler.js +++ b/src/SetFunctionHandler.ts @@ -1,4 +1,5 @@ import MutationFunctionHandler from './MutationFunctionHandler'; +import { Handler } from './types'; import { hasPlainObjectArgs } from './valueUtils'; /** @@ -9,7 +10,7 @@ import { hasPlainObjectArgs } from './valueUtils'; * - a delete function on the path proxy. * - an add function on the path proxy. */ -export default class SetFunctionHandler extends MutationFunctionHandler { +export default class SetFunctionHandler extends MutationFunctionHandler implements Handler { handle(pathData, path) { return (...args) => { // First, delete all existing values for the property/properties diff --git a/src/SortHandler.js b/src/SortHandler.ts similarity index 63% rename from src/SortHandler.js rename to src/SortHandler.ts index 2da8314e..2728f59e 100644 --- a/src/SortHandler.js +++ b/src/SortHandler.ts @@ -1,3 +1,5 @@ +import { Handler, PathData } from "./types"; + /** * Returns a function that creates a new path with the same values, * but sorted on the given property. @@ -7,13 +9,11 @@ * - a predicate on the path proxy * - a sort function on the path proxy (for multi-property sorting) */ -export default class SortHandler { - constructor(order = 'ASC') { - this.order = order; - } +export default class SortHandler implements Handler { + constructor(private order: 'ASC' | 'DESC' = 'ASC') {} - handle(pathData, pathProxy) { - return (...properties) => { + handle(pathData: PathData, pathProxy: PathData['proxy']) { + return (...properties: string[]) => { // Do nothing if no sort properties were given if (properties.length === 0) return pathProxy; @@ -23,8 +23,8 @@ export default class SortHandler { const { predicate } = pathProxy[property]; // Sort on the first property, and create paths for the next one - const childData = { property, predicate, sort: this.order }; - const childPath = pathData.extendPath(childData); + // const childData = { property, predicate, sort: this.order }; + const childPath = pathData.extendPath({ property, predicate, sort: this.order }); return rest.length === 0 ? childPath : childPath.sort(...rest); }; } diff --git a/src/SparqlHandler.js b/src/SparqlHandler.ts similarity index 92% rename from src/SparqlHandler.js rename to src/SparqlHandler.ts index 01f215d6..af90fc1c 100644 --- a/src/SparqlHandler.js +++ b/src/SparqlHandler.ts @@ -1,8 +1,10 @@ import { namedNode } from '@rdfjs/data-model'; +import * as RDF from '@rdfjs/types'; +import { Handler } from './types'; const NEEDS_ESCAPE = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/, ESCAPE_ALL = /["\\\t\n\r\b\f\u0000-\u0019]|[\ud800-\udbff][\udc00-\udfff]/g, - ESCAPED_CHARS = { + ESCAPED_CHARS: Record = { '\\': '\\\\', '"': '\\"', '\t': '\\t', '\n': '\\n', '\r': '\\r', '\b': '\\b', '\f': '\\f', }; @@ -13,7 +15,7 @@ const NEEDS_ESCAPE = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/, * Requires: * - a mutationExpressions or pathExpression property on the path proxy */ -export default class SparqlHandler { +export default class SparqlHandler implements Handler { async handle(pathData, path) { // First check if we have a mutation expression const mutationExpressions = await path.mutationExpressions; @@ -135,9 +137,9 @@ export default class SparqlHandler { } // Creates a unique query variable within the given scope, based on the suggestion - createVar(suggestion = '', scope) { + createVar(suggestion = '', scope?: Record) { let counter = 0; - let label = `?${suggestion.match(/[a-z0-9]*$/i)[0] || 'result'}`; + let label = `?${suggestion.match(/[a-z0-9]*$/i)?.[0] ?? 'result'}`; if (scope) { suggestion = label; while (scope[label]) @@ -148,7 +150,7 @@ export default class SparqlHandler { } // Converts an RDFJS term to a string that we can use in a query - termToString(term) { + termToString(term: RDF.Term) { // Determine escaped value let { value } = term; if (NEEDS_ESCAPE.test(value)) @@ -176,7 +178,7 @@ export default class SparqlHandler { } // Creates triple patterns for the given subject, predicate, and objects - triplePatterns(subjectString, predicateTerm, objectStrings, reverse = false) { + triplePatterns(subjectString: string, predicateTerm: RDF.Term, objectStrings: string[], reverse = false) { let subjectStrings = [subjectString]; if (reverse) [subjectStrings, objectStrings] = [objectStrings, subjectStrings]; @@ -188,9 +190,9 @@ export default class SparqlHandler { // Replaces a character by its escaped version // (borrowed from https://www.npmjs.com/package/n3) -function escapeCharacter(character) { +function escapeCharacter(character: string) { // Replace a single character by its escaped version - let result = ESCAPED_CHARS[character]; + let result: string | undefined = ESCAPED_CHARS[character]; if (result === undefined) { // Replace a single character with its 4-bit unicode escape sequence if (character.length === 1) { @@ -209,7 +211,7 @@ function escapeCharacter(character) { // Skolemizes the given term if it is a blank node let skolemId = 0; -function skolemize(term) { +function skolemize(term: RDF.Term) { if (term.termType !== 'BlankNode') return term; if (!term.skolemized) diff --git a/src/StringToLDflexHandler.js b/src/StringToLDflexHandler.ts similarity index 91% rename from src/StringToLDflexHandler.js rename to src/StringToLDflexHandler.ts index 485391a3..015e3904 100644 --- a/src/StringToLDflexHandler.js +++ b/src/StringToLDflexHandler.ts @@ -1,7 +1,9 @@ +import { Handler } from "./types"; + /** * Yields a function that interprets a string expression as an LDflex path. */ -export default class StringToLDflexHandler { +export default class StringToLDflexHandler implements Handler { handle(pathData, path) { // Resolves the given string expression against the LDflex object return (expression = '', ldflex = path) => { diff --git a/src/SubjectHandler.js b/src/SubjectHandler.ts similarity index 81% rename from src/SubjectHandler.js rename to src/SubjectHandler.ts index 4a26d258..d3de8fcf 100644 --- a/src/SubjectHandler.js +++ b/src/SubjectHandler.ts @@ -1,3 +1,5 @@ +import { Handler, PathData } from "./types"; + /** * Returns a new path starting from the subject of the current path. * @@ -5,8 +7,8 @@ * - (optional) a subject property on the path data * - (optional) a parent property on the path data */ -export default class SubjectHandler { - handle(pathData) { +export default class SubjectHandler implements Handler { + handle(pathData: PathData) { // Traverse parents until we find a subject let { subject, parent } = pathData; while (!subject && parent) diff --git a/src/SubjectsHandler.js b/src/SubjectsHandler.js deleted file mode 100644 index ed0899ca..00000000 --- a/src/SubjectsHandler.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Queries for all subjects of a document - */ -export default class SubjectsHandler { - handle(pathData) { - return pathData.extendPath({ - distinct: true, - select: '?subject', - finalClause: () => '?subject ?predicate ?object.', - property: pathData.property, - }); - } -} diff --git a/src/SubjectsHandler.ts b/src/SubjectsHandler.ts new file mode 100644 index 00000000..52675eae --- /dev/null +++ b/src/SubjectsHandler.ts @@ -0,0 +1,25 @@ +import { Handler } from "./types"; +import { Factory } from "sparqlalgebrajs"; +import { variable } from "@rdfjs/data-model"; +const { createDistinct, createProject, createBgp, createPattern } = new Factory(); + +createDistinct( + createProject( + createBgp([ createPattern(variable('s'), variable('p'), variable('o')) ]), + [ variable('s') ] + ) +) + +/** + * Queries for all subjects of a document + */ +export default class SubjectsHandler implements Handler { + handle(pathData) { + return pathData.extendPath({ + distinct: true, + select: '?subject', + finalClause: () => '?subject ?predicate ?object.', + property: pathData.property, + }); + } +} diff --git a/src/ThenHandler.js b/src/ThenHandler.ts similarity index 90% rename from src/ThenHandler.js rename to src/ThenHandler.ts index dc82d20f..515a1b64 100644 --- a/src/ThenHandler.js +++ b/src/ThenHandler.ts @@ -1,5 +1,6 @@ import { getThen } from './promiseUtils'; import { getFirstItem } from './iterableUtils'; +import { Handler } from './types'; /** * Thenable handler that resolves to either the subject @@ -10,7 +11,7 @@ import { getFirstItem } from './iterableUtils'; * - (optional) a subject on the path proxy * - (optional) results on the path proxy */ -export default class ThenHandler { +export default class ThenHandler implements Handler { handle({ subject }, pathProxy) { // Resolve to either the subject (zero-length path) or the first result return subject ? diff --git a/src/ToArrayHandler.js b/src/ToArrayHandler.ts similarity index 80% rename from src/ToArrayHandler.js rename to src/ToArrayHandler.ts index 9ed03c77..1764eb4b 100644 --- a/src/ToArrayHandler.js +++ b/src/ToArrayHandler.ts @@ -1,3 +1,4 @@ +import { Handler, PathData } from './types'; import { isAsyncIterable } from './valueUtils'; /** @@ -6,8 +7,8 @@ import { isAsyncIterable } from './valueUtils'; * Requires: * - (optional) an iterable path */ -export default class ToArrayHandler { - handle(pathData, path) { +export default class ToArrayHandler implements Handler { + handle(pathData: PathData, path) { return async map => { const items = []; if (isAsyncIterable(path)) { diff --git a/src/URIHandler.js b/src/URIHandler.ts similarity index 88% rename from src/URIHandler.js rename to src/URIHandler.ts index 7a6943a2..4009864a 100644 --- a/src/URIHandler.js +++ b/src/URIHandler.ts @@ -1,10 +1,11 @@ import { handler } from './handlerUtil'; +import { Term } from '@rdfjs/types' /** * Finds the index at which the break between the namespace and the * occurs - then execute a callback with this index as the second arg */ -function breakIndex(term, cb) { +function breakIndex(term: Term | undefined, cb: (str: string, i: number) => string): string | undefined { if (term?.termType !== 'NamedNode') return undefined; // Find the index of the last '#' or '/' if no '#' exists diff --git a/src/defaultHandlers.js b/src/defaultHandlers.ts similarity index 90% rename from src/defaultHandlers.js rename to src/defaultHandlers.ts index 47d98e24..5e67e9ea 100644 --- a/src/defaultHandlers.js +++ b/src/defaultHandlers.ts @@ -23,6 +23,7 @@ import ToArrayHandler from './ToArrayHandler'; import { termToPrimitive } from './valueUtils'; import { handler } from './handlerUtil'; import { prefixHandler, namespaceHandler, fragmentHandler } from './URIHandler'; +import { Handler } from './types'; /** * A map with default property handlers. @@ -90,17 +91,20 @@ export default { }; // Creates a handler for the given RDF/JS Term property -function termPropertyHandler(property) { +function termPropertyHandler(property: string): Handler { // If a resolved subject is present, // behave as an RDF/JS term and synchronously expose the property; // otherwise, return a promise to the property value - return handler(({ subject }, path) => - subject && (property in subject) ? subject[property] : - path.then && path.then(term => term?.[property])); + return handler(({ subject }, path) => subject?.[property] ?? path.then?.(term => term?.[property])) + + // TODO: Delete this + // return handler(({ subject }, path) => + // subject && (property in subject) ? subject[property] : + // path.then && path.then(term => term?.[property])); } // Creates a handler that converts the subject into a primitive -function subjectToPrimitiveHandler() { +function subjectToPrimitiveHandler(): Handler { return handler(({ subject }) => () => typeof subject?.termType !== 'string' ? undefined : termToPrimitive(subject)); diff --git a/src/handlerUtil.js b/src/handlerUtil.js deleted file mode 100644 index a0cbb33e..00000000 --- a/src/handlerUtil.js +++ /dev/null @@ -1,4 +0,0 @@ -// Creates a handler from the given function -export function handler(handle) { - return { handle }; -} diff --git a/src/handlerUtil.ts b/src/handlerUtil.ts new file mode 100644 index 00000000..90a76a71 --- /dev/null +++ b/src/handlerUtil.ts @@ -0,0 +1,6 @@ +import { Handler, HandlerFunction } from "./types"; + +// Creates a handler from the given function +export function handler(handle: T): Handler { + return { handle }; +} diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/iterableUtils.js b/src/iterableUtils.js deleted file mode 100644 index 6812f713..00000000 --- a/src/iterableUtils.js +++ /dev/null @@ -1,35 +0,0 @@ -const done = {}; - -/** - * Returns the elements of the iterable as an array - */ -export async function iterableToArray(iterable) { - const items = []; - for await (const item of iterable) - items.push(item); - return items; -} - -/** - * Gets the first element of the iterable. - */ -export function getFirstItem(iterable) { - const iterator = iterable[Symbol.asyncIterator](); - return iterator.next().then(item => item.value); -} - -/** - * Creates an async iterator with the item as only element. - */ -export function iteratorFor(item) { - return { - async next() { - if (item !== done) { - const value = await item; - item = done; - return { value }; - } - return { done: true }; - }, - }; -} diff --git a/src/iterableUtils.ts b/src/iterableUtils.ts new file mode 100644 index 00000000..99cc191e --- /dev/null +++ b/src/iterableUtils.ts @@ -0,0 +1,64 @@ +import { ResultStream } from '@rdfjs/types'; + +/** + * Returns the elements of the iterable as an array + */ +export async function iterableToArray(iterable: AsyncIterable): Promise { + const items: T[] = []; + for await (const item of iterable) + items.push(item); + return items; +} + +/** + * Gets the first element of the iterable. + */ +export async function getFirstItem(iterable: AsyncIterable): Promise { + for await (const item of iterable) + return item; + throw new Error('Expected iterable to contain at least one element'); +} + +/** + * Creates an async iterator with the item as only element. + */ +export async function *iteratorFor(item: T): AsyncIterator { + return item +} + +/** + * Transforms the readable into an asynchronously iterable object + * From: https://github.com/LDflex/LDflex-Comunica/blob/cf3b74013fda96063b5edbffd14fa7214ad119a0/src/ComunicaEngine.ts#L161 + */ +export async function *streamToAsyncIterable(readable: ResultStream): AsyncIterableIterator { + let item: T | null; + let error: Error | undefined; + let done = false; + let cb = () => {}; + + function settlePromise() { + cb(); + } + + function finish(error?: Error) { + done = true; + error = error; + } + + readable.on('readable', settlePromise); + readable.on('error', finish); + readable.on('end', finish); + + while (!done) { + while ((item = readable.read()) !== null) + yield item; + await new Promise(res => { cb = res }); + } + + readable.removeListener('readable', settlePromise); + readable.removeListener('error', finish); + readable.removeListener('end', finish); + + if (error) + throw error; +} diff --git a/src/promiseUtils.js b/src/promiseUtils.ts similarity index 97% rename from src/promiseUtils.js rename to src/promiseUtils.ts index 3fb6ee48..52f19933 100644 --- a/src/promiseUtils.js +++ b/src/promiseUtils.ts @@ -45,7 +45,7 @@ export function toIterablePromise(iterable) { * Returns a memoized version of the iterable * that can be iterated over as many times as needed. */ -export function memoizeIterable(iterable) { +export function memoizeIterable(iterable) { const cache = []; let iterator = iterable[Symbol.asyncIterator](); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..789a3f35 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,91 @@ +import type { Bindings, Term, StringSparqlQueryable, BindingsResultSupport } from '@rdfjs/types'; +import { JsonLdContext, IJsonLdContextNormalizedRaw } from "jsonld-context-parser"; + +export type MaybePromise = T | Promise + +export type HandlerFunction = (pathData: PathData, path?: PathData['proxy']) => any; +export type Handler = { handle: T }; +export type Handlers = Record; + +export interface Resolver { + supports(...args: any[]): boolean; + resolve(property: string, pathData: PathData, proxy: PathData['proxy']): PathData; +} + +export type Resolvers = Resolver[]; + +export interface Settings { + context: JsonLdContext, + handlers?: Handlers, + parsedContext?: MaybePromise, + resolvers?: Resolver[], + queryEngine: StringSparqlQueryable; +} + +export interface PathData { + settings: Settings; + subject: Term; + // resultsCache?: MaybePromise>; + // extendPath: HandlerFunction; // TODO: Check this + extendPath(pathData: PathData, path?: PathData): PathData; // TODO: Check this + proxy: any; + apply: any; + parent?: PathData; +} +// * - settings, an object that is passed on as-is to child paths +// * - proxy, a reference to the proxied object the user sees +// * - parent, a reference to the parent path +// * - apply, a function the will be invoked when the path is called as a function +// * - extendPath, a method to create a child path with this path as parent + +// export interface Data { +// property: string | undefined; +// resultsCache?: Data; // TODO: Check - is this optional, +// results: Data; // TODO CHECK +// parent?: null, +// subject?: RDF.Quad_Subject, +// sparql?: string, +// predicate?: RDF.Quad_Predicate, +// proxy?: LDflexProxyHandlers, // TODO: Check +// settings: LDflexSettings, +// // TODO: Check below definition +// extendPath(pathData: Data, path?: Data): Data, +// [Symbol.asyncIterator]?: AsyncIterableIterator, // TODO: CHECK - is this optional +// //[x: string]: any // TODO FIX - make better for path property access +// finalClause?(v: string): [string, string, string] // Generates the final clause for a sparql query +// [x: string]: any; // TODO make this stricter? +// } + + + + +// export type MaybePromise = Promise | T +// export type MaybeFunction = T | (() => T) +// export type MaybeArray = T | T[] +// export type WithOptional = Omit & Partial>; + +// export type LDflexHandleFunction = (pathData: any, path: any) => any; + +// export interface LDflexHandler { +// handle : LDflexHandleFunction +// } + +// export type LDflexProxyHandlers = { +// readonly [x: string]: LDflexHandler; +// readonly [Symbol.asyncIterator]: AsyncIterableIterator; +// } | { +// readonly __esModule: () => undefined; +// }; + + +// export enum fltr { +// regex = 'regex', +// le = '<', +// ge = '>', +// leq = '<=', +// geq = '>=', +// exists = 'exists', +// notExists = 'notexists', +// eq = '=', +// neq = '!=' +// } diff --git a/src/valueUtils.js b/src/valueUtils.ts similarity index 87% rename from src/valueUtils.js rename to src/valueUtils.ts index ac08fbb6..74ea4540 100644 --- a/src/valueUtils.js +++ b/src/valueUtils.ts @@ -1,4 +1,5 @@ import { namedNode, literal } from '@rdfjs/data-model'; +import * as RDF from '@rdfjs/types'; const xsd = 'http://www.w3.org/2001/XMLSchema#'; @@ -28,7 +29,7 @@ const xsdPrimitives = { }; // Checks whether the value is asynchronously iterable -export function isAsyncIterable(value) { +export function isAsyncIterable(value: any): value is AsyncIterable { return value && typeof value[Symbol.asyncIterator] === 'function'; } @@ -65,12 +66,12 @@ export function ensureArray(value) { } // Joins the arrays into a single array -export function joinArrays(arrays) { - return [].concat(...arrays); +export function joinArrays(arrays: T[][]): T[] { + return ([] as T[]).concat(...arrays); } // Ensures the value is an RDF/JS term -export function valueToTerm(value) { +export function valueToTerm(value: string | boolean | number | Date | RDF.Term): RDF.Term { switch (typeof value) { // strings case 'string': @@ -95,12 +96,12 @@ export function valueToTerm(value) { // other objects default: if (value) { - // RDF/JS Term - if (typeof value.termType === 'string') - return value; // Date if (value instanceof Date) return literal(value.toISOString(), xsdDateTimeTerm); + // RDF/JS Term + if (typeof value.termType === 'string') + return value; } } @@ -109,7 +110,9 @@ export function valueToTerm(value) { } // Converts the term into a primitive value -export function termToPrimitive(term) { +export function termToPrimitive(term: RDF.Literal): string | boolean | number | Date; +export function termToPrimitive(term: Exclude): string; +export function termToPrimitive(term: RDF.Term): string | boolean | number | Date { const { termType, value } = term; // Some literals convert into specific primitive values diff --git a/test.ts b/test.ts new file mode 100644 index 00000000..b58fa479 --- /dev/null +++ b/test.ts @@ -0,0 +1,11 @@ +import { Parser } from 'sparqljs'; +const parser = new Parser({ + +}) + +const x = parser.parse(` +PREFIX ex: + +SELECT * WHERE { ?s ex:a/ex:b ?o } +`) +console.log(x) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..56794816 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,101 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Projects */ + // "incremental": true, /* Enable incremental compilation */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ + // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "resolveJsonModule": true, /* Enable importing .json files */ + // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ + // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}