OpenHPS/openhps-solid

View on GitHub
src/common/SolidDataDriver.ts

Summary

Maintainability
D
2 days
Test Coverage
import { DataFrame, DataObject, Model, Constructor, FindOptions, FilterQuery, Serializable } from '@openhps/core';
import { SolidService, SolidSession } from './SolidService';
import { getSolidDataset, removeThing, saveSolidDatasetAt, Thing } from '@inrupt/solid-client';
import {
    RDFSerializer,
    Store,
    SPARQLDataDriver,
    SPARQLDriverOptions,
    Bindings,
    IriString,
    Subject,
} from '@openhps/rdf';
import type { QueryStringContext } from '@comunica/types';
import { QueryEngine } from './QueryEngine';
import { object } from '@openhps/rdf/dist/types/vocab/schema';

export class SolidDataDriver<T extends DataObject | DataFrame> extends SPARQLDataDriver<T> {
    public model: Model;
    // Solid service
    service: SolidService;
    protected options: SolidDataDriverOptions<T>;

    constructor(dataType: Constructor<T>, options?: SolidDataDriverOptions<T>) {
        super(dataType, options);
        this.options.engine = require('./engine-default'); // eslint-disable-line
        this.engine = new QueryEngine(this.options.engine);
        this.options.lenient = true;
        this.options.uriPrefix = this.options.uriPrefix || '/openhps';
        this.options.serialize = defaultThingSerializer;
        this.options.deserialize = defaultThingDeserializer;

        this.once('build', this._initService.bind(this));
    }

    private async _initService(): Promise<void> {
        return new Promise((resolve, reject) => {
            if (!this.model) {
                return resolve();
            }
            this.service = this.model.findService(SolidService);
            if (!this.service) {
                return reject(new Error(`Unable to find SolidDataService!`));
            }
            resolve();
        });
    }

    queryQuads(query: string, session?: SolidSession, options?: Partial<QueryStringContext>): Promise<Store> {
        if (session) {
            return super.queryQuads(query, {
                '@comunica/actor-http-inrupt-solid-client-authn:session': session,
                sources: [session.info.webId],
                lenient: true,
                ...options,
            });
        } else {
            return super.queryQuads(query, options);
        }
    }

    queryBindings(query: string, session?: SolidSession, options?: Partial<QueryStringContext>): Promise<Bindings[]> {
        if (session) {
            return super.queryBindings(query, {
                '@comunica/actor-http-inrupt-solid-client-authn:session': session,
                sources: [session.info.webId],
                lenient: true,
                ...options,
            });
        } else {
            return super.queryBindings(query, options);
        }
    }

    findByUID(id: string): Promise<T> {
        return new Promise((resolve, reject) => {
            this.service
                .findSessionByObjectUID(this.dataType, id)
                .then((session: SolidSession) => {
                    return this.service.getThing(session, `/${this.options.uriPrefix}/${id}`);
                })
                .then((thing) => {
                    const deserialized = this.options.deserialize(thing);
                    resolve(deserialized);
                })
                .catch(reject);
        });
    }

    findOne(query: SolidFilterQuery<T>, options: FindOptions = {}): Promise<T> {
        return this.service
            .findSessionByWebId(query.webId)
            .then((session) => {
                return this.service.getThing(session, query.uri);
            })
            .then((thing) => {
                const quads = RDFSerializer.serializeToQuads(thing);
                const store = new Store(quads);
                return super.findOne(query.query, options, {
                    source: store,
                });
            });
    }

    findAll(query: SolidFilterQuery<T>, options: FindOptions = {}): Promise<T[]> {
        return this.service
            .findSessionByWebId(query.webId)
            .then((session) => {
                return this.service.getThing(session, query.uri);
            })
            .then((thing) => {
                const quads = RDFSerializer.serializeToQuads(thing);
                const store = new Store(quads);
                return super.findAll(query.query, options, {
                    source: store,
                });
            });
    }

    count(query: SolidFilterQuery<T>): Promise<number> {
        return this.service
            .findSessionByWebId(query.webId)
            .then((session) => {
                return this.service.getThing(session, query.uri);
            })
            .then((thing) => {
                const quads = RDFSerializer.serializeToQuads(thing);
                const store = new Store(quads);
                return super.count(query.query, {
                    source: store,
                });
            });
    }

