Repository: solid/community-server (ts)

Issue:
PATCH writes unnecessary metadata
This might be the reason the original patch handler didn't copy metadata.

When doing a PATCH the patch handler reads out a resource, edits it, and writes it back. The issue is that that resource is going to have auto-generated metadata such as the `posix:` metadata for the `FileDataAccessor`, but also the `acl:accessControl` for all backends. This metadata will then be stored after the PATCH operation. In the case of a FileDataAccessor this is going to create extra `.meta` files, but also cause invalid metadata to be returned in the following GET operation. The `acl:accessControl` value is going to be fine since that is always the same value, but for example `dc:modified` is going to have 2 values.

One workaround is to have function that add dynamic metadata use `setMetadata`, thereby overwriting previous values.

This solution is of course not ideal. There should be a way the patch handlers knows that metadata is dynamic, or the backend knows the incoming metadata contains dynamic values that should be ignored.

Similar to #384, but this issue applies to all resources and has a potential workaround.

Pull request:
Remove generated metadata by using metadata graphs
Closes #774 .

Alternative solution to #782 .

In #782 the solution is to force all functions that generate metadata to also have a function that removes the same metadata.

The solution in this PR is to assign that metadata to a specific graph and then remove that entire graph from the metadata before storing it in the backend.

This second solution makes many things a lot easier, as it no longer requires double the work when generating metadata. The main issue with this one is that introducing graphs into the metadata might complicate certain things and make it harder to understand how to use it. I tried to keep everything working as it used to in case no graphs are mentioned. In general the idea is that it shouldn't matter in which graph the metadata is stored when requesting information. Things can go wrong now though if the same information would be stored in different graphs so that's something that needs to be remembered when writing metadata.

Opinions are welcome. Personally I prefer this one due to it requiring less work and also have more potential for extensability in the future.

Note that none of these solutions solves the issue of patching containers since there the problem is in the body itself.

Patch:
diff --git a/src/ldp/auxiliary/LinkMetadataGenerator.ts b/src/ldp/auxiliary/LinkMetadataGenerator.ts
index 5ec4eba26..bb7563b27 100644
--- a/src/ldp/auxiliary/LinkMetadataGenerator.ts
+++ b/src/ldp/auxiliary/LinkMetadataGenerator.ts
@@ -1,4 +1,5 @@
 import { namedNode } from '@rdfjs/data-model';
+import { SOLID_META } from '../../util/Vocabularies';
 import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
 import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
 import { MetadataGenerator } from './MetadataGenerator';
@@ -22,7 +23,9 @@ export class LinkMetadataGenerator extends MetadataGenerator {
   public async handle(metadata: RepresentationMetadata): Promise<void> {
     const identifier = { path: metadata.identifier.value };
     if (!this.identifierStrategy.isAuxiliaryIdentifier(identifier)) {
-      metadata.add(this.link, namedNode(this.identifierStrategy.getAuxiliaryIdentifier(identifier).path));
+      metadata.add(this.link,
+        namedNode(this.identifierStrategy.getAuxiliaryIdentifier(identifier).path),
+        SOLID_META.terms.ResponseMetadata);
     }
   }
 }
diff --git a/src/ldp/representation/RepresentationMetadata.ts b/src/ldp/representation/RepresentationMetadata.ts
index 8e8f48e57..7ba5181b9 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<string, MetadataValue>;
+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/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts
index 0625c9e42..0be878886 100644
--- a/src/storage/DataAccessorBasedStore.ts
+++ b/src/storage/DataAccessorBasedStore.ts
@@ -26,7 +26,18 @@ import {
 } from '../util/PathUtil';
 import { parseQuads } from '../util/QuadUtil';
 import { addResourceMetadata } from '../util/ResourceUtil';
-import { CONTENT_TYPE, DC, SOLID_HTTP, LDP, POSIX, PIM, RDF, VANN, XSD } from '../util/Vocabularies';
+import {
+  CONTENT_TYPE,
+  DC,
+  SOLID_HTTP,
+  LDP,
+  POSIX,
+  PIM,
+  RDF,
+  XSD,
+  SOLID_META,
+  PREFERRED_PREFIX_TERM,
+} from '../util/Vocabularies';
 import type { DataAccessor } from './accessors/DataAccessor';
 import type { ResourceStore } from './ResourceStore';
 
