OpenHPS/openhps-rdf

View on GitHub
src/service/SPARQLGenerator.ts

Summary

Maintainability
F
4 days
Test Coverage
import {
    ArrayTypeDescriptor,
    DataSerializerUtils,
    FilterQuery,
    MapTypeDescriptor,
    ObjectMemberMetadata,
    QuerySelector,
    Serializable,
} from '@openhps/core';
import {
    FilterPattern,
    GroupPattern,
    Pattern,
    UnionPattern,
    Generator as SparqlGenerator,
    SparqlQuery,
    ConstructQuery,
    BgpPattern,
} from 'sparqljs';
import { DataFactory, Quad_Subject } from 'n3';
import { RDFIdentifierOptions, RDFLiteralOptions, RDFObjectOptions } from '../decorators';
import { IriString, RDFSerializer } from '../rdf';

export class SPARQLGenerator<T> {
    protected dataType: Serializable<T>;
    protected baseUri: IriString;
    private _counter = 0;

    constructor(dataType: Serializable<T>, baseUri: IriString) {
        this.dataType = dataType;
        this.baseUri = baseUri;
    }

    protected get next(): number {
        return this._counter++;
    }

    createInsert(object: T): string {
        const quads = RDFSerializer.serializeToQuads(object, this.baseUri);
        const generator = new SparqlGenerator();
        const query: SparqlQuery = {
            type: 'update',
            prefixes: {
                '': this.baseUri,
            },
            updates: [
                {
                    updateType: 'insert',
                    insert: [
                        {
                            type: 'bgp',
                            triples: quads,
                        },
                    ],
                },
            ],
        };
        return generator.stringify(query);
    }

    createDelete(id: IriString): string {
        const generator = new SparqlGenerator();
        const identifierMember = RDFSerializer.getUriMetadata(this.dataType);
        const query: SparqlQuery = {
            type: 'update',
            prefixes: {
                '': this.baseUri,
            },
            updates: [
                {
                    updateType: 'insertdelete',
                    insert: [],
                    delete: [
                        {
                            type: 'bgp',
                            triples: [
                                {
                                    subject: DataFactory.variable('subject'),
                                    predicate: DataFactory.variable('predicate'),
                                    object: DataFactory.variable('object'),
                                },
                            ],
                        },
                    ],
                    where: [
                        {
                            type: 'group',
                            patterns: [
                                {
                                    type: 'bgp',
                                    triples: [
                                        {
                                            subject: DataFactory.variable('subject'),
                                            predicate: DataFactory.variable('predicate'),
                                            object: DataFactory.variable('object'),
                                        },
                                    ],
                                },
                                ...this.createQuery({
                                    [identifierMember.key]: id,
                                }),
                            ],
                        },
                    ],
                },
            ],
        };
        return generator.stringify(query);
    }

    createDeleteAll(query: FilterQuery<T>): string {
        const generator = new SparqlGenerator();
        const sparqlQuery: SparqlQuery = {
            type: 'update',
            prefixes: {
                '': this.baseUri,
            },
            updates: [
                {
                    updateType: 'insertdelete',
                    insert: [],
                    delete: [
                        {
                            type: 'bgp',
                            triples: [
                                {
                                    subject: DataFactory.variable('subject'),
                                    predicate: DataFactory.variable('predicate'),
                                    object: DataFactory.variable('object'),
                                },
                            ],
                        },
                    ],
                    where: [
                        {
                            type: 'bgp',
                            triples: [
                                {
                                    subject: DataFactory.variable('subject'),
                                    predicate: DataFactory.variable('predicate'),
                                    object: DataFactory.variable('object'),
                                },
                            ],
                        },
                        ...this.createQuery(query),
                    ],
                },
            ],
        };
        return generator.stringify(sparqlQuery);
    }

    createFindAll(query: FilterQuery<T>): string {
        const generator = new SparqlGenerator();
        return generator.stringify(this.createConstruct(query));
    }

    protected createConstruct(query: FilterQuery): ConstructQuery {
        const patterns = this.createQuery(query);
        // Variables
        const subjectVar = DataFactory.variable('subject');
        const propVar = DataFactory.variable('prop');
        const valVar = DataFactory.variable('val');
        const childVar = DataFactory.variable('child');
        const childPropVar = DataFactory.variable('childProp');
        const childPropValVar = DataFactory.variable('childPropVal');
        const dummyPredicate = DataFactory.namedNode('http://example.org#overrides');

        const constructQuery: ConstructQuery = {
            queryType: 'CONSTRUCT',
            template: [
                {
                    subject: subjectVar,
                    predicate: propVar,
                    object: valVar,
                },
                {
                    subject: childVar,
                    predicate: childPropVar,
                    object: childPropValVar,
                },
            ],
            where: [
                {
                    type: 'bgp',
                    triples: [
                        {
                            subject: subjectVar,
                            predicate: propVar,
                            object: valVar,
                        },
                        {
                            subject: subjectVar,
                            predicate: {
                                type: 'path',
                                pathType: '+',
                                items: [
                                    {
                                        type: 'path',
                                        pathType: '|',
                                        items: [
                                            dummyPredicate,
                                            {
                                                type: 'path',
                                                pathType: '!',
                                                items: [dummyPredicate],
                                            },
                                        ],
                                    },
                                ],
                            },
                            object: childVar,
                        },
                        {
                            subject: childVar,
                            predicate: childPropVar,
                            object: childPropValVar,
                        },
                    ],
                },
                ...patterns,
            ],
            type: 'query',
            prefixes: {
                '': this.baseUri,
            },
        };
        return constructQuery;
    }

