Repository: solid/community-server
Pull request #416 (2020-12-10 16:25:40)

Make RepresentationMetadata accept a ResourceIdentifier
Closes https://github.com/solid/community-server/issues/388


Patch:
diff --git a/src/init/AclInitializer.ts b/src/init/AclInitializer.ts
index 18199ee18..69cc24da6 100644
--- a/src/init/AclInitializer.ts
+++ b/src/init/AclInitializer.ts
@@ -66,7 +66,7 @@ export class AclInitializer extends Initializer {
     acl:mode        acl:Control;
     acl:accessTo    <${this.baseUrl}>;
     acl:default     <${this.baseUrl}>.`;
-    const metadata = new RepresentationMetadata(rootAcl.path, { [CONTENT_TYPE]: TEXT_TURTLE });
+    const metadata = new RepresentationMetadata(rootAcl, { [CONTENT_TYPE]: TEXT_TURTLE });
     this.logger.debug(`Installing root ACL document at ${rootAcl.path}`);
     await this.store.setRepresentation(
       rootAcl,
diff --git a/src/ldp/representation/RepresentationMetadata.ts b/src/ldp/representation/RepresentationMetadata.ts
index 058c138ff..bb6d8a529 100644
--- a/src/ldp/representation/RepresentationMetadata.ts
+++ b/src/ldp/representation/RepresentationMetadata.ts
@@ -2,7 +2,10 @@ import { DataFactory, Store } from 'n3';
 import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';
 import { getLoggerFor } from '../../logging/LogUtil';
 import { toObjectTerm, toNamedNode, isTerm } from '../../util/UriUtil';
+import type { ResourceIdentifier } from './ResourceIdentifier';
+import { isResourceIdentifier } from './ResourceIdentifier';
 
+export type MetadataIdentifier = ResourceIdentifier | NamedNode | BlankNode;
 export type MetadataOverrideValue = NamedNode | Literal | string | (NamedNode | Literal | string)[];
 
 /**
@@ -23,7 +26,7 @@ export class RepresentationMetadata {
    *
    * `@ignored` tag is necessary for Components-Generator.js
    */
-  public constructor(identifier?: NamedNode | BlankNode | string, overrides?: Record<string, MetadataOverrideValue>);
+  public constructor(identifier?: MetadataIdentifier, overrides?: Record<string, MetadataOverrideValue>);
 
   /**
    * @param metadata - Starts as a copy of the input metadata.
@@ -38,12 +41,12 @@ export class RepresentationMetadata {
   public constructor(overrides?: Record<string, MetadataOverrideValue>);
 
   public constructor(
-    input?: NamedNode | BlankNode | string | RepresentationMetadata | Record<string, MetadataOverrideValue>,
+    input?: MetadataIdentifier | RepresentationMetadata | Record<string, MetadataOverrideValue>,
     overrides?: Record<string, MetadataOverrideValue>,
   ) {
     this.store = new Store();
-    if (typeof input === 'string') {
-      this.id = DataFactory.namedNode(input);
+    if (isResourceIdentifier(input)) {
+      this.id = DataFactory.namedNode(input.path);
     } else if (isTerm(input)) {
       this.id = input;
     } else if (input instanceof RepresentationMetadata) {
diff --git a/src/ldp/representation/ResourceIdentifier.ts b/src/ldp/representation/ResourceIdentifier.ts
index cb8cf16ac..ff0c7b9a4 100644
--- a/src/ldp/representation/ResourceIdentifier.ts
+++ b/src/ldp/representation/ResourceIdentifier.ts
@@ -7,3 +7,10 @@ export interface ResourceIdentifier {
    */
   path: string;
 }
+
+/**
+ * Determines whether the object is a `ResourceIdentifier`.
+ */
+export const isResourceIdentifier = function(object: any): object is ResourceIdentifier {
+  return object && (typeof object.path === 'string');
+};
diff --git a/src/pods/generate/TemplatedResourcesGenerator.ts b/src/pods/generate/TemplatedResourcesGenerator.ts
index c3c0ae8d0..ae16fd218 100644
--- a/src/pods/generate/TemplatedResourcesGenerator.ts
+++ b/src/pods/generate/TemplatedResourcesGenerator.ts
@@ -51,7 +51,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
       representation: {
         binary: true,
         data: guardedStreamFrom([]),
-        metadata: new RepresentationMetadata(link.identifier.path),
+        metadata: new RepresentationMetadata(link.identifier),
       },
     };
 
@@ -74,7 +74,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
   private async generateDocument(filePath: string, mapper: FileIdentifierMapper, options: Dict<string>):
   Promise<Resource> {
     const link = await mapper.mapFilePathToUrl(filePath, false);
-    const metadata = new RepresentationMetadata(link.identifier.path);
+    const metadata = new RepresentationMetadata(link.identifier);
     metadata.contentType = link.contentType;
 
     const raw = await fsPromises.readFile(filePath, 'utf8');
diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts
index 04a3a7382..48611d4ce 100644
--- a/src/storage/DataAccessorBasedStore.ts
+++ b/src/storage/DataAccessorBasedStore.ts
@@ -370,7 +370,7 @@ export class DataAccessorBasedStore implements ResourceStore {
     return {
       binary: true,
       data: guardedStreamFrom([]),
-      metadata: new RepresentationMetadata(container.path),
+      metadata: new RepresentationMetadata(container),
     };
   }
 }
diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts
index ea994347d..9bd71978d 100644
--- a/src/storage/accessors/FileDataAccessor.ts
+++ b/src/storage/accessors/FileDataAccessor.ts
@@ -246,7 +246,7 @@ export class FileDataAccessor implements DataAccessor {
    */
   private async getBaseMetadata(link: ResourceLink, stats: Stats, isContainer: boolean):
   Promise<RepresentationMetadata> {
-    const metadata = new RepresentationMetadata(link.identifier.path)
+    const metadata = new RepresentationMetadata(link.identifier)
       .addQuads(await this.getRawMetadata(link.identifier));
     metadata.addQuads(generateResourceQuads(metadata.identifier as NamedNode, isContainer));
     metadata.addQuads(this.generatePosixQuads(metadata.identifier as NamedNode, stats));
diff --git a/src/storage/accessors/InMemoryDataAccessor.ts b/src/storage/accessors/InMemoryDataAccessor.ts
index bf7a9998f..33c38aba6 100644
--- a/src/storage/accessors/InMemoryDataAccessor.ts
+++ b/src/storage/accessors/InMemoryDataAccessor.ts
@@ -28,7 +28,7 @@ export class InMemoryDataAccessor implements DataAccessor {
   public constructor(base: string) {
     this.base = ensureTrailingSlash(base);
 
-    const metadata = new RepresentationMetadata(this.base);
+    const metadata = new RepresentationMetadata({ path: this.base });
     metadata.addQuads(generateResourceQuads(DataFactory.namedNode(this.base), true));
     this.store = { entries: {}, metadata };
   }
diff --git a/src/storage/accessors/SparqlDataAccessor.ts b/src/storage/accessors/SparqlDataAccessor.ts
index dc6a7904a..1819e4190 100644
--- a/src/storage/accessors/SparqlDataAccessor.ts
+++ b/src/storage/accessors/SparqlDataAccessor.ts
@@ -94,7 +94,7 @@ export class SparqlDataAccessor implements DataAccessor {
       throw new NotFoundHttpError();
     }
 
-    const metadata = new RepresentationMetadata(identifier.path).addQuads(quads);
+    const metadata = new RepresentationMetadata(identifier).addQuads(quads);
     if (!isContainerIdentifier(identifier)) {
       metadata.contentType = INTERNAL_QUADS;
     }
diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts
index 3fecf133a..39df46886 100644
--- a/src/storage/patch/SparqlUpdatePatchHandler.ts
+++ b/src/storage/patch/SparqlUpdatePatchHandler.ts
@@ -108,7 +108,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
     this.logger.debug(`${store.size} quads will be stored to ${identifier.path}.`);
 
     // Write the result
-    const metadata = new RepresentationMetadata(identifier.path, { [CONTENT_TYPE]: INTERNAL_QUADS });
+    const metadata = new RepresentationMetadata(identifier, { [CONTENT_TYPE]: INTERNAL_QUADS });
     const representation: Representation = {
       binary: false,
       data: guardStream(store.match() as Readable),


Test patch:
diff --git a/test/unit/ldp/representation/RepresentationMetadata.test.ts b/test/unit/ldp/representation/RepresentationMetadata.test.ts
index 83d82f297..03d53a6b6 100644
--- a/test/unit/ldp/representation/RepresentationMetadata.test.ts
+++ b/test/unit/ldp/representation/RepresentationMetadata.test.ts
@@ -28,12 +28,12 @@ describe('A RepresentationMetadata', (): void => {
     });
 
     it('converts identifier strings to named nodes.', async(): Promise<void> => {
-      metadata = new RepresentationMetadata('identifier');
+      metadata = new RepresentationMetadata({ path: 'identifier' });
       expect(metadata.identifier).toEqualRdfTerm(namedNode('identifier'));
     });
 
     it('copies an other metadata object.', async(): Promise<void> => {
-      const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' });
+      const other = new RepresentationMetadata({ path: 'otherId' }, { 'test:pred': 'objVal' });
       metadata = new RepresentationMetadata(other);
       expect(metadata.identifier).toEqualRdfTerm(namedNode('otherId'));
       expect(metadata.quads()).toBeRdfIsomorphic([
@@ -59,7 +59,7 @@ describe('A RepresentationMetadata', (): void => {
     });
 
     it('can combine overrides with other metadata.', async(): Promise<void> => {
-      const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' });
+      const other = new RepresentationMetadata({ path: 'otherId' }, { 'test:pred': 'objVal' });
       metadata = new RepresentationMetadata(other, { 'test:pred': 'objVal2' });
       expect(metadata.quads()).toBeRdfIsomorphic([
         quad(namedNode('otherId'), namedNode('test:pred'), literal('objVal2')) ]);
@@ -101,7 +101,7 @@ describe('A RepresentationMetadata', (): void => {
     });
 
     it('updates its identifier when copying metadata.', async(): Promise<void> => {
-      const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' });
+      const other = new RepresentationMetadata({ path: 'otherId' }, { 'test:pred': 'objVal' });
       metadata.setMetadata(other);
 
       // `setMetadata` should have the same result as the following
diff --git a/test/unit/storage/accessors/FileDataAccessor.test.ts b/test/unit/storage/accessors/FileDataAccessor.test.ts
index 46c609871..4e87a6aaa 100644
--- a/test/unit/storage/accessors/FileDataAccessor.test.ts
+++ b/test/unit/storage/accessors/FileDataAccessor.test.ts
@@ -119,7 +119,7 @@ describe('A FileDataAccessor', (): void => {
 
       const childQuads = metadata.quads().filter((quad): boolean =>
         quad.subject.value === `${base}container/resource`);
-      const childMetadata = new RepresentationMetadata(`${base}container/resource`).addQuads(childQuads);
+      const childMetadata = new RepresentationMetadata({ path: `${base}container/resource` }).addQuads(childQuads);
       expect(childMetadata.get(RDF.type)?.value).toBe(LDP.Resource);
       expect(childMetadata.get(POSIX.size)).toEqualRdfTerm(toTypedLiteral('data'.length, XSD.integer));
       expect(childMetadata.get(DCTERMS.modified)).toEqualRdfTerm(toTypedLiteral(now.toISOString(), XSD.dateTime));
@@ -160,7 +160,8 @@ describe('A FileDataAccessor', (): void => {
     });
 
     it('writes metadata to the corresponding metadata file.', async(): Promise<void> => {
-      metadata = new RepresentationMetadata(`${base}res.ttl`, { [CONTENT_TYPE]: 'text/turtle', likes: 'apples' });
+      metadata = new RepresentationMetadata({ path: `${base}res.ttl` },
+        { [CONTENT_TYPE]: 'text/turtle', likes: 'apples' });
       await expect(accessor.writeDocument({ path: `${base}res.ttl` }, data, metadata)).resolves.toBeUndefined();
       expect(cache.data['res.ttl']).toBe('data');
       expect(cache.data['res.ttl.meta']).toMatch(`<${base}res.ttl> <likes> "apples".`);
@@ -266,21 +267,21 @@ describe('A FileDataAccessor', (): void => {
     });
 
     it('writes metadata to the corresponding metadata file.', async(): Promise<void> => {
-      metadata = new RepresentationMetadata(`${base}container/`, { likes: 'apples' });
+      metadata = new RepresentationMetadata({ path: `${base}container/` }, { likes: 'apples' });
       await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
       expect(cache.data.container).toEqual({ '.meta': expect.stringMatching(`<${base}container/> <likes> "apples".`) });
     });
 
     it('overwrites existing metadata.', async(): Promise<void> => {
       cache.data.container = { '.meta': `<${base}container/> <likes> "pears".` };
-      metadata = new RepresentationMetadata(`${base}container/`, { likes: 'apples' });
+      metadata = new RepresentationMetadata({ path: `${base}container/` }, { likes: 'apples' });
       await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
       expect(cache.data.container).toEqual({ '.meta': expect.stringMatching(`<${base}container/> <likes> "apples".`) });
     });
 
     it('does not write metadata that is stored by the file system.', async(): Promise<void> => {
       metadata = new RepresentationMetadata(
-        `${base}container/`,
+        { path: `${base}container/` },
         { [RDF.type]: [ toNamedNode(LDP.BasicContainer), toNamedNode(LDP.Resource) ]},
       );
       await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
diff --git a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts
index 588a8fda2..cfecdf0e3 100644
--- a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts
+++ b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts
@@ -86,9 +86,10 @@ describe('An InMemoryDataAccessor', (): void => {
     });
 
     it('adds stored metadata when requesting document metadata.', async(): Promise<void> => {
-      const inputMetadata = new RepresentationMetadata(`${base}resource`, { [RDF.type]: toNamedNode(LDP.Resource) });
-      await accessor.writeDocument({ path: `${base}resource` }, data, inputMetadata);
-      metadata = await accessor.getMetadata({ path: `${base}resource` });
+      const identifier = { path: `${base}resource` };
+      const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Resource) });
+      await accessor.writeDocument(identifier, data, inputMetadata);
+      metadata = await accessor.getMetadata(identifier);
       expect(metadata.identifier.value).toBe(`${base}resource`);
       const quads = metadata.quads();
       expect(quads).toHaveLength(1);
@@ -96,10 +97,11 @@ describe('An InMemoryDataAccessor', (): void => {
     });
 
     it('adds stored metadata when requesting container metadata.', async(): Promise<void> => {
-      const inputMetadata = new RepresentationMetadata(`${base}container/`, { [RDF.type]: toNamedNode(LDP.Container) });
-      await accessor.writeContainer({ path: `${base}container/` }, inputMetadata);
+      const identifier = { path: `${base}container/` };
+      const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) });
+      await accessor.writeContainer(identifier, inputMetadata);
 
-      metadata = await accessor.getMetadata({ path: `${base}container/` });
+      metadata = await accessor.getMetadata(identifier);
       expect(metadata.identifier.value).toBe(`${base}container/`);
       const quads = metadata.quads();
       expect(quads).toHaveLength(1);
@@ -107,8 +109,9 @@ describe('An InMemoryDataAccessor', (): void => {
     });
 
     it('can overwrite the metadata of an existing container without overwriting children.', async(): Promise<void> => {
-      const inputMetadata = new RepresentationMetadata(`${base}container/`, { [RDF.type]: toNamedNode(LDP.Container) });
-      await accessor.writeContainer({ path: `${base}container/` }, inputMetadata);
+      const identifier = { path: `${base}container/` };
+      const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) });
+      await accessor.writeContainer(identifier, inputMetadata);
       const resourceMetadata = new RepresentationMetadata();
       await accessor.writeDocument(
         { path: `${base}container/resource` }, data, resourceMetadata,
@@ -116,9 +119,9 @@ describe('An InMemoryDataAccessor', (): void => {
 
       const newMetadata = new RepresentationMetadata(inputMetadata);
       newMetadata.add(RDF.type, toNamedNode(LDP.BasicContainer));
-      await accessor.writeContainer({ path: `${base}container/` }, newMetadata);
+      await accessor.writeContainer(identifier, newMetadata);
 
-      metadata = await accessor.getMetadata({ path: `${base}container/` });
+      metadata = await accessor.getMetadata(identifier);
       expect(metadata.identifier.value).toBe(`${base}container/`);
       const quads = metadata.quads();
       expect(quads).toHaveLength(3);
diff --git a/test/unit/storage/accessors/SparqlDataAccessor.test.ts b/test/unit/storage/accessors/SparqlDataAccessor.test.ts
index 097936775..ceaccda88 100644
--- a/test/unit/storage/accessors/SparqlDataAccessor.test.ts
+++ b/test/unit/storage/accessors/SparqlDataAccessor.test.ts
@@ -152,7 +152,7 @@ describe('A SparqlDataAccessor', (): void => {
   });
 
   it('overwrites the metadata when writing a container and updates parent.', async(): Promise<void> => {
-    metadata = new RepresentationMetadata('http://test.com/container/',
+    metadata = new RepresentationMetadata({ path: 'http://test.com/container/' },
       { [RDF.type]: [ toNamedNode(LDP.Resource), toNamedNode(LDP.Container) ]});
     await expect(accessor.writeContainer({ path: 'http://test.com/container/' }, metadata)).resolves.toBeUndefined();
 
@@ -171,7 +171,7 @@ describe('A SparqlDataAccessor', (): void => {
   });
 
   it('overwrites the data and metadata when writing a resource and updates parent.', async(): Promise<void> => {
-    metadata = new RepresentationMetadata('http://test.com/container/resource',
+    metadata = new RepresentationMetadata({ path: 'http://test.com/container/resource' },
       { [RDF.type]: [ toNamedNode(LDP.Resource) ]});
     await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata))
       .resolves.toBeUndefined();
@@ -190,7 +190,7 @@ describe('A SparqlDataAccessor', (): void => {
   });
 
   it('overwrites the data and metadata when writing an empty resource.', async(): Promise<void> => {
-    metadata = new RepresentationMetadata('http://test.com/container/resource',
+    metadata = new RepresentationMetadata({ path: 'http://test.com/container/resource' },
       { [RDF.type]: [ toNamedNode(LDP.Resource) ]});
     const empty = guardedStreamFrom([]);
     await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, empty, metadata))
@@ -209,7 +209,7 @@ describe('A SparqlDataAccessor', (): void => {
   });
 
   it('removes all references when deleting a resource.', async(): Promise<void> => {
-    metadata = new RepresentationMetadata('http://test.com/container/',
+    metadata = new RepresentationMetadata({ path: 'http://test.com/container/' },
       { [RDF.type]: [ toNamedNode(LDP.Resource), toNamedNode(LDP.Container) ]});
     await expect(accessor.deleteResource({ path: 'http://test.com/container/' })).resolves.toBeUndefined();
 
@@ -246,14 +246,14 @@ describe('A SparqlDataAccessor', (): void => {
   });
 
   it('errors when the SPARQL endpoint fails during writing.', async(): Promise<void> => {
-    const path = 'http://test.com/container/';
-    metadata = new RepresentationMetadata(path);
+    const identifier = { path: 'http://test.com/container/' };
+    metadata = new RepresentationMetadata(identifier);
 
     updateError = 'error';
-    await expect(accessor.writeContainer({ path }, metadata)).rejects.toBe(updateError);
+    await expect(accessor.writeContainer(identifier, metadata)).rejects.toBe(updateError);
 
     updateError = new Error();
-    await expect(accessor.writeContainer({ path }, metadata)).rejects.toThrow(updateError);
+    await expect(accessor.writeContainer(identifier, metadata)).rejects.toThrow(updateError);
 
     updateError = undefined;
   });