Skip to content

Commit

Permalink
Merge pull request #12 from comake/feat/sparql
Browse files Browse the repository at this point in the history
feat/sparql
  • Loading branch information
adlerfaulkner authored Jan 5, 2023
2 parents 18c79ac + 3e88e3c commit 6936ffb
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 75 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@comake/skql-js-engine",
"version": "0.12.0",
"version": "0.13.0",
"description": "Standard Knowledge Query Language Javascript Engine",
"keywords": [
"skl",
Expand Down
16 changes: 8 additions & 8 deletions src/Skql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@ import type {
} from '@comake/openapi-operation-executor';
import { OpenApiOperationExecutor } from '@comake/openapi-operation-executor';
import type { ReferenceNodeObject } from '@comake/rmlmapper-js';
import type { Quad } from '@rdfjs/types';
import axios from 'axios';
import type { AxiosError, AxiosResponse } from 'axios';
import type { ContextDefinition, NodeObject } from 'jsonld';
import type { ContextDefinition, GraphObject, NodeObject } from 'jsonld';
import type { Frame } from 'jsonld/jsonld-spec';
import SHACLValidator from 'rdf-validate-shacl';
import type ValidationReport from 'rdf-validate-shacl/src/validation-report';
import { Mapper } from './mapping/Mapper';
import type { FindAllOptions, FindOneOptions, FindOptionsWhere } from './storage/FindOptionsTypes';
import { MemoryQueryAdapter } from './storage/memory/MemoryQueryAdapter';
import type { MemoryQueryAdapterOptions } from './storage/memory/MemoryQueryAdapterOptions';
import type { EntityOrTArray, QuadOrObject, QueryAdapter } from './storage/QueryAdapter';
import type { QueryAdapter, RawQueryResult } from './storage/QueryAdapter';
import { SparqlQueryAdapter } from './storage/sparql/SparqlQueryAdapter';
import type { SparqlQueryAdapterOptions } from './storage/sparql/SparqlQueryAdapterOptions';
import type { OrArray, Entity } from './util/Types';
Expand Down Expand Up @@ -76,11 +75,12 @@ export class Skql {
this.do = new Proxy({} as VerbInterface, { get: getVerbHandler });
}

public async executeRawQuery<T extends QuadOrObject = Quad>(
query: string,
frame?: Frame,
): Promise<EntityOrTArray<T>> {
return await this.adapter.executeRawQuery<T>(query, frame);
public async executeRawQuery<T extends RawQueryResult>(query: string): Promise<T[]> {
return await this.adapter.executeRawQuery<T>(query);
}

public async executeRawEntityQuery(query: string, frame?: Frame): Promise<GraphObject> {
return await this.adapter.executeRawEntityQuery(query, frame);
}

public async find(options?: FindOneOptions): Promise<Entity> {
Expand Down
12 changes: 6 additions & 6 deletions src/storage/QueryAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
/* eslint-disable @typescript-eslint/method-signature-style */
import type { Quad } from '@rdfjs/types';
import type { GraphObject } from 'jsonld';
import type { Frame } from 'jsonld/jsonld-spec';
import type { Entity } from '../util/Types';
import type { FindAllOptions, FindOneOptions, FindOptionsWhere } from './FindOptionsTypes';

export type EntityOrTArray<T extends Quad | Record<string, number | boolean | string>> = T extends Quad
? Entity[] | GraphObject
: T[];
export type QuadOrObject = Quad | Record<string, number | boolean | string>;
export type RawQueryResult = Record<string, number | boolean | string>;

/**
* Adapts SKQL CRUD operations to a specific persistence layer.
Expand All @@ -18,7 +14,11 @@ export interface QueryAdapter {
/**
* Performs a raw query for data matching the query.
*/
executeRawQuery<T extends QuadOrObject>(query: string, frame?: Frame): Promise<EntityOrTArray<T>>;
executeRawQuery<T extends RawQueryResult>(query: string): Promise<T[]>;
/**
* Performs a raw query for entities matching the query. The query must be a CONSTRUCT query.
*/
executeRawEntityQuery(query: string, frame?: Frame): Promise<GraphObject>;
/**
* Finds first entity by a given find options.
* If entity was not found in the database - returns null.
Expand Down
17 changes: 12 additions & 5 deletions src/storage/memory/MemoryQueryAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/naming-convention */
import type { ReferenceNodeObject } from '@comake/rmlmapper-js';
import type { GraphObject } from 'jsonld';
import type { Frame } from 'jsonld/jsonld-spec';
import { toJSValueFromDataType } from '../../util/TripleUtil';
import type { Entity, EntityFieldValue, PossibleArrayFieldValues } from '../../util/Types';
Expand All @@ -15,7 +18,7 @@ import type {
FieldPrimitiveValue,
ValueObject,
} from '../FindOptionsTypes';
import type { EntityOrTArray, QuadOrObject, QueryAdapter } from '../QueryAdapter';
import type { QueryAdapter, RawQueryResult } from '../QueryAdapter';
import type { MemoryQueryAdapterOptions } from './MemoryQueryAdapterOptions';

/**
Expand All @@ -34,9 +37,14 @@ export class MemoryQueryAdapter implements QueryAdapter {
this.setTimestamps = options.setTimestamps ?? false;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async executeRawQuery<T extends QuadOrObject>(query: string, frame?: Frame): Promise<EntityOrTArray<T>> {
return [] as unknown as EntityOrTArray<T>;
public async executeRawQuery<T extends RawQueryResult>(query: string): Promise<T[]> {
return [] as T[];
}

public async executeRawEntityQuery(query: string, frame?: Frame): Promise<GraphObject> {
return {
'@graph': [],
};
}

public async find(options?: FindOneOptions): Promise<Entity | null> {
Expand Down Expand Up @@ -256,7 +264,6 @@ export class MemoryQueryAdapter implements QueryAdapter {
return res !== null;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async count(where?: FindOptionsWhere): Promise<number> {
return 0;
}
Expand Down
39 changes: 21 additions & 18 deletions src/storage/sparql/SparqlQueryAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { Quad, Literal, NamedNode } from '@rdfjs/types';
import type { GraphObject } from 'jsonld';
import type { Frame } from 'jsonld/jsonld-spec';
import SparqlClient from 'sparql-http-client';
import type {
Expand All @@ -11,14 +13,13 @@ import { Generator } from 'sparqljs';
import { toJSValueFromDataType, triplesToJsonld, triplesToJsonldWithFrame } from '../../util/TripleUtil';
import type { Entity } from '../../util/Types';
import type { FindOneOptions, FindAllOptions, FindOptionsWhere } from '../FindOptionsTypes';
import type { QueryAdapter, EntityOrTArray, QuadOrObject } from '../QueryAdapter';
import type { QueryAdapter, RawQueryResult } from '../QueryAdapter';
import type { SparqlQueryAdapterOptions } from './SparqlQueryAdapterOptions';
import { SparqlQueryBuilder } from './SparqlQueryBuilder';
import { SparqlUpdateBuilder } from './SparqlUpdateBuilder';

export type QuadOrVariableQueryResult<T extends QuadOrObject> = T extends Quad
? Quad
: Record<keyof T, NamedNode | Literal>;
export type SelectVariableQueryResult<T> = Record<keyof T, NamedNode | Literal>;

/**
* A {@link QueryAdapter} that stores data in a database through a sparql endpoint.
*/
Expand All @@ -36,25 +37,27 @@ export class SparqlQueryAdapter implements QueryAdapter {
this.setTimestamps = options.setTimestamps ?? false;
}

public async executeRawQuery<T extends QuadOrObject>(
query: string,
frame?: Frame,
): Promise<EntityOrTArray<T>> {
const response = await this.executeSparqlSelectAndGetData<QuadOrVariableQueryResult<T>>(query);
public async executeRawQuery<T extends RawQueryResult>(query: string): Promise<T[]> {
const response = await this.executeSparqlSelectAndGetData<SelectVariableQueryResult<T>>(query);
if (response.length === 0) {
return [] as unknown as EntityOrTArray<T>;
return [] as T[];
}
if (response[0].subject && response[0].predicate && response[0].object) {
return await triplesToJsonldWithFrame(response as Quad[], frame) as EntityOrTArray<T>;
}
return (response as Record<keyof T, NamedNode | Literal>[])
.map((result): Record<string, number | boolean | string> =>
Object.entries(result).reduce((obj, [ key, value ]): Record<string, number | boolean | string> => ({
return response
.map((result): RawQueryResult =>
Object.entries(result).reduce((obj, [ key, value ]): RawQueryResult => ({
...obj,
[key]: value.termType === 'Literal'
? toJSValueFromDataType(value.value, value.datatype?.value)
: value.value,
}), {})) as EntityOrTArray<T>;
}), {})) as T[];
}

public async executeRawEntityQuery(query: string, frame?: Frame): Promise<GraphObject> {
const response = await this.executeSparqlSelectAndGetData(query);
if (response.length === 0) {
return { '@graph': []};
}
return await triplesToJsonldWithFrame(response, frame);
}

public async find(options?: FindOneOptions): Promise<Entity | null> {
Expand Down Expand Up @@ -128,7 +131,7 @@ export class SparqlQueryAdapter implements QueryAdapter {
await this.executeSparqlUpdate(query);
}

private async executeSparqlSelectAndGetData<T extends Quad | Record<string, NamedNode | Literal> = Quad>(
private async executeSparqlSelectAndGetData<T extends Quad | SelectVariableQueryResult<any> = Quad>(
query: string,
): Promise<T[]> {
const stream = await this.sparqlClient.query.select(query);
Expand Down
2 changes: 1 addition & 1 deletion test/deploy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"main": "./dist/index.js",
"dependencies": {
"@comake/skql-js-engine": "file:./comake-skql-js-engine-0.12.0.tgz",
"@comake/skql-js-engine": "file:./comake-skql-js-engine-0.13.0.tgz",
"jsonld": "^6.0.0"
},
"devDependencies": {
Expand Down
15 changes: 11 additions & 4 deletions test/unit/Skql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,18 @@ describe('SKQL', (): void => {
jest.restoreAllMocks();
});

it('delegates calls to query to the query adapter.', async(): Promise<void> => {
const executeQuerySpy = jest.spyOn(MemoryQueryAdapter.prototype, 'executeRawQuery');
it('delegates calls to executeRawQuery to the query adapter.', async(): Promise<void> => {
const executeRawQuerySpy = jest.spyOn(MemoryQueryAdapter.prototype, 'executeRawQuery');
await expect(skql.executeRawQuery('')).resolves.toEqual([]);
expect(executeQuerySpy).toHaveBeenCalledTimes(1);
expect(executeQuerySpy).toHaveBeenCalledWith('', undefined);
expect(executeRawQuerySpy).toHaveBeenCalledTimes(1);
expect(executeRawQuerySpy).toHaveBeenCalledWith('');
});

it('delegates calls to executeRawEntityQuery to the query adapter.', async(): Promise<void> => {
const executeRawEntityQuerySpy = jest.spyOn(MemoryQueryAdapter.prototype, 'executeRawEntityQuery');
await expect(skql.executeRawEntityQuery('', {})).resolves.toEqual({ '@graph': []});
expect(executeRawEntityQuerySpy).toHaveBeenCalledTimes(1);
expect(executeRawEntityQuerySpy).toHaveBeenCalledWith('', {});
});

it('delegates calls to find to the query adapter.', async(): Promise<void> => {
Expand Down
15 changes: 14 additions & 1 deletion test/unit/storage/memory/MemoryQueryAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('a MemoryQueryAdapter', (): void => {
});

describe('executeRawQuery', (): void => {
it('is not supported.', async(): Promise<void> => {
it('is not supported and returns an empty array.', async(): Promise<void> => {
schemas = [{
'@id': 'https://skl.standard.storage/data/123',
'@type': 'https://skl.standard.storage/File',
Expand All @@ -33,6 +33,19 @@ describe('a MemoryQueryAdapter', (): void => {
});
});

describe('executeRawEntityQuery', (): void => {
it('is not supported and returns an empty GraphObject.', async(): Promise<void> => {
schemas = [{
'@id': 'https://skl.standard.storage/data/123',
'@type': 'https://skl.standard.storage/File',
}];
adapter = new MemoryQueryAdapter({ type: 'memory', schemas });
await expect(
adapter.executeRawEntityQuery('', {}),
).resolves.toEqual({ '@graph': []});
});
});

describe('find', (): void => {
it('returns a schema by id.', async(): Promise<void> => {
schemas = [{
Expand Down
81 changes: 50 additions & 31 deletions test/unit/storage/sparql/SparqlQueryAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,55 @@ describe('a SparqlQueryAdapter', (): void => {
});

describe('executeRawQuery', (): void => {
it('executes a sparql construct query and returns an empty array if no triples are found.',
it('executes a variable selection and returns an empty array if there are no results.', async(): Promise<void> => {
await expect(
adapter.executeRawQuery([
'SELECT ?modifiedAt ?related',
'WHERE {',
' {',
' SELECT ?modifiedAt ?related WHERE {',
' ?entity <http://purl.org/dc/terms/modified> ?modifiedAt',
' ?entity <https://example.com/related> ?related',
' }',
' LIMIT 1',
' }',
'}',
].join('\n')),
).resolves.toEqual([]);
});

it('executes a variable selection and returns an array of values.', async(): Promise<void> => {
response = [{
modifiedAt: DataFactory.literal('2022-10-10T00:00:00.000Z', XSD.dateTime),
related: DataFactory.namedNode('https://example.com/data/1'),
}];
await expect(
adapter.executeRawQuery([
'SELECT ?modifiedAt ?related',
'WHERE {',
' {',
' SELECT ?modifiedAt ?related WHERE {',
' ?entity <http://purl.org/dc/terms/modified> ?modifiedAt',
' ?entity <https://example.com/related> ?related',
' }',
' LIMIT 1',
' }',
'}',
].join('\n')),
).resolves.toEqual([
{
modifiedAt: '2022-10-10T00:00:00.000Z',
related: 'https://example.com/data/1',
},
]);
});
});

describe('executeRawEntityQuery', (): void => {
it('executes a sparql construct query and returns an empty GraphObject if no triples are found.',
async(): Promise<void> => {
await expect(
adapter.executeRawQuery([
adapter.executeRawEntityQuery([
'CONSTRUCT { ?subject ?predicate ?object. }',
'WHERE {',
' {',
Expand All @@ -61,7 +106,7 @@ describe('a SparqlQueryAdapter', (): void => {
' GRAPH ?entity { ?subject ?predicate ?object. }',
'}',
].join('\n')),
).resolves.toEqual([]);
).resolves.toEqual({ '@graph': []});
expect(select).toHaveBeenCalledTimes(1);
expect(select.mock.calls[0][0].split('\n')).toEqual([
'CONSTRUCT { ?subject ?predicate ?object. }',
Expand All @@ -76,15 +121,15 @@ describe('a SparqlQueryAdapter', (): void => {
'}',
]);
});
it('executes a sparql construct query and returns an array of nodes.',
it('executes a sparql construct query and returns GraphObject with an array of Entities.',
async(): Promise<void> => {
response = [{
subject: data1,
predicate: rdfTypeNamedNode,
object: file,
}];
await expect(
adapter.executeRawQuery([
adapter.executeRawEntityQuery([
'CONSTRUCT { ?subject ?predicate ?object. }',
'WHERE {',
' {',
Expand Down Expand Up @@ -116,32 +161,6 @@ describe('a SparqlQueryAdapter', (): void => {
'}',
]);
});

it('executes a variable selection and returns an array of values.', async(): Promise<void> => {
response = [{
modifiedAt: DataFactory.literal('2022-10-10T00:00:00.000Z', XSD.dateTime),
related: DataFactory.namedNode('https://example.com/data/1'),
}];
await expect(
adapter.executeRawQuery([
'SELECT ?modifiedAt ?related',
'WHERE {',
' {',
' SELECT ?modifiedAt ?related WHERE {',
' ?entity <http://purl.org/dc/terms/modified> ?modifiedAt',
' ?entity <https://example.com/related> ?related',
' }',
' LIMIT 1',
' }',
'}',
].join('\n')),
).resolves.toEqual([
{
modifiedAt: '2022-10-10T00:00:00.000Z',
related: 'https://example.com/data/1',
},
]);
});
});

describe('count', (): void => {
Expand Down

0 comments on commit 6936ffb

Please sign in to comment.