    protected isRegexQuery(query: any): boolean {
        return Object.prototype.toString.call(query) === '[object RegExp]';
    }

    protected generatePath(path: string, query: any, dataType: Serializable<any>): Pattern[] {
        return this.createQuery(
            path
                .split('.')
                .reverse()
                .reduce((res, key) => ({ [key]: res }), query),
            dataType,
        );
    }

    protected generateOp(key: string, query: Array<FilterQuery>): Pattern[] {
        const patterns: Pattern[] = [];
        switch (key) {
            case '$and':
                patterns.push(
                    ...query.map(
                        (q) =>
                            ({
                                type: 'group',
                                patterns: this.createQuery(q),
                            }) as GroupPattern,
                    ),
                );
                break;
            case '$or':
                patterns.push({
                    type: 'union',
                    patterns: query.map((q) => this.createQuery(q)).flat(),
                } as UnionPattern);
                break;
        }
        return patterns;
    }

    protected generateComponent(
        key: string,
        query: any,
        dataType: Serializable<any>,
        subject: Quad_Subject = DataFactory.variable('subject'),
    ): Pattern[] {
        const patterns: Pattern[] = [];
        if (key.startsWith('$')) {
            patterns.push(...this.generateOp(key, query));
        } else if (key.includes('.')) {
            patterns.push(...this.generatePath(key, query, dataType));
        } else {
            const rootMetadata = DataSerializerUtils.getOwnMetadata(dataType);

            let member: ObjectMemberMetadata;
            rootMetadata.knownTypes.forEach((knownType) => {
                if (!member) {
                    const metadata = DataSerializerUtils.getMetadata(knownType);
                    member = Array.from(metadata.dataMembers.values()).filter((member) => member.key === key)[0];
                    if (member) {
                        return;
                    }
                }
            });

            const rootMember = rootMetadata.dataMembers.get(key);
            const memberOptions =
                member && member.options && member.options.rdf
                    ? member
                    : rootMember && rootMember.options && rootMember.options.rdf
                      ? rootMember
                      : undefined;

            if (!memberOptions || !memberOptions.options.rdf) {
                return [];
            }

            if (memberOptions.options.rdf.identifier) {
                const rdf: RDFIdentifierOptions = memberOptions.options.rdf;
                const pattern: Pattern = {
                    type: 'filter',
                    expression: {
                        type: 'operation',
                        operator: '=',
                        args: [subject, DataFactory.namedNode(this.baseUri + rdf.serializer(query, dataType))],
                    },
                } as FilterPattern;
                patterns.push(pattern);
                return patterns;
            }
            const rdf: RDFObjectOptions = memberOptions.options.rdf as RDFObjectOptions;

            if (this.isRegexQuery(query)) {
                const regexp = query.toString();
                const regexPatterns: Pattern[][] = (Array.isArray(rdf.predicate) ? rdf.predicate : [rdf.predicate]).map(
                    (predicate) => {
                        return [
                            {
                                type: 'bgp',
                                triples: [
                                    {
                                        subject,
                                        predicate: DataFactory.namedNode(predicate),
                                        object: DataFactory.variable('object'),
                                    },
                                ],
                            } as BgpPattern,
                            {
                                type: 'filter',
                                expression: {
                                    type: 'operation',
                                    operator: 'regex',
                                    args: [
                                        DataFactory.variable('object'),
                                        DataFactory.literal(
                                            regexp.substring(regexp.indexOf('/') + 1, regexp.lastIndexOf('/')),
                                        ),
                                        DataFactory.literal('i'),
                                    ],
                                },
                            } as FilterPattern,
                        ];
                    },
                );

                patterns.push(
                    Array.isArray(rdf.predicate)
                        ? {
                              type: 'union',
                              patterns: regexPatterns.map((patterns) => {
                                  return {
                                      type: 'group',
                                      patterns,
                                  };
                              }),
                          }
                        : {
                              type: 'group',
                              patterns: regexPatterns[0],
                          },
                );
            } else if (typeof query === 'object') {
                const objectPatterns: Pattern[][] = (
                    Array.isArray(rdf.predicate) ? rdf.predicate : [rdf.predicate]
                ).map((predicate) => {
                    const patterns: Pattern[] = [];
                    const selectorPatterns = this.generateSelector(query, predicate, dataType, member, subject);
                    if (selectorPatterns.length > 0) {
                        patterns.push(...selectorPatterns);
                    } else {
                        const object = DataFactory.variable(`o${this.next}`);
                        const pattern: GroupPattern = {
                            type: 'group',
                            patterns: [
                                {
                                    type: 'bgp',
                                    triples: [
                                        {
                                            subject,
                                            predicate: DataFactory.namedNode(predicate),
                                            object,
                                        },
                                    ],
                                },
                            ],
                        };
                        pattern.patterns.push(
                            ...this.createQuery(query, member.type().ctor, object)
                                .map((x) => (x.type === 'group' ? x.patterns : [x]))
                                .flat(),
                        );
                        patterns.push(pattern);
                    }
                    return patterns;
                });
                patterns.push(
                    Array.isArray(rdf.predicate)
                        ? {
                              type: 'union',
                              patterns: objectPatterns.map((patterns) => {
                                  return {
                                      type: 'group',
                                      patterns,
                                  };
                              }),
                          }
                        : {
                              type: 'group', // TODO: Does not have to be grouped
                              patterns: objectPatterns[0],
                          },
                );
            } else {
                const rdfLiteralOptions = rdf as RDFLiteralOptions;
                const pattern: Pattern[] = (Array.isArray(rdf.predicate) ? rdf.predicate : [rdf.predicate]).map(
                    (predicate) => {
                        return {
                            type: 'bgp',
                            triples: [
                                {
                                    subject,
                                    predicate: DataFactory.namedNode(predicate),
                                    object: DataFactory.literal(
                                        query,
                                        rdfLiteralOptions.language
                                            ? rdfLiteralOptions.language
                                            : rdfLiteralOptions.datatype
                                              ? DataFactory.namedNode(rdfLiteralOptions.datatype)
                                              : undefined,
                                    ),
                                },
                            ],
                        };
                    },
                );
                patterns.push(
                    pattern.length > 1
                        ? {
                              type: 'union',
                              patterns: pattern,
                          }
                        : pattern[0],
                );
            }
        }
        return patterns;
    }