@@ -98,16 +109,23 @@ export class DataAccessorBasedStore implements ResourceStore {
       for await (const child of this.accessor.getChildren(identifier)) {
         if (!this.auxiliaryStrategy.isAuxiliaryIdentifier({ path: child.identifier.value })) {
           metadata.addQuads(child.quads());
-          metadata.add(LDP.terms.contains, child.identifier as NamedNode);
+          metadata.add(LDP.terms.contains, child.identifier as NamedNode, SOLID_META.terms.ResponseMetadata);
         }
       }
 
       // Generate a container representation from the metadata
-      const data = metadata.quads();
-      metadata.addQuad(DC.terms.namespace, VANN.terms.preferredNamespacePrefix, 'dc');
-      metadata.addQuad(LDP.terms.namespace, VANN.terms.preferredNamespacePrefix, 'ldp');
-      metadata.addQuad(POSIX.terms.namespace, VANN.terms.preferredNamespacePrefix, 'posix');
-      metadata.addQuad(XSD.terms.namespace, VANN.terms.preferredNamespacePrefix, 'xsd');
+      // All triples should be in the same graph for the data representation
+      const data = metadata.quads().map((triple): Quad => {
+        if (triple.graph.termType === 'DefaultGraph') {
+          return triple;
+        }
+        return DataFactory.quad(triple.subject, triple.predicate, triple.object);
+      });
+
+      metadata.addQuad(DC.terms.namespace, PREFERRED_PREFIX_TERM, 'dc', SOLID_META.terms.ResponseMetadata);
+      metadata.addQuad(LDP.terms.namespace, PREFERRED_PREFIX_TERM, 'ldp', SOLID_META.terms.ResponseMetadata);
+      metadata.addQuad(POSIX.terms.namespace, PREFERRED_PREFIX_TERM, 'posix', SOLID_META.terms.ResponseMetadata);
+      metadata.addQuad(XSD.terms.namespace, PREFERRED_PREFIX_TERM, 'xsd', SOLID_META.terms.ResponseMetadata);
       representation = new BasicRepresentation(data, metadata, INTERNAL_QUADS);
     } else {
       // Retrieve a document representation from the accessor
@@ -317,6 +335,11 @@ export class DataAccessorBasedStore implements ResourceStore {
       }
     }
 
+    // Remove all generated metadata to prevent it from being stored permanently
+    representation.metadata.removeQuads(
+      representation.metadata.quads(null, null, null, SOLID_META.terms.ResponseMetadata),
+    );
+
     await (isContainer ?
       this.accessor.writeContainer(identifier, representation.metadata) :
       this.accessor.writeDocument(identifier, representation.data, representation.metadata));
diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts
index 98cc513a2..a82208ce2 100644
--- a/src/storage/accessors/FileDataAccessor.ts
+++ b/src/storage/accessors/FileDataAccessor.ts
@@ -15,7 +15,7 @@ import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil';
 import { parseQuads, serializeQuads } from '../../util/QuadUtil';
 import { addResourceMetadata } from '../../util/ResourceUtil';
 import { toLiteral } from '../../util/TermUtil';
-import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../util/Vocabularies';
+import { CONTENT_TYPE, DC, LDP, POSIX, RDF, SOLID_META, XSD } from '../../util/Vocabularies';
 import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper';
 import type { DataAccessor } from './DataAccessor';
 
