UNPKG

@comake/skl-js-engine

Version:

Standard Knowledge Language Javascript Engine

422 lines (384 loc) 16.4 kB
import { RDF } from '@comake/rmlmapper-js'; import DataFactory from '@rdfjs/data-model'; import type { BlankNode, NamedNode, Term } from '@rdfjs/types'; import type { NodeObject, ValueObject } from 'jsonld/jsonld'; import type { Update, GraphQuads, Triple, InsertDeleteOperation, UpdateOperation, ClearDropOperation, Pattern } from 'sparqljs'; import { EngineConstants } from '../../../constants'; import { bindNow, created, createSparqlBasicGraphPattern, createSparqlClearUpdate, createSparqlDropUpdate, createSparqlGraphQuads, createSparqlOptional, createSparqlUpdate, dropAll, firstPredicate, modified, nilPredicate, now, rdfTypeNamedNode, restPredicate } from '../../../util/SparqlUtil'; import { valueToLiteral } from '../../../util/TripleUtil'; import type { Entity } from '../../../util/Types'; import { ensureArray } from '../../../util/Util'; import { VariableGenerator } from './VariableGenerator'; export interface EntityUpdateQueries { clear: ClearDropOperation[]; insertions: GraphQuads[]; timestampInsertions: GraphQuads[]; } export interface EntityUpdateTriples { entityTriples: Triple[]; timestampTriples: Triple[]; insertions: GraphQuads[]; } export interface SparqlUpdateBuilderArgs { setTimestamps?: boolean; } export class SparqlUpdateBuilder { private readonly variableGenerator: VariableGenerator; private readonly setTimestamps: boolean; public constructor(args?: SparqlUpdateBuilderArgs) { this.variableGenerator = new VariableGenerator(); this.setTimestamps = args?.setTimestamps ?? false; } public buildPartialUpdate(idOrIds: string | string[], attributes: Partial<Entity>): Update { const ids = ensureArray(idOrIds); const updates = this.idsAndAttributesToGraphDeletionsAndInsertions(ids, attributes); return createSparqlUpdate(updates); } public buildUpdate(entityOrEntities: Entity | Entity[]): Update { const entities = ensureArray(entityOrEntities); const { clear, insertions, timestampInsertions } = this.entitiesToGraphDeletionsAndInsertions(entities); const insertUpdate: InsertDeleteOperation = { updateType: 'insert', insert: insertions }; const updates = [ ...clear, insertUpdate ]; if (timestampInsertions.length > 0) { updates.push({ updateType: 'insertdelete', delete: [], insert: timestampInsertions, where: [ bindNow ] }); } return createSparqlUpdate(updates); } public buildDeleteById(idOrIds: string | string[]): Update { const ids = ensureArray(idOrIds); const drops = this.idsToGraphDropUpdates(ids); return createSparqlUpdate(drops); } public buildDelete(entityOrEntities: Entity | Entity[]): Update { const entities = ensureArray(entityOrEntities); const drops = this.entitiesToGraphDropUpdates(entities); return createSparqlUpdate(drops); } public buildDeleteAll(): Update { return createSparqlUpdate([ dropAll ]); } private idsAndAttributesToGraphDeletionsAndInsertions( ids: string[], attributes: Partial<Entity> ): InsertDeleteOperation[] { return ids.flatMap((id): InsertDeleteOperation[] => { const subject = DataFactory.namedNode(id); const { attributesToUpdate, attributesToDelete } = this.partitionAttributes(attributes); const { triples: deletionTriples, deletions: deletionDeletions } = this.partialEntityToDeletionTriples(attributesToUpdate, subject); const { triples: insertionTriples, insertions: insertionInsertions } = this.partialEntityToTriples(subject, attributesToUpdate); const { triples: deleteOnlyTriples } = this.partialEntityToDeletionTriples(attributesToDelete, subject); const updates: InsertDeleteOperation[] = []; if (deletionTriples.length > 0) { updates.push({ updateType: 'insertdelete', delete: [ createSparqlGraphQuads(subject, deletionTriples) ], insert: [], where: deletionTriples.map( (triple): Pattern => createSparqlOptional([ createSparqlBasicGraphPattern([ triple ]) ]) ), using: { default: [ subject ] } } as InsertDeleteOperation); } for (const deletion of deletionDeletions) { updates.push({ updateType: 'insertdelete', delete: [ deletion ], insert: [], where: [ ...deletion.triples.map( (triple): Pattern => createSparqlOptional([ createSparqlBasicGraphPattern([ triple ]) ]) ) ], using: { default: [ deletion.name ] } } as InsertDeleteOperation); } if (insertionTriples.length > 0) { updates.push({ updateType: 'insert', insert: [ createSparqlGraphQuads(subject, insertionTriples), ...insertionInsertions ] } as InsertDeleteOperation); } if (deleteOnlyTriples.length > 0) { updates.push({ updateType: 'insertdelete', delete: [ createSparqlGraphQuads(subject, deleteOnlyTriples) ], insert: [], where: deleteOnlyTriples.map( (triple): Pattern => createSparqlOptional([ createSparqlBasicGraphPattern([ triple ]) ]) ), using: { default: [ subject ] } } as InsertDeleteOperation); } const hasAnyChanges = deletionTriples.length > 0 || insertionTriples.length > 0 || deleteOnlyTriples.length > 0; if (this.setTimestamps && hasAnyChanges) { const modifiedVariable = DataFactory.variable(this.variableGenerator.getNext()); const modifiedDeletionTriple = { subject, predicate: modified, object: modifiedVariable }; const modifiedInsertionTriple = { subject, predicate: modified, object: now }; updates.push({ updateType: 'insertdelete', delete: [ createSparqlGraphQuads(subject, [ modifiedDeletionTriple ]) ], insert: [ createSparqlGraphQuads(subject, [ modifiedInsertionTriple ]) ], where: [ createSparqlOptional([ createSparqlBasicGraphPattern([ modifiedDeletionTriple ]) ]), bindNow ], using: { default: [ subject ] } } as InsertDeleteOperation); } return updates; }); } private partitionAttributes( attributes: Partial<Entity> ): { attributesToUpdate: Partial<Entity>; attributesToDelete: Partial<Entity> } { const attributesToUpdate: Partial<Entity> = {}; const attributesToDelete: Partial<Entity> = {}; Object.entries(attributes).forEach(([ key, value ]) => { if (key === '@id') { return; } if (value === null) { attributesToDelete[key] = value; } else if (value !== undefined) { attributesToUpdate[key] = value; } }); return { attributesToUpdate, attributesToDelete }; } private entitiesToGraphDeletionsAndInsertions(entities: Entity[]): EntityUpdateQueries { return entities.reduce( (obj: EntityUpdateQueries, entity): EntityUpdateQueries => { const entityGraphName = DataFactory.namedNode(entity['@id']); const { entityTriples, timestampTriples, insertions } = this.entityToTriples(entity, entityGraphName); obj.clear.push(createSparqlClearUpdate(entityGraphName)); obj.insertions.push(createSparqlGraphQuads(entityGraphName, entityTriples), ...insertions); if (timestampTriples.length > 0) { obj.timestampInsertions.push(createSparqlGraphQuads(entityGraphName, timestampTriples)); } return obj; }, { clear: [], insertions: [], timestampInsertions: []} ); } private idsToGraphDropUpdates(ids: string[]): UpdateOperation[] { return ids.map((id): UpdateOperation => { const entityGraphName = DataFactory.namedNode(id); return createSparqlDropUpdate(entityGraphName); }); } private entitiesToGraphDropUpdates(entities: Entity[]): UpdateOperation[] { return entities.map((entity): UpdateOperation => { const entityGraphName = DataFactory.namedNode(entity['@id']); return createSparqlDropUpdate(entityGraphName); }); } private partialEntityToDeletionTriples(entity: NodeObject, subject: NamedNode): { triples: Triple[]; deletions: GraphQuads[] } { return Object.entries(entity).reduce((acc: { triples: Triple[]; deletions: GraphQuads[] }, [ key, value ]): { triples: Triple[]; deletions: GraphQuads[] } => { if (key !== '@id') { let deletions: GraphQuads[] = []; if (value && typeof value === 'object' && '@id' in value && typeof value['@id'] === 'string' && '@type' in value) { const { triples: nestedTriples, deletions: nestedDeletions } = this.partialEntityToDeletionTriples(value as NodeObject, DataFactory.namedNode(value['@id'])); deletions = [ ...deletions, createSparqlGraphQuads(DataFactory.namedNode(value['@id']), nestedTriples), ...nestedDeletions ]; } return { triples: [ ...acc.triples, this.buildTriplesWithSubjectPredicateAndVariableValue( subject, key === '@type' ? rdfTypeNamedNode : DataFactory.namedNode(key), this.variableGenerator.getNext() ) ], deletions: [ ...acc.deletions, ...deletions ] }; } return acc; }, { triples: [], deletions: [] }); } private partialEntityToTriples(subject: NamedNode, entity: NodeObject): { triples: Triple[]; insertions: GraphQuads[] } { return Object.entries(entity).reduce((acc: { triples: Triple[]; insertions: GraphQuads[] }, [ key, value ]): { triples: Triple[]; insertions: GraphQuads[] } => { const values = ensureArray(value); if (key !== '@id') { let predicateTriples: { triples: Triple[]; insertions: GraphQuads[] }; if (key === '@type') { predicateTriples = { triples: this.buildTriplesWithSubjectPredicateAndIriValue( subject, rdfTypeNamedNode, values as string[] ), insertions: [] }; } else { predicateTriples = this.buildTriplesForSubjectPredicateAndValues(subject, key, values); } return { triples: [ ...acc.triples, ...predicateTriples.triples ], insertions: [ ...acc.insertions, ...predicateTriples.insertions ] }; } return acc; }, { triples: [], insertions: [] }); } private entityToTriples(entity: NodeObject, subject: BlankNode | NamedNode): EntityUpdateTriples { const entityTriples = Object.entries(entity).reduce((acc: { triples: Triple[]; insertions: GraphQuads[] }, [ key, value ]): { triples: Triple[]; insertions: GraphQuads[] } => { const values = ensureArray(value); if (key !== '@id') { if (key === '@type') { const predicateTriples = this.buildTriplesWithSubjectPredicateAndIriValue( subject, rdfTypeNamedNode, values as string[] ); return { triples: [ ...acc.triples, ...predicateTriples ], insertions: acc.insertions }; } if (!(this.setTimestamps && key === EngineConstants.prop.dateModified)) { const predicateTriples = this.buildTriplesForSubjectPredicateAndValues(subject, key, values); return { triples: [ ...acc.triples, ...predicateTriples.triples ], insertions: [ ...acc.insertions, ...predicateTriples.insertions ] }; } } return acc; }, { triples: [], insertions: [] }); const timestampTriples = []; if (this.setTimestamps && subject.termType === 'NamedNode') { if (!(EngineConstants.prop.dateCreated in entity)) { timestampTriples.push({ subject, predicate: created, object: now }); } timestampTriples.push({ subject, predicate: modified, object: now }); } return { entityTriples: entityTriples.triples, insertions: entityTriples.insertions, timestampTriples }; } private buildTriplesForSubjectPredicateAndValues( subject: BlankNode | NamedNode, predicate: string, values: any[] ): { triples: Triple[]; insertions: GraphQuads[] } { const predicateTerm = DataFactory.namedNode(predicate); // Return values.flatMap((value: any): { triples: Triple[]; insertions: GraphQuads[] } => // this.buildTriplesWithSubjectPredicateAndValue(subject, predicateTerm, value)); return values.reduce((acc: { triples: Triple[]; insertions: GraphQuads[] }, value: any) => { const { triples, insertions } = this.buildTriplesWithSubjectPredicateAndValue(subject, predicateTerm, value); return { triples: [ ...acc.triples, ...triples ], insertions: [ ...acc.insertions, ...insertions ] }; }, { triples: [], insertions: [] }); } private buildTriplesWithSubjectPredicateAndIriValue( subject: BlankNode | NamedNode, predicate: NamedNode, values: string[] ): Triple[] { return values.map( (valueItem): Triple => ({ subject, predicate, object: DataFactory.namedNode(valueItem) } as Triple) ); } private buildTriplesWithSubjectPredicateAndVariableValue( subject: NamedNode, predicate: NamedNode, value: string ): Triple { return { subject, predicate, object: DataFactory.variable(value) }; } private buildTriplesWithSubjectPredicateAndValue( subject: BlankNode | NamedNode, predicate: NamedNode, value: any ): { triples: Triple[]; insertions: GraphQuads[] } { const isObject = typeof value === 'object'; if (isObject) { if ('@list' in value) { return { triples: this.buildTriplesForList(subject, predicate, value['@list']), insertions: [] }; } if ('@value' in value) { return { triples: [ { subject, predicate, object: this.jsonLdValueObjectToLiteral(value) } as Triple ], insertions: [] }; } const isReferenceObject = '@id' in value; const isBlankNodeReferenceObject = !isReferenceObject || (value['@id'] as string).startsWith('_:'); if (isBlankNodeReferenceObject) { return { triples: this.buildTriplesForBlankNode(subject, predicate, value as NodeObject), insertions: [] }; } if (isReferenceObject) { const triples = [ { subject, predicate, object: DataFactory.namedNode(value['@id']) } as Triple ]; if (value['@type']) { const { entityTriples, insertions } = this.entityToTriples(value as NodeObject, DataFactory.namedNode(value['@id'])); return { triples, insertions: [ ...insertions, createSparqlGraphQuads(DataFactory.namedNode(value['@id']), entityTriples) ] }; } return { triples, insertions: [] }; } } return { triples: [ { subject, predicate, object: valueToLiteral(value) } as Triple ], insertions: [] }; } private jsonLdValueObjectToLiteral(value: ValueObject): Term { if (typeof value['@value'] === 'object') { return DataFactory.literal(JSON.stringify(value['@value']), RDF.JSON); } if ((value as any)['@language']) { return DataFactory.literal(value['@value'] as string, (value as any)['@language']); } if ((value as any)['@type']) { return DataFactory.literal(value['@value'].toString(), (value as any)['@type']); } return valueToLiteral(value['@value']); } private buildTriplesForList(subject: BlankNode | NamedNode, predicate: NamedNode, value: NodeObject[]): Triple[] { const blankNode = DataFactory.blankNode(this.variableGenerator.getNext()); const rest = value.length > 1 ? this.buildTriplesForList(blankNode, restPredicate, value.slice(1)) : [{ subject: blankNode, predicate: restPredicate, object: nilPredicate }]; return [ { subject, predicate, object: blankNode }, ...this.buildTriplesWithSubjectPredicateAndValue(blankNode, firstPredicate, value[0]).triples, ...rest ]; } private buildTriplesForBlankNode(subject: BlankNode | NamedNode, predicate: NamedNode, value: NodeObject): Triple[] { const blankNode = DataFactory.blankNode(this.variableGenerator.getNext()); const { entityTriples } = this.entityToTriples(value, blankNode); return [{ subject, predicate, object: blankNode }, ...entityTriples ]; } }