    insert(_, object: T): Promise<T> {
        return new Promise((resolve, reject) => {
            if (!object.webId) {
                if (this.service.session) {
                    object.webId = this.service.session.info.webId;
                } else {
                    return reject(new Error(`Unable to store data object or frame without WebID!`));
                }
            }
            this.service
                .findSessionByWebId(object.webId)
                .then(async (session) => {
                    if (!session) {
                        reject(new Error(`Unable to find solid session for ${object.webId}!`));
                        return;
                    }
                    if (!session.info.isLoggedIn) {
                        reject(new Error(`Solid session is not logged in!`));
                        return;
                    }
                    // Link the object
                    this.service.linkSession(object, session.info.sessionId, this.dataType);
                    const podURL = await this.service.getPodUrl(session);
                    const items: Thing[] = this.options.serialize(object, podURL);
                    if (items.length === 0) {
                        reject(new Error(`Unable to serialize object to RDF!`));
                        return;
                    }
                    const documentURL = new URL(items[0].url);
                    documentURL.hash = '';
                    await this.linkObjectToSession(object.uid, documentURL.href as IriString, session, this.dataType);
                    this.service
                        .getDataset(session, documentURL.href)
                        .then((dataset) => {
                            let promise = Promise.resolve(dataset);
                            for (let i = 0; i < items.length; i++) {
                                promise = promise.then(
                                    (dataset) =>
                                        new Promise((resolve, reject) => {
                                            this.service
                                                .createThing(session, items[i], dataset)
                                                .then((dataset) => {
                                                    resolve(dataset);
                                                })
                                                .catch(reject);
                                        }),
                                );
                            }
                            return promise;
                        })
                        .then((dataset) => {
                            return this.service.saveDataset(session, documentURL.href, dataset);
                        })
                        .then(() => {
                            resolve(object);
                        })
                        .catch(reject);
                })
                .catch(reject);
        });
    }

    delete(id: string): Promise<void> {
        return new Promise((resolve, reject) => {
            let uri: IriString;
            let currentSession: SolidSession;
            this.service
                .findSessionByObjectUID(this.dataType, id)
                .then(async (session) => {
                    if (!session) {
                        reject(new Error(`Unable to find solid session for ${this.dataType.name} with id '${id}'!`));
                        return;
                    }
                    currentSession = session as SolidSession;
                    uri = await this.findObjectURI(id, session, this.dataType);
                    // Unlink the object
                    this.service.unlinkSession(id, this.dataType);
                    await this.unlinkObjectFromSession(id, session, this.dataType);
                    // Delete from Pod
                    return getSolidDataset(uri, {
                        fetch: session.fetch,
                    });
                })
                .then((dataset) => {
                    dataset = removeThing(dataset, uri);
                    return saveSolidDatasetAt(uri, dataset, {
                        fetch: currentSession.fetch,
                    });
                })
                .then(() => resolve())
                .catch(reject);
        });
    }

    deleteAll(): Promise<void> {
        throw new Error(`Not supported with SolidDataDriver!`);
    }

    protected findObjectURI(id: string, session: SolidSession, type: Serializable<any>): Promise<IriString> {
        return new Promise((resolve, reject) => {
            const prefix = `${SolidService.PREFIX}:${type.name}`;
            const key = `${prefix}:${session.info.sessionId}:${id}`;
            this.service.storage
                .get(key)
                .then((value) => resolve(value as IriString))
                .catch(reject);
        });
    }

    protected linkObjectToSession(
        id: string,
        uri: IriString,
        session: SolidSession,
        type: Serializable<any>,
    ): Promise<void> {
        return new Promise((resolve, reject) => {
            const prefix = `${SolidService.PREFIX}:${type.name}`;
            const key = `${prefix}:${session.info.sessionId}:${id}`;
            this.service.storage.set(key, uri).then(resolve).catch(reject);
        });
    }

    protected unlinkObjectFromSession(id: string, session: SolidSession, type: Serializable<any>): Promise<void> {
        return new Promise((resolve, reject) => {
            const prefix = `${SolidService.PREFIX}:${type.name}`;
            const key = `${prefix}:${session.info.sessionId}:${id}`;
            this.service.storage.delete(key).then(resolve).catch(reject);
        });
    }
}

export interface SolidDataDriverOptions<T> extends SPARQLDriverOptions {
    /**
     * Serialize the object to an RDF thing
     */
    serialize?: (obj: T, baseURI?: IriString) => Thing[];
    /**
     * Deserialize the RDF thing to instance
     */
    deserialize?: (obj: Thing) => T;
    /**
     * URI prefix
     * @default /openhps
     */
    uriPrefix?: string;
}

/**
 * @param object
 * @param baseURI
 */
export function defaultThingSerializer<T extends DataObject | DataFrame>(object: T, baseURI: IriString): Thing[] {
    return RDFSerializer.serializeToSubjects(object, baseURI);
}

/**
 * @param thing
 */
export function defaultThingDeserializer<T extends DataObject | DataFrame>(thing: Thing): T {
    return RDFSerializer.deserializeFromSubjects(thing.url as IriString, [thing as Subject]);
}

export interface SolidFilterQuery<T> {
    webId: string;
    uri: string;
    query: FilterQuery<T>;
}