@@ -319,10 +319,14 @@ export class FileDataAccessor implements DataAccessor {
    * @param stats - Stats of the file/directory corresponding to the resource.
    */
   private addPosixMetadata(metadata: RepresentationMetadata, stats: Stats): void {
-    metadata.add(DC.terms.modified, toLiteral(stats.mtime.toISOString(), XSD.terms.dateTime));
-    metadata.add(POSIX.terms.mtime, toLiteral(Math.floor(stats.mtime.getTime() / 1000), XSD.terms.integer));
+    metadata.add(DC.terms.modified,
+      toLiteral(stats.mtime.toISOString(), XSD.terms.dateTime),
+      SOLID_META.terms.ResponseMetadata);
+    metadata.add(POSIX.terms.mtime,
+      toLiteral(Math.floor(stats.mtime.getTime() / 1000), XSD.terms.integer),
+      SOLID_META.terms.ResponseMetadata);
     if (!stats.isDirectory()) {
-      metadata.add(POSIX.terms.size, toLiteral(stats.size, XSD.terms.integer));
+      metadata.add(POSIX.terms.size, toLiteral(stats.size, XSD.terms.integer), SOLID_META.terms.ResponseMetadata);
     }
   }
 
diff --git a/src/util/TermUtil.ts b/src/util/TermUtil.ts
index 6308e6efc..3a55b789d 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<T extends Term>(subject: T): T;
-export function toSubjectTerm<T extends Term>(subject: T | string): T | NamedNode;
-export function toSubjectTerm(subject: Term | string): Term {
+export function toNamedTerm(subject: string): NamedNode;
+export function toNamedTerm<T extends Term>(subject: T): T;
+export function toNamedTerm<T extends Term>(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/src/util/Vocabularies.ts b/src/util/Vocabularies.ts
index 07efce64b..c37cd0207 100644
--- a/src/util/Vocabularies.ts
+++ b/src/util/Vocabularies.ts
@@ -128,6 +128,11 @@ export const SOLID_HTTP = createUriAndTermNamespace('urn:npm:solid:community-ser
   'slug',
 );
 
+export const SOLID_META = createUriAndTermNamespace('urn:npm:solid:community-server:meta:',
+  // This identifier is used as graph for all metadata that is generated on the fly and should not be stored
+  'ResponseMetadata',
+);
+
 export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/',
   'preferredNamespacePrefix',
 );


Test patch:
diff --git a/test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts b/test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts
index e4b115be5..a2d13fd39 100644
--- a/test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts
+++ b/test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts
@@ -2,6 +2,7 @@ import type { AuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/
 import { LinkMetadataGenerator } from '../../../../src/ldp/auxiliary/LinkMetadataGenerator';
 import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
+import { SOLID_META } from '../../../../src/util/Vocabularies';
 
 describe('A LinkMetadataGenerator', (): void => {
   const link = 'link';
@@ -35,5 +36,6 @@ describe('A LinkMetadataGenerator', (): void => {
     await expect(generator.handle(metadata)).resolves.toBeUndefined();
     expect(metadata.quads()).toHaveLength(1);
     expect(metadata.get(link)?.value).toBe(auxiliaryId.path);
+    expect(metadata.getAll(link, SOLID_META.terms.ResponseMetadata)).toHaveLength(1);
   });
 });
diff --git a/test/unit/ldp/representation/RepresentationMetadata.test.ts b/test/unit/ldp/representation/RepresentationMetadata.test.ts
index 54faed1ee..15221dc4a 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<void> => {
-      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<void> => {
@@ -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<void> => {
+      const newQuad = quad(namedNode('random'), namedNode('new'), literal('triple'));
+      metadata.addQuad('random', 'new', 'triple');
+      expect(metadata.quads()).toBeRdfIsomorphic([ ...inputQuads, newQuad ]);
+    });
+
+    it('can add a quad with a graph.', async(): Promise<void> => {
+      const newQuad = quad(namedNode('random'), namedNode('new'), literal('triple'), namedNode('graph'));
+      metadata.addQuad('random', 'new', 'triple', 'graph');
+      expect(metadata.quads()).toBeRdfIsomorphic([ ...inputQuads, newQuad ]);
+    });
+
     it('can add quads.', async(): Promise<void> => {
       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<void> => {
+      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<void> => {
+      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<void> => {
       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<void> => {
-      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<void> => {
-      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<void> => {
-      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<void> => {
       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<void> => {
+      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<void> => {
-      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<void> => {
+      expect(metadata.getAll(namedNode('has'), defaultGraph())).toEqualRdfTermArray(
+        [ literal('data'), literal('moreData') ],
+      );
     });
 
     it('can get the single value for a predicate.', async(): Promise<void> => {
diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts
index 9b8f2d3b8..e9c909134 100644
--- a/test/unit/storage/DataAccessorBasedStore.test.ts
+++ b/test/unit/storage/DataAccessorBasedStore.test.ts
@@ -21,9 +21,8 @@ import type { Guarded } from '../../../src/util/GuardedStream';
 import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
 import { trimTrailingSlashes } from '../../../src/util/PathUtil';
 import { guardedStreamFrom } from '../../../src/util/StreamUtil';
-import { CONTENT_TYPE, SOLID_HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies';
-import quad = DataFactory.quad;
-import namedNode = DataFactory.namedNode;
+import { CONTENT_TYPE, SOLID_HTTP, LDP, PIM, RDF, SOLID_META } from '../../../src/util/Vocabularies';
+const { namedNode, quad } = DataFactory;
 
 class SimpleDataAccessor implements DataAccessor {
   public readonly data: Record<string, Representation> = {};
@@ -379,8 +378,8 @@ describe('A DataAccessorBasedStore', (): void => {
     it('can write resources.', async(): Promise<void> => {
       const resourceID = { path: `${root}resource` };
       await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
-        { path: 'http://test.com/' },
-        { path: 'http://test.com/resource' },
+        { path: root },
+        { path: `${root}resource` },
       ]);
       await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
     });
@@ -393,13 +392,26 @@ describe('A DataAccessorBasedStore', (): void => {
       representation.metadata.contentType = 'text/turtle';
       representation.data = guardedStreamFrom([ `<${`${root}resource/`}> a <coolContainer>.` ]);
       await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
-        { path: `${root}` },
+        { path: root },
         { path: `${root}container/` },
       ]);
       expect(accessor.data[resourceID.path]).toBeTruthy();
       expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
     });
 
+    it('does not write generated metadata.', async(): Promise<void> => {
+      const resourceID = { path: `${root}resource` };
+      representation.metadata.add('notGen', 'value');
+      representation.metadata.add('gen', 'value', SOLID_META.terms.ResponseMetadata);
+      await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([
+        { path: root },
+        { path: `${root}resource` },
+      ]);
+      await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]);
+      expect(accessor.data[resourceID.path].metadata.get('notGen')?.value).toBe('value');
+      expect(accessor.data[resourceID.path].metadata.get('gen')).toBeUndefined();
+    });
+
     it('can write resources even if root does not exist.', async(): Promise<void> => {
       // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
       delete accessor.data[root];
diff --git a/test/unit/storage/accessors/FileDataAccessor.test.ts b/test/unit/storage/accessors/FileDataAccessor.test.ts
index bc5858058..da3396683 100644
--- a/test/unit/storage/accessors/FileDataAccessor.test.ts
+++ b/test/unit/storage/accessors/FileDataAccessor.test.ts
@@ -11,9 +11,10 @@ import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError
 import type { SystemError } from '../../../../src/util/errors/SystemError';
 import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
 import type { Guarded } from '../../../../src/util/GuardedStream';
+import { isContainerPath } from '../../../../src/util/PathUtil';
 import { guardedStreamFrom, readableToString } from '../../../../src/util/StreamUtil';
 import { toLiteral } from '../../../../src/util/TermUtil';
-import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../../../src/util/Vocabularies';
+import { CONTENT_TYPE, DC, LDP, POSIX, RDF, SOLID_META, XSD } from '../../../../src/util/Vocabularies';
 import { mockFs } from '../../../util/Util';
 
 jest.mock('fs');
@@ -102,6 +103,7 @@ describe('A FileDataAccessor', (): void => {
       expect(metadata.get(POSIX.size)).toEqualRdfTerm(toLiteral('data'.length, XSD.terms.integer));
       expect(metadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime));
       expect(metadata.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000), XSD.terms.integer));
+      expect(metadata.quads(null, null, null, SOLID_META.terms.ResponseMetadata)).toHaveLength(3);
     });
 
     it('does not generate size metadata for a container.', async(): Promise<void> => {
@@ -121,6 +123,7 @@ describe('A FileDataAccessor', (): void => {
       expect(metadata.get(POSIX.size)).toBeUndefined();
       expect(metadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime));
       expect(metadata.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000), XSD.terms.integer));
+      expect(metadata.quads(null, null, null, SOLID_META.terms.ResponseMetadata)).toHaveLength(2);
     });
 
     it('generates metadata for container child resources.', async(): Promise<void> => {
@@ -136,6 +139,8 @@ describe('A FileDataAccessor', (): void => {
         expect(child.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime));
         expect(child.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000),
           XSD.terms.integer));
+        expect(child.quads(null, null, null, SOLID_META.terms.ResponseMetadata))
+          .toHaveLength(isContainerPath(child.identifier.value) ? 2 : 3);
       }
     });
 
diff --git a/test/unit/util/TermUtil.test.ts b/test/unit/util/TermUtil.test.ts
index 5c300d8ac..f29d34699 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<void> => {
       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<void> => {
-      expect(toSubjectTerm('nn')).toEqualRdfTerm(namedNode('nn'));
+      expect(toNamedTerm('nn')).toEqualRdfTerm(namedNode('nn'));
     });
   });