diff --git a/package-lock.json b/package-lock.json index 6884301..dd9930e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@comake/skl-js-engine", - "version": "0.19.0", + "version": "0.20.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@comake/skl-js-engine", - "version": "0.19.0", + "version": "0.20.0", "license": "BSD-4-Clause", "dependencies": { "@comake/openapi-operation-executor": "^0.11.1", diff --git a/package.json b/package.json index de26a05..1a1fb96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@comake/skl-js-engine", - "version": "0.19.0", + "version": "0.20.0", "description": "Standard Knowledge Language Javascript Engine", "keywords": [ "skl", diff --git a/src/storage/FindOptionsTypes.ts b/src/storage/FindOptionsTypes.ts index c99fc07..43ac687 100644 --- a/src/storage/FindOptionsTypes.ts +++ b/src/storage/FindOptionsTypes.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import type { Variable } from 'sparqljs'; import type { OrArray, JSONArray, JSONObject } from '../util/Types'; import type { FindOperator } from './FindOperator'; import type { InverseRelationOperatorValue } from './operator/InverseRelation'; @@ -12,6 +13,8 @@ export interface FindOneOptions { relations?: FindOptionsRelations; order?: FindOptionsOrder; skipFraming?: boolean; + group?: Variable; + entitySelectVariable?: Variable; } export type FindOptionsRelationsValue = | diff --git a/src/storage/query-adapter/sparql/SparqlQueryAdapter.ts b/src/storage/query-adapter/sparql/SparqlQueryAdapter.ts index 7d4a133..6d479b4 100644 --- a/src/storage/query-adapter/sparql/SparqlQueryAdapter.ts +++ b/src/storage/query-adapter/sparql/SparqlQueryAdapter.ts @@ -128,10 +128,11 @@ export class SparqlQueryAdapter implements QueryAdapter { const queryData = queryBuilder.buildEntitySelectPatternsFromOptions(entityVariable, options); const entitySelectQuery = queryData.where.length > 0 ? createSparqlSelectQuery( - entityVariable, + options?.entitySelectVariable ?? entityVariable, queryData.where, queryData.orders, - queryData.group, + // FIXME: This will not work if queryData.group is defined, figure out what can make it defined. + queryData.group ?? options?.group, options?.limit, options?.offset, ) diff --git a/src/storage/query-adapter/sparql/SparqlQueryBuilder.ts b/src/storage/query-adapter/sparql/SparqlQueryBuilder.ts index 38f8c76..d5a51aa 100644 --- a/src/storage/query-adapter/sparql/SparqlQueryBuilder.ts +++ b/src/storage/query-adapter/sparql/SparqlQueryBuilder.ts @@ -681,7 +681,7 @@ export class SparqlQueryBuilder { return createSparqlNotEqualOperation(leftSide, rightSide as Expression); } - private resolveValueToTerm(value: FieldPrimitiveValue | ValueWhereFieldObject): NamedNode | Literal { + private resolveValueToTerm(value: FieldPrimitiveValue | ValueWhereFieldObject): NamedNode | Literal | Variable { if (typeof value === 'object' && '@value' in value) { return valueToLiteral( (value as ValueWhereFieldObject)['@value'], diff --git a/src/util/TripleUtil.ts b/src/util/TripleUtil.ts index 09e7a7e..0cba7d3 100644 --- a/src/util/TripleUtil.ts +++ b/src/util/TripleUtil.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import DataFactory from '@rdfjs/data-model'; -import type { Quad, Quad_Object, Quad_Subject, Literal } from '@rdfjs/types'; +import type { Quad, Quad_Object, Quad_Subject, Literal, Variable } from '@rdfjs/types'; import * as jsonld from 'jsonld'; import type { ContextDefinition, GraphObject, NodeObject, ValueObject } from 'jsonld'; import type { Frame } from 'jsonld/jsonld-spec'; @@ -254,7 +254,7 @@ export async function triplesToJsonldWithFrame( export function valueToLiteral( value: string | boolean | number | Date | JSONObject | JSONArray, datatype?: string, -): Literal { +): Literal | Variable { if (datatype) { if (datatype === '@json' || datatype === RDF.JSON) { return DataFactory.literal(JSON.stringify(value), RDF.JSON); @@ -270,6 +270,9 @@ export function valueToLiteral( if (typeof value === 'boolean') { return DataFactory.literal(value.toString(), XSD.boolean); } + if (typeof value === 'string' && value.startsWith('?') && value.length > 1) { + return DataFactory.variable(value.slice(1)); + } if (value instanceof Date) { return DataFactory.literal(value.toISOString(), XSD.dateTime); } diff --git a/test/unit/storage/query-adapter/sparql/SparqlQueryAdapter.test.ts b/test/unit/storage/query-adapter/sparql/SparqlQueryAdapter.test.ts index ec062d1..6d5e549 100644 --- a/test/unit/storage/query-adapter/sparql/SparqlQueryAdapter.test.ts +++ b/test/unit/storage/query-adapter/sparql/SparqlQueryAdapter.test.ts @@ -701,6 +701,39 @@ describe('a SparqlQueryAdapter', (): void => { '}', ]); }); + + it('executes a group by query with custom entity select variable.', async(): Promise => { + await adapter.findAll({ + where: { + type: 'https://schema.org/Place', + 'https://standardknowledge.com/ontologies/core/deduplicationGroup': '?deduplicationGroup' + }, + group: DataFactory.variable('deduplicationGroup'), + entitySelectVariable: { + variable: DataFactory.variable('entity'), + expression: { + type: 'aggregate', + aggregation: 'MIN', + expression: DataFactory.variable('entity'), + }, + }, + }); + expect(select).toHaveBeenCalledTimes(1); + expect(select.mock.calls[0][0].split('\n')).toEqual([ + 'CONSTRUCT { ?subject ?predicate ?object. }', + 'WHERE {', + ' {', + ' SELECT DISTINCT (MIN(?entity) AS ?entity) WHERE {', + ' ?entity (/(*)) ;', + ' ?deduplicationGroup.', + ' FILTER(EXISTS { GRAPH ?entity { ?entity ?c1 ?c2. } })', + ' }', + ' GROUP BY ?deduplicationGroup', + ' }', + ' GRAPH ?entity { ?subject ?predicate ?object. }', + '}', + ]); + }); }); describe('findAllBy', (): void => {