diff --git a/package-lock.json b/package-lock.json index 12ee28a..15c2e8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@comake/skl-js-engine", - "version": "0.15.2", + "version": "0.15.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@comake/skl-js-engine", - "version": "0.15.2", + "version": "0.15.3", "license": "BSD-4-Clause", "dependencies": { "@comake/openapi-operation-executor": "^0.11.1", diff --git a/package.json b/package.json index fbf7306..6be84e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@comake/skl-js-engine", - "version": "0.15.2", + "version": "0.15.3", "description": "Standard Knowledge Language Javascript Engine", "keywords": [ "skl", diff --git a/src/storage/sparql/SparqlUpdateBuilder.ts b/src/storage/sparql/SparqlUpdateBuilder.ts index 7ca1170..7247805 100644 --- a/src/storage/sparql/SparqlUpdateBuilder.ts +++ b/src/storage/sparql/SparqlUpdateBuilder.ts @@ -39,6 +39,12 @@ import { VariableGenerator } from './VariableGenerator'; export interface EntityUpdateQueries { clear: ClearDropOperation[]; insertions: GraphQuads[]; + timestampInsertions: GraphQuads[]; +} + +export interface EntityUpdateTriples { + entityTriples: Triple[]; + timestampTriples: Triple[]; } export interface SparqlUpdateBuilderArgs { @@ -62,22 +68,21 @@ export class SparqlUpdateBuilder { public buildUpdate(entityOrEntities: Entity | Entity[]): Update { const entities = ensureArray(entityOrEntities); - const { clear, insertions } = this.entitiesToGraphDeletionsAndInsertions(entities); - let insertUpdate: InsertDeleteOperation; - if (this.setTimestamps) { - insertUpdate = { + 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: insertions, + insert: timestampInsertions, where: [ bindNow ], - }; - } else { - insertUpdate = { - updateType: 'insert', - insert: insertions, - }; + }); } - return createSparqlUpdate([ ...clear, insertUpdate ]); + return createSparqlUpdate(updates); } public buildDelete(entityOrEntities: Entity | Entity[]): Update { @@ -94,37 +99,45 @@ export class SparqlUpdateBuilder { ids: string[], attributes: Partial, ): InsertDeleteOperation[] { - return ids.map((id): InsertDeleteOperation => { + return ids.flatMap((id): InsertDeleteOperation[] => { const subject = DataFactory.namedNode(id); const deletionTriples = this.partialEntityToDeletionTriples(attributes, subject); const insertionTriples = this.partialEntityToTriples(subject, attributes); - const whereTriples = [ ...deletionTriples ]; - const whereAdditions: Pattern[] = []; + const updates: InsertDeleteOperation[] = [ + { + updateType: 'insertdelete', + delete: [ createSparqlGraphQuads(subject, deletionTriples) ], + insert: [ createSparqlGraphQuads(subject, insertionTriples) ], + where: deletionTriples.map((triple): Pattern => + createSparqlOptional([ + createSparqlBasicGraphPattern([ triple ]), + ])), + using: { + default: [ subject ], + }, + } as InsertDeleteOperation, + ]; if (this.setTimestamps) { const modifiedVariable = DataFactory.variable(this.variableGenerator.getNext()); const modifiedDeletionTriple = { subject, predicate: modified, object: modifiedVariable }; const modifiedInsertionTriple = { subject, predicate: modified, object: now }; - deletionTriples.push(modifiedDeletionTriple); - insertionTriples.push(modifiedInsertionTriple); - whereTriples.push(modifiedDeletionTriple); - whereAdditions.push(bindNow); - } - const update = { - updateType: 'insertdelete', - delete: [ createSparqlGraphQuads(subject, deletionTriples) ], - insert: [ createSparqlGraphQuads(subject, insertionTriples) ], - where: [ - ...whereTriples.map((triple): Pattern => + updates.push({ + updateType: 'insertdelete', + delete: [ createSparqlGraphQuads(subject, [ modifiedDeletionTriple ]) ], + insert: [ createSparqlGraphQuads(subject, [ modifiedInsertionTriple ]) ], + where: [ createSparqlOptional([ - createSparqlBasicGraphPattern([ triple ]), - ])), - ...whereAdditions, - ], - using: { - default: [ subject ], - }, - } as InsertDeleteOperation; - return update; + createSparqlBasicGraphPattern([ modifiedDeletionTriple ]), + ]), + bindNow, + ], + using: { + default: [ subject ], + }, + } as InsertDeleteOperation); + } + + return updates; }); } @@ -134,11 +147,14 @@ export class SparqlUpdateBuilder { return entities .reduce((obj: EntityUpdateQueries, entity): EntityUpdateQueries => { const entityGraphName = DataFactory.namedNode(entity['@id']); - const insertionTriples = this.entityToTriples(entity, entityGraphName); + const { entityTriples, timestampTriples } = this.entityToTriples(entity, entityGraphName); obj.clear.push(createSparqlClearUpdate(entityGraphName)); - obj.insertions.push(createSparqlGraphQuads(entityGraphName, insertionTriples)); + obj.insertions.push(createSparqlGraphQuads(entityGraphName, entityTriples)); + if (timestampTriples.length > 0) { + obj.timestampInsertions.push(createSparqlGraphQuads(entityGraphName, timestampTriples)); + } return obj; - }, { clear: [], insertions: []}); + }, { clear: [], insertions: [], timestampInsertions: []}); } private entitiesToGraphDropUpdates( @@ -187,43 +203,37 @@ export class SparqlUpdateBuilder { return entityTriples; } - private entityToTriples(entity: NodeObject, subject: BlankNode | NamedNode): Triple[] { + private entityToTriples(entity: NodeObject, subject: BlankNode | NamedNode): EntityUpdateTriples { const entityTriples = Object.entries(entity).reduce((triples: Triple[], [ key, value ]): Triple[] => { const values = ensureArray(value); if (key !== '@id') { - let predicateTriples: Triple[]; if (key === '@type') { - predicateTriples = this.buildTriplesWithSubjectPredicateAndIriValue( + const predicateTriples = this.buildTriplesWithSubjectPredicateAndIriValue( subject, rdfTypeNamedNode, values as string[], ); - } else { - predicateTriples = this.buildTriplesForSubjectPredicateAndValues(subject, key, values); + return [ ...triples, ...predicateTriples ]; + } + if (!(this.setTimestamps && key === DCTERMS.modified)) { + const predicateTriples = this.buildTriplesForSubjectPredicateAndValues(subject, key, values); + return [ ...triples, ...predicateTriples ]; } - return [ ...triples, ...predicateTriples ]; } return triples; }, []); + const timestampTriples = []; if (this.setTimestamps && subject.termType === 'NamedNode') { - if (!entity[DCTERMS.created]) { - entityTriples.push({ subject, predicate: created, object: now }); - } - if (entity[DCTERMS.modified]) { - for (const triple of entityTriples) { - if (triple.subject.equals(subject) && - 'termType' in triple.predicate && - triple.predicate.equals(modified) - ) { - triple.object = now; - } - } - } else { - entityTriples.push({ subject, predicate: modified, object: now }); + if (!(DCTERMS.created in entity)) { + timestampTriples.push({ subject, predicate: created, object: now }); } + timestampTriples.push({ subject, predicate: modified, object: now }); } - return entityTriples; + return { + entityTriples, + timestampTriples, + }; } private buildTriplesForSubjectPredicateAndValues( @@ -313,9 +323,10 @@ export class SparqlUpdateBuilder { 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 }, - ...this.entityToTriples(value, blankNode), + ...entityTriples, ]; } } diff --git a/test/unit/storage/sparql/SparqlQueryAdapter.test.ts b/test/unit/storage/sparql/SparqlQueryAdapter.test.ts index 003fbb2..f2cdf0d 100644 --- a/test/unit/storage/sparql/SparqlQueryAdapter.test.ts +++ b/test/unit/storage/sparql/SparqlQueryAdapter.test.ts @@ -5,7 +5,7 @@ import SparqlClient from 'sparql-http-client'; import { InverseRelation } from '../../../../src/storage/operator/InverseRelation'; import { SparqlQueryAdapter } from '../../../../src/storage/sparql/SparqlQueryAdapter'; import { rdfTypeNamedNode } from '../../../../src/util/SparqlUtil'; -import { SKL, XSD } from '../../../../src/util/Vocabularies'; +import { DCTERMS, SKL, XSD } from '../../../../src/util/Vocabularies'; import { streamFrom } from '../../../util/Util'; const endpointUrl = 'https://example.com/sparql'; @@ -758,6 +758,28 @@ describe('a SparqlQueryAdapter', (): void => { 'INSERT DATA { GRAPH { . } }', ]); }); + + it('saves a single schema with setTimestamps on.', async(): Promise => { + adapter = new SparqlQueryAdapter({ type: 'sparql', endpointUrl, setTimestamps: true }); + const entity = { + '@id': 'https://example.com/data/1', + '@type': 'https://standardknowledge.com/ontologies/core/File', + }; + await expect(adapter.save(entity)).resolves.toEqual(entity); + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][0].split('\n')).toEqual([ + 'CLEAR SILENT GRAPH ;', + 'INSERT DATA { GRAPH { . } };', + 'INSERT {', + ' GRAPH {', + ` <${DCTERMS.created}> ?now.`, + ` <${DCTERMS.modified}> ?now.`, + ' }', + '}', + 'WHERE { BIND(NOW() AS ?now) }', + ]); + }); + it('saves multiple schema.', async(): Promise => { const entities = [ { @@ -796,6 +818,29 @@ describe('a SparqlQueryAdapter', (): void => { `WHERE { OPTIONAL { <${SKL.sourceId}> ?c1. } }`, ]); }); + + it('updates a schema with setTimestamps on.', async(): Promise => { + adapter = new SparqlQueryAdapter({ type: 'sparql', endpointUrl, setTimestamps: true }); + await expect(adapter.update( + 'https://example.com/data/1', + { [SKL.sourceId]: 'abc123' }, + )).resolves.toBeUndefined(); + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][0].split('\n')).toEqual([ + `DELETE { GRAPH { <${SKL.sourceId}> ?c1. } }`, + `INSERT { GRAPH { <${SKL.sourceId}> "abc123". } }`, + 'USING ', + `WHERE { OPTIONAL { <${SKL.sourceId}> ?c1. } };`, + `DELETE { GRAPH { <${DCTERMS.modified}> ?c2. } }`, + `INSERT { GRAPH { <${DCTERMS.modified}> ?now. } }`, + 'USING ', + `WHERE {`, + ` OPTIONAL { <${DCTERMS.modified}> ?c2. }`, + ' BIND(NOW() AS ?now)', + '}', + ]); + }); + it('updates multiple schemas by attribute.', async(): Promise => { await expect(adapter.update( [ 'https://example.com/data/1', 'https://example.com/data/2' ], diff --git a/test/unit/storage/sparql/SparqlUpdateBuilder.test.ts b/test/unit/storage/sparql/SparqlUpdateBuilder.test.ts index 4cbcc2e..d2e06cf 100644 --- a/test/unit/storage/sparql/SparqlUpdateBuilder.test.ts +++ b/test/unit/storage/sparql/SparqlUpdateBuilder.test.ts @@ -91,7 +91,6 @@ describe('A SparqlUpdateBuilder', (): void => { name: data1, triples: [ { subject: data1, predicate, object: c1 }, - { subject: data1, predicate: modified, object: c2 }, ], }, ], @@ -105,7 +104,6 @@ describe('A SparqlUpdateBuilder', (): void => { predicate, object: DataFactory.literal('marshmellow'), }, - { subject: data1, predicate: modified, object: now }, ], }, ], @@ -120,6 +118,32 @@ describe('A SparqlUpdateBuilder', (): void => { triples: [{ subject: data1, predicate, object: c1 }], }], }, + ], + }, + { + updateType: 'insertdelete', + delete: [ + { + type: 'graph', + name: data1, + triples: [ + { subject: data1, predicate: modified, object: c2 }, + ], + }, + ], + insert: [ + { + type: 'graph', + name: data1, + triples: [ + { subject: data1, predicate: modified, object: now }, + ], + }, + ], + using: { + default: [ data1 ], + }, + where: [ { type: 'optional', patterns: [{ @@ -350,6 +374,18 @@ describe('A SparqlUpdateBuilder', (): void => { name: data1, }, }, + { + updateType: 'insert', + insert: [ + { + type: 'graph', + name: data1, + triples: [ + { subject: data1, predicate: rdfTypeNamedNode, object: file }, + ], + }, + ], + }, { updateType: 'insertdelete', delete: [], @@ -358,7 +394,6 @@ describe('A SparqlUpdateBuilder', (): void => { type: 'graph', name: data1, triples: [ - { subject: data1, predicate: rdfTypeNamedNode, object: file }, { subject: data1, predicate: created, object: now }, { subject: data1, predicate: modified, object: now }, ], @@ -408,8 +443,7 @@ describe('A SparqlUpdateBuilder', (): void => { }, }, { - updateType: 'insertdelete', - delete: [], + updateType: 'insert', insert: [ { type: 'graph', @@ -421,6 +455,18 @@ describe('A SparqlUpdateBuilder', (): void => { predicate: created, object: DataFactory.literal('2022-10-12T00:00:00.000Z', XSD.dateTime), }, + ], + }, + ], + }, + { + updateType: 'insertdelete', + delete: [], + insert: [ + { + type: 'graph', + name: data1, + triples: [ { subject: data1, predicate: modified, object: now }, ], },