Repository: solid/community-server
Pull request #647 (2021-03-02 15:46:13)

feat: added cloneRepresentation function to ResourceUtil


Patch:
diff --git a/package-lock.json b/package-lock.json
index 5b51486a4..a55d69599 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1067,9 +1067,9 @@
       }
     },
     "@solid/identity-token-verifier": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/@solid/identity-token-verifier/-/identity-token-verifier-0.6.1.tgz",
-      "integrity": "sha512-zKs2a32Ib6myG5I1D+qKv9wJznDkVIYQ43lUSbW7UlqXu/XXEiBg2SbGn3I02QJxB/S0505kdroLqTY29leKIw==",
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/@solid/identity-token-verifier/-/identity-token-verifier-0.6.2.tgz",
+      "integrity": "sha512-jEDNhFmCBQZtHCwnrV8b+dtwSx8zm3aeqtVLdzA5jbXIWrLZSg33SU48ApsRWk4tSo7sOPDuXitD2kSLsj7S2w==",
       "requires": {
         "cross-fetch": "^3.0.6",
         "jose": "^3.6.2",
@@ -6081,9 +6081,9 @@
       "dev": true
     },
     "jose": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/jose/-/jose-3.6.2.tgz",
-      "integrity": "sha512-JzqHr6UqydWv25HwLzRdBJgMetiPkDBE/WJSyJjhNwN3+GmcHu4yoUgRle0NNViCdfAYMf9DuOG8o+mUW/DXdg=="
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-3.7.0.tgz",
+      "integrity": "sha512-9vAWSk4zmWH60zbubOZ5Rju3bO6rr5eOUjxuU8snB17Fl2fc7ytFQ6AjtDk8vLzHvecNMj2F4yFkAyurJTpuww=="
     },
     "js-tokens": {
       "version": "4.0.0",
diff --git a/package.json b/package.json
index 16bdcbf49..abe07b0ed 100644
--- a/package.json
+++ b/package.json
@@ -75,7 +75,7 @@
   ],
   "dependencies": {
     "@rdfjs/data-model": "^1.2.0",
-    "@solid/identity-token-verifier": "^0.6.1",
+    "@solid/identity-token-verifier": "^0.6.2",
     "@types/arrayify-stream": "^1.0.0",
     "@types/async-lock": "^1.1.2",
     "@types/cors": "^2.8.9",
diff --git a/src/ldp/auxiliary/RdfValidator.ts b/src/ldp/auxiliary/RdfValidator.ts
index 663e921e0..a9fc56eea 100644
--- a/src/ldp/auxiliary/RdfValidator.ts
+++ b/src/ldp/auxiliary/RdfValidator.ts
@@ -1,8 +1,7 @@
 import arrayifyStream from 'arrayify-stream';
 import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
 import { INTERNAL_QUADS } from '../../util/ContentTypes';
-import { guardedStreamFrom } from '../../util/StreamUtil';
-import { BasicRepresentation } from '../representation/BasicRepresentation';
+import { cloneRepresentation } from '../../util/ResourceUtil';
 import type { Representation } from '../representation/Representation';
 import { Validator } from './Validator';
 
@@ -23,23 +22,22 @@ export class RdfValidator extends Validator {
     if (representation.metadata.contentType === INTERNAL_QUADS) {
       return;
     }
-
-    // eslint-disable-next-line unicorn/expiring-todo-comments
-    // TODO: Everything below should be part of a utility cloneRepresentation function.
-
     const identifier = { path: representation.metadata.identifier.value };
-
-    // Read data in memory first so it does not get lost
-    const data = await arrayifyStream(representation.data);
     const preferences = { type: { [INTERNAL_QUADS]: 1 }};
-
-    // Creating new representation since converter might edit metadata
-    const tempRepresentation = new BasicRepresentation(data, identifier, representation.metadata.contentType);
-    const result = await this.converter.handleSafe({ identifier, representation: tempRepresentation, preferences });
+    let result;
+    try {
+      // Creating new representation since converter might edit metadata
+      const tempRepresentation = await cloneRepresentation(representation);
+      result = await this.converter.handleSafe({
+        identifier,
+        representation: tempRepresentation,
+        preferences,
+      });
+    } catch (error: unknown) {
+      representation.data.destroy();
+      throw error;
+    }
     // Drain stream to make sure data was parsed correctly
     await arrayifyStream(result.data);
-
-    // Stream has been drained so need to create new stream
-    representation.data = guardedStreamFrom(data);
   }
 }
