@comake/skl-js-engine
Version:
Standard Knowledge Language Javascript Engine
422 lines (384 loc) • 16.4 kB
text/typescript
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 ];
}
}