diff --git a/src/ldp/representation/RepresentationMetadata.ts b/src/ldp/representation/RepresentationMetadata.ts index 8e8f48e572..7ba5181b9a 100644 --- a/src/ldp/representation/RepresentationMetadata.ts +++ b/src/ldp/representation/RepresentationMetadata.ts @@ -1,8 +1,8 @@ import { DataFactory, Store } from 'n3'; -import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js'; +import type { BlankNode, DefaultGraph, Literal, NamedNode, Quad, Term } from 'rdf-js'; import { getLoggerFor } from '../../logging/LogUtil'; import { InternalServerError } from '../../util/errors/InternalServerError'; -import { toSubjectTerm, toObjectTerm, toCachedNamedNode, isTerm } from '../../util/TermUtil'; +import { toNamedTerm, toObjectTerm, toCachedNamedNode, isTerm } from '../../util/TermUtil'; import { CONTENT_TYPE, CONTENT_TYPE_TERM } from '../../util/Vocabularies'; import type { ResourceIdentifier } from './ResourceIdentifier'; import { isResourceIdentifier } from './ResourceIdentifier'; @@ -10,6 +10,7 @@ import { isResourceIdentifier } from './ResourceIdentifier'; export type MetadataIdentifier = ResourceIdentifier | NamedNode | BlankNode; export type MetadataValue = NamedNode | Literal | string | (NamedNode | Literal | string)[]; export type MetadataRecord = Record; +export type MetadataGraph = NamedNode | BlankNode | DefaultGraph | string; /** * Determines whether the object is a `RepresentationMetadata`. @@ -159,13 +160,18 @@ export class RepresentationMetadata { * @param subject - Subject of quad to add. * @param predicate - Predicate of quad to add. * @param object - Object of quad to add. + * @param graph - Optional graph of quad to add. */ public addQuad( subject: NamedNode | BlankNode | string, predicate: NamedNode | string, object: NamedNode | BlankNode | Literal | string, + graph?: MetadataGraph, ): this { - this.store.addQuad(toSubjectTerm(subject), toCachedNamedNode(predicate), toObjectTerm(object, true)); + this.store.addQuad(toNamedTerm(subject), + toCachedNamedNode(predicate), + toObjectTerm(object, true), + graph ? toNamedTerm(graph) : undefined); return this; } @@ -181,14 +187,19 @@ export class RepresentationMetadata { * @param subject - Subject of quad to remove. * @param predicate - Predicate of quad to remove. * @param object - Object of quad to remove. + * @param graph - Optional graph of quad to remove. */ public removeQuad( subject: NamedNode | BlankNode | string, predicate: NamedNode | string, object: NamedNode | BlankNode | Literal | string, + graph?: MetadataGraph, ): this { - this.store.removeQuad(toSubjectTerm(subject), toCachedNamedNode(predicate), toObjectTerm(object, true)); - return this; + const quads = this.quads(toNamedTerm(subject), + toCachedNamedNode(predicate), + toObjectTerm(object, true), + graph ? toNamedTerm(graph) : undefined); + return this.removeQuads(quads); } /** @@ -203,18 +214,20 @@ export class RepresentationMetadata { * Adds a value linked to the identifier. Strings get converted to literals. * @param predicate - Predicate linking identifier to value. * @param object - Value(s) to add. + * @param graph - Optional graph of where to add the values to. */ - public add(predicate: NamedNode | string, object: MetadataValue): this { - return this.forQuads(predicate, object, (pred, obj): any => this.addQuad(this.id, pred, obj)); + public add(predicate: NamedNode | string, object: MetadataValue, graph?: MetadataGraph): this { + return this.forQuads(predicate, object, (pred, obj): any => this.addQuad(this.id, pred, obj, graph)); } /** * Removes the given value from the metadata. Strings get converted to literals. * @param predicate - Predicate linking identifier to value. * @param object - Value(s) to remove. + * @param graph - Optional graph of where to remove the values from. */ - public remove(predicate: NamedNode | string, object: MetadataValue): this { - return this.forQuads(predicate, object, (pred, obj): any => this.removeQuad(this.id, pred, obj)); + public remove(predicate: NamedNode | string, object: MetadataValue, graph?: MetadataGraph): this { + return this.forQuads(predicate, object, (pred, obj): any => this.removeQuad(this.id, pred, obj, graph)); } /** @@ -234,33 +247,36 @@ export class RepresentationMetadata { /** * Removes all values linked through the given predicate. * @param predicate - Predicate to remove. + * @param graph - Optional graph where to remove from. */ - public removeAll(predicate: NamedNode | string): this { - this.removeQuads(this.store.getQuads(this.id, toCachedNamedNode(predicate), null, null)); + public removeAll(predicate: NamedNode | string, graph?: MetadataGraph): this { + this.removeQuads(this.store.getQuads(this.id, toCachedNamedNode(predicate), null, graph ?? null)); return this; } /** - * Finds all object values matching the given predicate. - * @param predicate - Predicate to get the values for. + * Finds all object values matching the given predicate and/or graph. + * @param predicate - Optional predicate to get the values for. + * @param graph - Optional graph where to get from. * * @returns An array with all matches. */ - public getAll(predicate: NamedNode | string): Term[] { - return this.store.getQuads(this.id, toCachedNamedNode(predicate), null, null) + public getAll(predicate: NamedNode | string, graph?: MetadataGraph): Term[] { + return this.store.getQuads(this.id, toCachedNamedNode(predicate), null, graph ?? null) .map((quad): Term => quad.object); } /** * @param predicate - Predicate to get the value for. + * @param graph - Optional graph where the triple should be found. * * @throws Error * If there are multiple matching values. * * @returns The corresponding value. Undefined if there is no match */ - public get(predicate: NamedNode | string): Term | undefined { - const terms = this.getAll(predicate); + public get(predicate: NamedNode | string, graph?: MetadataGraph): Term | undefined { + const terms = this.getAll(predicate, graph); if (terms.length === 0) { return; } @@ -278,11 +294,12 @@ export class RepresentationMetadata { * In case the object is undefined this is identical to `removeAll(predicate)`. * @param predicate - Predicate linking to the value. * @param object - Value(s) to set. + * @param graph - Optional graph where the triple should be stored. */ - public set(predicate: NamedNode | string, object?: MetadataValue): this { - this.removeAll(predicate); + public set(predicate: NamedNode | string, object?: MetadataValue, graph?: MetadataGraph): this { + this.removeAll(predicate, graph); if (object) { - this.add(predicate, object); + this.add(predicate, object, graph); } return this; } diff --git a/src/util/TermUtil.ts b/src/util/TermUtil.ts index 6308e6efce..3a55b789d4 100644 --- a/src/util/TermUtil.ts +++ b/src/util/TermUtil.ts @@ -38,17 +38,17 @@ export function isTerm(input?: any): input is Term { } /** - * Converts a subject to a named node when needed. + * Converts a string to a named node when needed. * @param subject - Subject to potentially transform. */ -export function toSubjectTerm(subject: string): NamedNode; -export function toSubjectTerm(subject: T): T; -export function toSubjectTerm(subject: T | string): T | NamedNode; -export function toSubjectTerm(subject: Term | string): Term { +export function toNamedTerm(subject: string): NamedNode; +export function toNamedTerm(subject: T): T; +export function toNamedTerm(subject: T | string): T | NamedNode; +export function toNamedTerm(subject: Term | string): Term { return typeof subject === 'string' ? namedNode(subject) : subject; } -export const toPredicateTerm = toSubjectTerm; +export const toPredicateTerm = toNamedTerm; /** * Converts an object term when needed. diff --git a/test/unit/ldp/representation/RepresentationMetadata.test.ts b/test/unit/ldp/representation/RepresentationMetadata.test.ts index 54faed1ee1..8e7256dd69 100644 --- a/test/unit/ldp/representation/RepresentationMetadata.test.ts +++ b/test/unit/ldp/representation/RepresentationMetadata.test.ts @@ -1,18 +1,35 @@ import 'jest-rdf'; -import { literal, namedNode, quad } from '@rdfjs/data-model'; -import type { Literal, NamedNode, Quad } from 'rdf-js'; +import { defaultGraph, literal, namedNode, quad } from '@rdfjs/data-model'; +import type { NamedNode, Quad } from 'rdf-js'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { CONTENT_TYPE } from '../../../../src/util/Vocabularies'; +// Helper functions to filter quads +function getQuads(quads: Quad[], subject?: string, predicate?: string, object?: string, graph?: string): Quad[] { + return quads.filter((qq): boolean => + (!subject || qq.subject.value === subject) && + (!predicate || qq.predicate.value === predicate) && + (!object || qq.object.value === object) && + (!graph || qq.graph.value === graph)); +} + +function removeQuads(quads: Quad[], subject?: string, predicate?: string, object?: string, graph?: string): Quad[] { + const filtered = getQuads(quads, subject, predicate, object, graph); + return quads.filter((qq): boolean => !filtered.includes(qq)); +} + describe('A RepresentationMetadata', (): void => { let metadata: RepresentationMetadata; const identifier = namedNode('http://example.com/id'); + const graphNode = namedNode('http://graph'); const inputQuads = [ quad(identifier, namedNode('has'), literal('data')), quad(identifier, namedNode('has'), literal('moreData')), quad(identifier, namedNode('hasOne'), literal('otherData')), + quad(identifier, namedNode('has'), literal('data'), graphNode), quad(namedNode('otherNode'), namedNode('linksTo'), identifier), quad(namedNode('otherNode'), namedNode('has'), literal('otherData')), + quad(namedNode('otherNode'), namedNode('graphData'), literal('otherData'), graphNode), ]; describe('constructor', (): void => { @@ -87,8 +104,9 @@ describe('A RepresentationMetadata', (): void => { }); it('can query quads.', async(): Promise => { - expect(metadata.quads(null, namedNode('has'), null)).toHaveLength(3); - expect(metadata.quads(null, null, literal('otherData'))).toHaveLength(2); + expect(metadata.quads(null, namedNode('has'))).toHaveLength(getQuads(inputQuads, undefined, 'has').length); + expect(metadata.quads(null, null, literal('otherData'))) + .toHaveLength(getQuads(inputQuads, undefined, undefined, 'otherData').length); }); it('can change the stored identifier.', async(): Promise => { @@ -96,10 +114,10 @@ describe('A RepresentationMetadata', (): void => { metadata.identifier = newIdentifier; const newQuads = inputQuads.map((triple): Quad => { if (triple.subject.equals(identifier)) { - return quad(newIdentifier, triple.predicate, triple.object); + return quad(newIdentifier, triple.predicate, triple.object, triple.graph); } if (triple.object.equals(identifier)) { - return quad(triple.subject, triple.predicate, newIdentifier); + return quad(triple.subject, triple.predicate, newIdentifier, triple.graph); } return triple; }); @@ -129,6 +147,18 @@ describe('A RepresentationMetadata', (): void => { expect(metadata.quads()).toBeRdfIsomorphic(expectedMetadata.quads()); }); + it('can add a quad.', async(): Promise => { + const newQuad = quad(namedNode('random'), namedNode('new'), literal('triple')); + metadata.addQuad('random', 'new', 'triple'); + expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.concat([ newQuad ])); + }); + + it('can add a quad with a graph.', async(): Promise => { + const newQuad = quad(namedNode('random'), namedNode('new'), literal('triple'), namedNode('graph')); + metadata.addQuad('random', 'new', 'triple', 'graph'); + expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.concat([ newQuad ])); + }); + it('can add quads.', async(): Promise => { const newQuads: Quad[] = [ quad(namedNode('random'), namedNode('new'), namedNode('triple')), @@ -137,6 +167,18 @@ describe('A RepresentationMetadata', (): void => { expect(metadata.quads()).toBeRdfIsomorphic([ ...newQuads, ...inputQuads ]); }); + it('can remove a quad.', async(): Promise => { + const old = inputQuads[0]; + metadata.removeQuad(old.subject as any, old.predicate as any, old.object as any, old.graph as any); + expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(1)); + }); + + it('removes all matching triples if graph is undefined.', async(): Promise => { + metadata.removeQuad(identifier, 'has', 'data'); + expect(metadata.quads()).toHaveLength(inputQuads.length - 2); + expect(metadata.quads()).toBeRdfIsomorphic(removeQuads(inputQuads, identifier.value, 'has', 'data')); + }); + it('can remove quads.', async(): Promise => { metadata.removeQuads([ inputQuads[0] ]); expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(1)); @@ -164,30 +206,46 @@ describe('A RepresentationMetadata', (): void => { }); it('can remove a single value for a predicate.', async(): Promise => { - metadata.remove(inputQuads[0].predicate as NamedNode, inputQuads[0].object as Literal); - expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(1)); + metadata.remove(namedNode('has'), literal('data')); + expect(metadata.quads()).toBeRdfIsomorphic(removeQuads(inputQuads, identifier.value, 'has', 'data')); }); it('can remove single values as string.', async(): Promise => { - metadata.remove(inputQuads[0].predicate as NamedNode, inputQuads[0].object.value); - expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(1)); + metadata.remove(namedNode('has'), 'data'); + expect(metadata.quads()).toBeRdfIsomorphic(removeQuads(inputQuads, identifier.value, 'has', 'data')); }); it('can remove multiple values for a predicate.', async(): Promise => { - metadata.remove(namedNode('has'), [ inputQuads[0].object, inputQuads[1].object ] as NamedNode[]); - expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(2)); + metadata.remove(namedNode('has'), [ literal('data'), 'moreData' ]); + let expected = removeQuads(inputQuads, identifier.value, 'has', 'data'); + expected = removeQuads(expected, identifier.value, 'has', 'moreData'); + expect(metadata.quads()).toBeRdfIsomorphic(expected); }); it('can remove all values for a predicate.', async(): Promise => { const pred = namedNode('has'); metadata.removeAll(pred); - const updatedNodes = inputQuads.filter((triple): boolean => - !triple.subject.equals(identifier) || !triple.predicate.equals(pred)); - expect(metadata.quads()).toBeRdfIsomorphic(updatedNodes); + expect(metadata.quads()).toBeRdfIsomorphic(removeQuads(inputQuads, identifier.value, 'has')); + }); + + it('can remove all values for a predicate in a specific graph.', async(): Promise => { + const pred = namedNode('has'); + metadata.removeAll(pred, graphNode); + expect(metadata.quads()).toBeRdfIsomorphic( + removeQuads(inputQuads, identifier.value, 'has', undefined, graphNode.value), + ); }); it('can get all values for a predicate.', async(): Promise => { - expect(metadata.getAll(namedNode('has'))).toEqualRdfTermArray([ literal('data'), literal('moreData') ]); + expect(metadata.getAll(namedNode('has'))).toEqualRdfTermArray( + [ literal('data'), literal('moreData'), literal('data') ], + ); + }); + + it('can get all values for a predicate in a graph.', async(): Promise => { + expect(metadata.getAll(namedNode('has'), defaultGraph())).toEqualRdfTermArray( + [ literal('data'), literal('moreData') ], + ); }); it('can get the single value for a predicate.', async(): Promise => { diff --git a/test/unit/util/TermUtil.test.ts b/test/unit/util/TermUtil.test.ts index 5c300d8ac0..f29d34699e 100644 --- a/test/unit/util/TermUtil.test.ts +++ b/test/unit/util/TermUtil.test.ts @@ -2,7 +2,7 @@ import 'jest-rdf'; import { literal, namedNode } from '@rdfjs/data-model'; import { toCachedNamedNode, - toSubjectTerm, + toNamedTerm, toPredicateTerm, toObjectTerm, toLiteral, @@ -45,11 +45,11 @@ describe('TermUtil', (): void => { describe('toSubjectTerm function', (): void => { it('returns the input if it was a term.', async(): Promise => { const nn = namedNode('name'); - expect(toSubjectTerm(nn)).toBe(nn); + expect(toNamedTerm(nn)).toBe(nn); }); it('returns a named node when a string is used.', async(): Promise => { - expect(toSubjectTerm('nn')).toEqualRdfTerm(namedNode('nn')); + expect(toNamedTerm('nn')).toEqualRdfTerm(namedNode('nn')); }); });