diff --git a/src/util/ResourceUtil.ts b/src/util/ResourceUtil.ts
index 6c6482b67..b93769c71 100644
--- a/src/util/ResourceUtil.ts
+++ b/src/util/ResourceUtil.ts
@@ -1,7 +1,12 @@
+import arrayifyStream from 'arrayify-stream';
 import { DataFactory } from 'n3';
 import type { NamedNode, Quad } from 'rdf-js';
+import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
+import type { Representation } from '../ldp/representation/Representation';
 import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
 import { pushQuad } from './QuadUtil';
+import { guardedStreamFrom } from './StreamUtil';
+
 import { LDP, RDF } from './Vocabularies';
 
 /**
@@ -33,3 +38,21 @@ export function generateContainmentQuads(containerURI: NamedNode, childURIs: str
   return new RepresentationMetadata(containerURI,
     { [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads();
 }
+
+/**
+ * Helper function to clone a representation, the original representation can still be used.
+ * This function loads the entire stream in memory.
+ * @param representation - The representation to clone.
+ *
+ * @returns The cloned representation.
+ */
+export async function cloneRepresentation(representation: Representation): Promise<BasicRepresentation> {
+  const data = await arrayifyStream(representation.data);
+  const result = new BasicRepresentation(
+    data,
+    new RepresentationMetadata(representation.metadata),
+    representation.binary,
+  );
+  representation.data = guardedStreamFrom(data);
+  return result;
+}


Test patch:
diff --git a/test/unit/ldp/auxiliary/RdfValidator.test.ts b/test/unit/ldp/auxiliary/RdfValidator.test.ts
index 8216e75a2..8745d1c8a 100644
--- a/test/unit/ldp/auxiliary/RdfValidator.test.ts
+++ b/test/unit/ldp/auxiliary/RdfValidator.test.ts
@@ -39,6 +39,6 @@ describe('An RdfValidator', (): void => {
     const representation = new BasicRepresentation('data', 'content-type');
     await expect(validator.handle(representation)).rejects.toThrow('bad data!');
     // Make sure the data on the readable has not been reset
-    expect(representation.data.readableEnded).toBe(true);
+    expect(representation.data.destroyed).toBe(true);
   });
 });
diff --git a/test/unit/util/ResourceUtil.test.ts b/test/unit/util/ResourceUtil.test.ts
new file mode 100644
index 000000000..67dfa6846
--- /dev/null
+++ b/test/unit/util/ResourceUtil.test.ts
@@ -0,0 +1,27 @@
+import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
+import type { Representation } from '../../../src/ldp/representation/Representation';
+import * as resourceUtils from '../../../src/util/ResourceUtil';
+import 'jest-rdf';
+
+describe('ResourceUtil', (): void => {
+  let representation: Representation;
+
+  beforeEach(async(): Promise<void> => {
+    representation = new BasicRepresentation('data', 'metadata');
+  });
+
+  describe('cloneRepresentation', (): void => {
+    it('returns a clone of the passed representation.', async(): Promise<void> => {
+      const res = await resourceUtils.cloneRepresentation(representation);
+      expect(res.binary).toBe(representation.binary);
+      expect(res.metadata.identifier).toBe(representation.metadata.identifier);
+      expect(res.metadata.contentType).toBe(representation.metadata.contentType);
+    });
+
+    it('ensures that original representation does not update when the clone is updated.', async(): Promise<void> => {
+      const res = await resourceUtils.cloneRepresentation(representation);
+      res.metadata.contentType = 'typetype';
+      expect(representation.metadata.contentType).not.toBe(res.metadata.contentType);
+    });
+  });
+});