    protected generateSelector<T>(
        subquery: QuerySelector<T>,
        predicate: IriString,
        dataType: Serializable<T>,
        member: ObjectMemberMetadata,
        subject: Quad_Subject,
    ): Pattern[] {
        const patterns: Pattern[] = [];
        for (const selector of Object.keys(subquery)) {
            patterns.push(...this.generateComparisonSelector(selector, subquery, predicate, dataType, subject));
            patterns.push(...this.generateArraySelector(selector, subquery, predicate, dataType, member, subject));
        }
        return patterns;
    }

    protected generateComparisonSelector<T>(
        selector: string,
        subquery: QuerySelector<T>,
        predicate: IriString,
        dataType: Serializable<any>,
        subject: Quad_Subject,
    ): Pattern[] {
        const patterns: Pattern[] = [];
        let operator = undefined;
        switch (selector) {
            case '$gt':
                operator = '>';
                break;
            case '$gte':
                operator = '>=';
                break;
            case '$lt':
                operator = '<';
                break;
            case '$lte':
                operator = '<=';
                break;
            case '$eq':
                operator = '=';
                break;
        }

        if (operator) {
            const object = DataFactory.variable(`o${this.next}`);
            patterns.push({
                type: 'bgp',
                triples: [
                    {
                        subject,
                        predicate: DataFactory.namedNode(predicate),
                        object,
                    },
                ],
            });
            patterns.push({
                type: 'filter',
                expression: {
                    type: 'operation',
                    operator,
                    args: [object, DataFactory.literal((subquery as any)[selector])],
                },
            } as FilterPattern);
        }
        return patterns;
    }

    protected generateArraySelector<T>(
        selector: string,
        subquery: QuerySelector<T>,
        predicate: IriString,
        dataType: Serializable<any>,
        member: ObjectMemberMetadata,
        subject: Quad_Subject,
    ): Pattern[] {
        const patterns: Pattern[] = [];
        const objectType =
            member.type().ctor === Map
                ? (member.type() as MapTypeDescriptor).valueType
                : (member.type() as ArrayTypeDescriptor).elementType;
        const object = DataFactory.variable(`o${this.next}`);

        switch (selector) {
            case '$in':
                // result = result && Array.from(value).includes(subquery[selector]);
                break;
            case '$nin':
                // result = result && !Array.from(value).includes(subquery[selector]);
                break;
            case '$elemMatch':
                patterns.push({
                    type: 'group',
                    patterns: [
                        {
                            type: 'bgp',
                            triples: [
                                {
                                    subject,
                                    predicate: DataFactory.namedNode(predicate),
                                    object,
                                },
                            ],
                        },
                        ...this.createQuery(subquery[selector], objectType.ctor, object)
                            .map((x) => (x.type === 'group' ? x.patterns : [x]))
                            .flat(),
                    ],
                });
                break;
        }
        return patterns;
    }

    createQuery<T>(
        query: FilterQuery<T>,
        dataType: Serializable<any> = this.dataType,
        subject?: Quad_Subject,
    ): Pattern[] {
        const patterns: Pattern[] = [];
        if (query) {
            for (const key of Object.keys(query)) {
                patterns.push(...this.generateComponent(key, query[key], dataType, subject));
            }
        }
        return patterns;
    }
}