diff --git a/src/JSONLDResolver.js b/src/JSONLDResolver.js index 9543c5f4..b9818887 100644 --- a/src/JSONLDResolver.js +++ b/src/JSONLDResolver.js @@ -31,7 +31,23 @@ export default class JSONLDResolver { const reverse = lazyThenable(() => this._context.then(context => context[property] && context[property]['@reverse'])); const resultsCache = this.getResultsCache(pathData, predicate, reverse); - return pathData.extendPath({ property, predicate, resultsCache, reverse }); + return pathData.extendPath({ property, predicate, resultsCache, reverse, asFunction: this.apply }); + } + + /** + * Function for fixing the object values of the predicate. + * Extends the proxy with the new values. + */ + apply(pathData, args) { + if (!args || args.length === 0) + throw new Error('At least 1 argument is required'); + if (args.some(arg => !arg.termType)) + throw new Error('Args need to be RDF objects'); + pathData.values = args; + // Prevent children from inheriting this apply function + delete pathData.asFunction; + // No extendPath call since we don't want to add a chain so return original proxy + return pathData.proxy; } /** diff --git a/src/PathExpressionHandler.js b/src/PathExpressionHandler.js index ea423a87..b664b07f 100644 --- a/src/PathExpressionHandler.js +++ b/src/PathExpressionHandler.js @@ -14,6 +14,7 @@ export default class PathExpressionHandler { predicate: await current.predicate, reverse: await current.reverse, sort: current.sort, + values: current.values, }); } // Move to parent link diff --git a/src/PathProxy.js b/src/PathProxy.js index eec068b4..c1ae85ed 100644 --- a/src/PathProxy.js +++ b/src/PathProxy.js @@ -35,7 +35,10 @@ export default class PathProxy { [data, settings] = [settings, {}]; // Create the path's internal data object and the proxy that wraps it - const path = { settings, ...data }; + // This needs to be a function or `apply` can't be proxied + // eslint-disable-next-line no-empty-function + function path() { } + Object.assign(path, { settings, ...data }); const proxy = path.proxy = new Proxy(path, this); // Add an extendPath method to create child paths @@ -66,8 +69,18 @@ export default class PathProxy { if (resolver.supports(property)) return resolver.resolve(property, pathData, pathData.proxy); } - // Otherwise, the property does not exist return undefined; } + + /** + * Handles calling the proxy as a function + */ + apply(pathData, thisArg, args) { + if (pathData.asFunction && typeof pathData.asFunction === 'function') + return pathData.asFunction(pathData, args); + + // Otherwise, there is no apply handler + return undefined; + } } diff --git a/src/SparqlHandler.js b/src/SparqlHandler.js index 520968a8..29afa6fa 100644 --- a/src/SparqlHandler.js +++ b/src/SparqlHandler.js @@ -100,12 +100,29 @@ export default class SparqlHandler { let object = this.termToString(skolemize(root.subject)); let queryVar = object; const sorts = []; - const clauses = pathExpression.map((segment, index) => { + const clauses = []; + let staticValid = false; + pathExpression.forEach((segment, index) => { // Obtain components and generate triple pattern const subject = object; - const { predicate, reverse, sort } = segment; - object = index < lastIndex ? this.createVar(`v${index}`, scope) : lastVar; - const patttern = this.triplePattern(subject, predicate, object, reverse); + const { predicate, reverse, sort, values } = segment; + // Don't update variable if there are static values instead + if (values && values.length > 0) { + if (!staticValid) + throw new Error('Can not have static objects if the subject is also static'); + // Can't have 2 subsequent static triple sets + staticValid = false; + const valueStrs = values.map(v => this.termToString(v)); + if (reverse) + clauses.push(...valueStrs.map(v => this.triplePattern(v, predicate, subject))); + else + clauses.push(this.triplePattern(subject, predicate, valueStrs.join(', '))); + } + else { + staticValid = true; + object = index < lastIndex ? this.createVar(`v${index}`, scope) : lastVar; + clauses.push(this.triplePattern(subject, predicate, object, reverse)); + } // If the sort option was not set, use this object as a query variable if (!sort) { @@ -118,7 +135,6 @@ export default class SparqlHandler { // TODO: use a descriptive lastVar in case of sorting object = queryVar; } - return patttern; }); return { queryVar, sorts, clauses }; } diff --git a/test/integration/execute-query-test.js b/test/integration/execute-query-test.js index e2edd629..ed2740d7 100644 --- a/test/integration/execute-query-test.js +++ b/test/integration/execute-query-test.js @@ -9,11 +9,13 @@ import SubjectsHandler from '../../src/SubjectsHandler'; import SetFunctionHandler from '../../src/SetFunctionHandler'; import DataHandler from '../../src/DataHandler'; import JSONLDResolver from '../../src/JSONLDResolver'; +import MutationExpressionsHandler from '../../src/MutationExpressionsHandler'; import { createQueryEngine, deindent } from '../util'; import { namedNode, literal } from '@rdfjs/data-model'; import { iterableToArray } from '../../src/iterableUtils'; import context from '../context'; +import ThenHandler from '../../src/ThenHandler'; const subject = namedNode('https://example.org/#me'); const queryEngine = createQueryEngine([ @@ -26,6 +28,8 @@ const resolvers = [ new JSONLDResolver(context), ]; const handlersPath = { + then: new ThenHandler(), + mutationExpressions: new MutationExpressionsHandler(), sparql: new SparqlHandler(), pathExpression: new PathExpressionHandler(), results: new ExecuteQueryHandler(), @@ -38,6 +42,8 @@ const handlersPath = { }; const handlersMutation = { + then: new ThenHandler(), + mutationExpressions: new MutationExpressionsHandler(), sparql: new SparqlHandler(), pathExpression: new PathExpressionHandler(), results: new ExecuteQueryHandler(), @@ -122,6 +128,13 @@ describe('a query path with a path and mutation expression handler', () => { expect(names.map(n => `${n}`)).toEqual(['Alice', 'Bob', 'Carol']); }); + it('returns results for a path with 4 links and fixed values', async () => { + const names = []; + for await (const firstName of person.friends.friends(namedNode('http://ex.org/John')).firstName) + names.push(firstName); + expect(names.map(n => `${n}`)).toEqual(['Alice', 'Bob', 'Carol']); + }); + it('returns true for an addition with 3 links', async () => { expect(await person.friends.firstName.add('Ruben')).toBeTruthy(); }); diff --git a/test/integration/sparql-test.js b/test/integration/sparql-test.js index ecb10858..ff1c95d0 100644 --- a/test/integration/sparql-test.js +++ b/test/integration/sparql-test.js @@ -81,6 +81,27 @@ describe('a query path with a path expression handler', () => { ORDER BY ASC(?v3) ASC(?givenName)`)); }); + it('resolves a path with 3 links and fixed values', async () => { + const query = await person.friends.friends(namedNode('http://ex.org/Alice'), namedNode('http://ex.org/Bob')).name.sparql; + expect(query).toEqual(deindent(` + SELECT ?name WHERE { + ?v0. + ?v0 , . + ?v0 ?name. + }`)); + }); + + it('resolves a path with 3 links and reversed fixed values', async () => { + const query = await person.friends.friendOf(namedNode('http://ex.org/Alice'), namedNode('http://ex.org/Bob')).name.sparql; + expect(query).toEqual(deindent(` + SELECT ?name WHERE { + ?v0. + ?v0. + ?v0. + ?v0 ?name. + }`)); + }); + it('resolves a path with 1 link and a predicates call', async () => { const query = await person.predicates.sparql; expect(query).toEqual(deindent(` @@ -183,7 +204,7 @@ describe('a query path with a path expression handler', () => { it('errors on a path with 3 links and an addition without args', async () => { expect(() => person.friends.friends.add().sparql) - .toThrow(new Error('Mutation on [object Object] can not be invoked without arguments')); + .toThrow(); }); it('resolves a path with 3 links and an addition with a raw arg and path arg', async () => { diff --git a/test/unit/JSONLDResolver-test.js b/test/unit/JSONLDResolver-test.js index 8c078144..d9568e3e 100644 --- a/test/unit/JSONLDResolver-test.js +++ b/test/unit/JSONLDResolver-test.js @@ -60,6 +60,43 @@ describe('a JSONLDResolver instance with a context', () => { }); }); + describe('resolving properties as functions', () => { + const pathData = { extendPath: jest.fn(x => Object.assign(x, { proxy: x })) }; + + let result; + beforeEach(() => result = resolver.resolve('knows', pathData)); + + it('exists', () => { + result = resolver.resolve('knows', pathData); + expect(result.asFunction).toBeInstanceOf(Function); + }); + + it('errors if there are no arguments', () => { + expect(() => result.asFunction(result, [])).toThrowError(); + }); + + it('errors if the input is not an RDF object', () => { + expect(() => result.asFunction(result, ['Ruben'])).toThrowError(); + }); + + describe('with 2 values', () => { + const ruben = namedNode('Ruben'); + const joachim = namedNode('Joachim'); + let applied; + beforeEach(() => { + applied = result.asFunction(result, [ruben, joachim]); + }); + + it('stores the new values in the result', () => { + expect(applied.values).toEqual([ruben, joachim]); + }); + + it('deletes the asFunction property', () => { + expect(applied).not.toHaveProperty('asFunction'); + }); + }); + }); + describe('resolving the foaf:knows property', () => { const extendedPath = {}; const pathData = { extendPath: jest.fn(() => extendedPath) }; diff --git a/test/unit/PathProxy-test.js b/test/unit/PathProxy-test.js index 65acdf3b..21c91adf 100644 --- a/test/unit/PathProxy-test.js +++ b/test/unit/PathProxy-test.js @@ -19,6 +19,12 @@ describe('a PathProxy without handlers or resolvers', () => { expect(path[Symbol('symbol')]).toBeUndefined(); }); }); + + describe('when calling it as a function', () => { + it('throws an error', () => { + expect(path()).toBeUndefined(); + }); + }); }); }); @@ -334,3 +340,24 @@ describe('a PathProxy whose paths are extended', () => { }); }); }); + +describe('a PathProxy with an apply function', () => { + let pathProxy; + beforeAll(() => (pathProxy = new PathProxy())); + + describe('a created path', () => { + let path, f; + beforeAll(() => { + f = jest.fn(x => x); + path = pathProxy.createPath({ asFunction: f }); + }); + + it('calls the apply function', () => { + path('test'); + expect(f).toBeCalledTimes(1); + const args = f.mock.calls[0]; + expect(typeof args[0].asFunction).toBe('function'); + expect(args[1]).toEqual(['test']); + }); + }); +}); diff --git a/test/unit/SparqlHandler-test.js b/test/unit/SparqlHandler-test.js index d9932391..cf6e50e2 100644 --- a/test/unit/SparqlHandler-test.js +++ b/test/unit/SparqlHandler-test.js @@ -184,6 +184,48 @@ describe('a SparqlHandler instance', () => { ?v0 ?p2. }`)); }); + + it('supports fixed values', async () => { + const pathExpression = [ + { subject: namedNode('https://example.org/#me') }, + { predicate: namedNode('https://ex.org/p1') }, + { predicate: namedNode('https://ex.org/p2'), values: [namedNode('https://ex.org/o1'), namedNode('https://ex.org/o2')] }, + ]; + + const pathData = { property: 'p2' }; + expect(await handler.handle(pathData, { pathExpression })).toEqual(deindent(` + SELECT ?v0 WHERE { + ?v0. + ?v0 , . + }`)); + }); + + it('errors on static triples in a query', async () => { + const pathExpression = [ + { subject: namedNode('https://example.org/#me') }, + { predicate: namedNode('https://ex.org/p1'), values: [namedNode('https://ex.org/o1'), namedNode('https://ex.org/o2')] }, + { predicate: namedNode('https://ex.org/p2') }, + ]; + + const pathData = { property: 'p2' }; + await expect(handler.handle(pathData, { pathExpression })).rejects.toEqual(new Error('Can not have static objects if the subject is also static')); + }); + + it('supports reversed fixed values', async () => { + const pathExpression = [ + { subject: namedNode('https://example.org/#me') }, + { predicate: namedNode('https://ex.org/p1') }, + { predicate: namedNode('https://ex.org/p2'), values: [namedNode('https://ex.org/o1'), namedNode('https://ex.org/o2')], reverse: true }, + ]; + + const pathData = { property: 'p2' }; + expect(await handler.handle(pathData, { pathExpression })).toEqual(deindent(` + SELECT ?v0 WHERE { + ?v0. + ?v0. + ?v0. + }`)); + }); }); describe('with mutationExpressions', () => {