Repository: solid/community-server
Pull request #398 (2020-12-07 14:08:49)

feat: Create new resources when patching
Was going through the Solid spec and noticed PATCH is supposed to do this.


Patch:
diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts
index 470dfe131..3fecf133a 100644
--- a/src/storage/patch/SparqlUpdatePatchHandler.ts
+++ b/src/storage/patch/SparqlUpdatePatchHandler.ts
@@ -10,6 +10,7 @@ import { RepresentationMetadata } from '../../ldp/representation/RepresentationM
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
 import { getLoggerFor } from '../../logging/LogUtil';
 import { INTERNAL_QUADS } from '../../util/ContentTypes';
+import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
 import { guardStream } from '../../util/GuardedStream';
 import type { ResourceLocker } from '../../util/locking/ResourceLocker';
@@ -82,15 +83,23 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
    */
   private async applyPatch(identifier: ResourceIdentifier, deletes: Algebra.Pattern[], inserts: Algebra.Pattern[]):
   Promise<void> {
-    // Read the quads of the current representation
-    const quads = await this.source.getRepresentation(identifier, { type: [{ value: INTERNAL_QUADS, weight: 1 }]});
     const store = new Store<BaseQuad>();
-    const importEmitter = store.import(quads.data);
-    await new Promise((resolve, reject): void => {
-      importEmitter.on('end', resolve);
-      importEmitter.on('error', reject);
-    });
-    this.logger.debug(`${store.size} quads in ${identifier.path}.`);
+    try {
+      // Read the quads of the current representation
+      const quads = await this.source.getRepresentation(identifier, { type: [{ value: INTERNAL_QUADS, weight: 1 }]});
+      const importEmitter = store.import(quads.data);
+      await new Promise((resolve, reject): void => {
+        importEmitter.on('end', resolve);
+        importEmitter.on('error', reject);
+      });
+      this.logger.debug(`${store.size} quads in ${identifier.path}.`);
+    } catch (error: unknown) {
+      // In case the resource does not exist yet we want to create it
+      if (!(error instanceof NotFoundHttpError)) {
+        throw error;
+      }
+      this.logger.debug(`Patching new resource ${identifier.path}.`);
+    }
 
     // Apply the patch
     store.removeQuads(deletes);


Test patch:
diff --git a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
index 8180edf44..aaa0f65f8 100644
--- a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
+++ b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts
@@ -8,6 +8,7 @@ import { RepresentationMetadata } from '../../../../src/ldp/representation/Repre
 import { SparqlUpdatePatchHandler } from '../../../../src/storage/patch/SparqlUpdatePatchHandler';
 import type { ResourceStore } from '../../../../src/storage/ResourceStore';
 import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
+import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
 import type { Lock } from '../../../../src/util/locking/Lock';
 import type { ResourceLocker } from '../../../../src/util/locking/ResourceLocker';
@@ -215,4 +216,22 @@ describe('A SparqlUpdatePatchHandler', (): void => {
     await expect(handler.handle(input)).rejects.toThrow('error');
     expect(order).toEqual([ 'acquire', 'getRepresentation', 'release' ]);
   });
+
+  it('creates a new resource if it does not exist yet.', async(): Promise<void> => {
+    // There is no initial data
+    startQuads = [];
+    source.getRepresentation = jest.fn((): any => {
+      order.push('getRepresentation');
+      throw new NotFoundHttpError();
+    });
+
+    await handler.handle({ identifier: { path: 'path' },
+      patch: { algebra: translate(
+        'INSERT DATA { <http://test.com/s1> <http://test.com/p1> <http://test.com/o1>. }',
+        { quads: true },
+      ) } as SparqlUpdatePatch });
+    expect(await basicChecks(startQuads.concat(
+      [ quad(namedNode('http://test.com/s1'), namedNode('http://test.com/p1'), namedNode('http://test.com/o1')) ],
+    ))).toBe(true);
+  });
 });