diff --git a/packages/solid/src/SolidLdoDataset.ts b/packages/solid/src/SolidLdoDataset.ts index 010f986..e3b6a34 100644 --- a/packages/solid/src/SolidLdoDataset.ts +++ b/packages/solid/src/SolidLdoDataset.ts @@ -1,4 +1,5 @@ -import { LdoDataset } from "@ldo/ldo"; +import type { LdoBase, ShapeType } from "@ldo/ldo"; +import { LdoDataset, startTransaction } from "@ldo/ldo"; import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types"; import type { Container } from "./resource/Container"; import type { Leaf } from "./resource/Leaf"; @@ -7,6 +8,8 @@ import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; import type { ContainerUri, LeafUri } from "./util/uriTypes"; import { SolidLdoTransactionDataset } from "./SolidLdoTransactionDataset"; import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset"; +import type { SubjectNode } from "@ldo/rdf-utils"; +import type { Resource } from "./resource/Resource"; /** * A SolidLdoDataset has all the functionality of an LdoDataset with the added @@ -87,4 +90,27 @@ export class SolidLdoDataset extends LdoDataset { this.transactionDatasetFactory, ); } + + /** + * Shorthand for solidLdoDataset + * .usingType(shapeType) + * .write(...resources.map((r) => r.uri)) + * .fromSubject(subject); + * @param shapeType - The shapetype to represent the data + * @param subject - A subject URI + * @param resources - The resources changes to should written to + */ + createData( + shapeType: ShapeType, + subject: string | SubjectNode, + resource: Resource, + ...additionalResources: Resource[] + ): Type { + const resources = [resource, ...additionalResources]; + const linkedDataObject = this.usingType(shapeType) + .write(...resources.map((r) => r.uri)) + .fromSubject(subject); + startTransaction(linkedDataObject); + return linkedDataObject; + } } diff --git a/packages/solid/src/SolidLdoTransactionDataset.ts b/packages/solid/src/SolidLdoTransactionDataset.ts index 9c2665d..212bfba 100644 --- a/packages/solid/src/SolidLdoTransactionDataset.ts +++ b/packages/solid/src/SolidLdoTransactionDataset.ts @@ -95,7 +95,7 @@ export class SolidLdoTransactionDataset async ([graph, datasetChanges]) => { if (graph.termType === "DefaultGraph") { // Undefined means that this is the default graph - this.bulk(datasetChanges); + this.parentDataset.bulk(datasetChanges); return [ graph, datasetChanges, diff --git a/packages/solid/test/Integration.test.ts b/packages/solid/test/Integration.test.ts index 0e74e8c..fcc759b 100644 --- a/packages/solid/test/Integration.test.ts +++ b/packages/solid/test/Integration.test.ts @@ -10,6 +10,7 @@ import type { import { changeData, commitData, createSolidLdoDataset } from "../src"; import { ROOT_CONTAINER, + WEB_ID, createApp, getAuthenticatedFetch, } from "./solidServer.helper"; @@ -20,7 +21,6 @@ import { defaultGraph, } from "@rdfjs/data-model"; import type { CreateSuccess } from "../src/requester/results/success/CreateSuccess"; -import { createDataset } from "@ldo/dataset"; import type { AggregateSuccess } from "../src/requester/results/success/SuccessResult"; import type { UpdateDefaultGraphSuccess, @@ -89,6 +89,12 @@ const TEST_CONTAINER_TTL = `@prefix dc: . posix:size 522. posix:mtime 1697810234; posix:size 10.`; +const TEST_CONTAINER_ACL_URI = `${TEST_CONTAINER_URI}.acl`; +const TEST_CONTAINER_ACL = `<#b30e3fd1-b5a8-4763-ad9d-e95de9cf7933> a ; + <${TEST_CONTAINER_URI}>; + <${TEST_CONTAINER_URI}>; + , , , ; + <${WEB_ID}>.`; async function testRequestLoads( request: () => Promise, @@ -164,6 +170,13 @@ describe("Integration", () => { slug: TEST_CONTAINER_SLUG, }, }); + await authFetch(TEST_CONTAINER_ACL_URI, { + method: "PUT", + headers: { + "content-type": "text/turtle", + }, + body: TEST_CONTAINER_ACL, + }); await Promise.all([ authFetch(TEST_CONTAINER_URI, { method: "POST", @@ -328,8 +341,8 @@ describe("Integration", () => { expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("noncompliantPodError"); - expect(result.message).toBe( - "Response from https://solidweb.me/jackson3/test_ldo/sample2.ttl is not compliant with the Solid Specification: Resource requests must return a content-type header.", + expect(result.message).toMatch( + /\Response from .* is not compliant with the Solid Specification: Resource requests must return a content-type header\./, ); }); @@ -349,8 +362,8 @@ describe("Integration", () => { expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("noncompliantPodError"); - expect(result.message).toBe( - 'Response from https://solidweb.me/jackson3/test_ldo/sample2.ttl is not compliant with the Solid Specification: Request returned noncompliant turtle: Unexpected "Error" on line 1.', + expect(result.message).toMatch( + /\Response from .* is not compliant with the Solid Specification: Request returned noncompliant turtle: Unexpected "Error" on line 1\./, ); }); @@ -384,8 +397,8 @@ describe("Integration", () => { expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("noncompliantPodError"); - expect(result.message).toBe( - "Response from https://solidweb.me/jackson3/test_ldo/ is not compliant with the Solid Specification: No link header present in request.", + expect(result.message).toMatch( + /\Response from .* is not compliant with the Solid Specification: No link header present in request\./, ); }); @@ -404,26 +417,22 @@ describe("Integration", () => { resource.read(), ]); + expect(fetchMock).toHaveBeenCalledTimes(1); expect(result.type).toBe("dataReadSuccess"); expect(result1.type).toBe("dataReadSuccess"); - expect(fetchMock).toHaveBeenCalledTimes(1); }); it("batches the read request when a read request is in queue", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const [, result, result1] = await Promise.all([ - resource.update({ - added: createDataset([ - createQuad(namedNode("a"), namedNode("b"), namedNode("c")), - ]), - }), + resource.createAndOverwrite(), resource.read(), resource.read(), ]); + expect(fetchMock).toHaveBeenCalledTimes(3); expect(result.type).toBe("dataReadSuccess"); expect(result1.type).toBe("dataReadSuccess"); - expect(fetchMock).toHaveBeenCalledTimes(2); }); }); @@ -544,8 +553,8 @@ describe("Integration", () => { expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("noncompliantPodError"); - expect(result.message).toBe( - "Response from https://solidweb.me/jackson3/test_ldo/ is not compliant with the Solid Specification: No link header present in request.", + expect(result.message).toMatch( + /\Response from .* is not compliant with the Solid Specification: No link header present in request\./, ); }); @@ -1419,16 +1428,55 @@ describe("Integration", () => { * =========================================================================== */ describe("methods", () => { - it("uses changeData to start a transaction", () => { - const resource = solidLdoDataset.getResource( - "https://example.com/resource.ttl", + it("creates a data object for a specific subject", async () => { + const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); + const post = solidLdoDataset.createData( + PostShShapeType, + "https://example.com/subject", + resource, ); + post.type = { "@id": "CreativeWork" }; + expect(post.type["@id"]).toBe("CreativeWork"); + const result = await commitData(post); + expect(result.type).toBe("aggregateSuccess"); + expect( + solidLdoDataset.has( + createQuad( + namedNode("https://example.com/subject"), + namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + namedNode("http://schema.org/CreativeWork"), + namedNode(SAMPLE_DATA_URI), + ), + ), + ).toBe(true); + }); + + it("handles an error when committing data", async () => { + fetchMock.mockResolvedValueOnce( + new Response(SAMPLE_DATA_URI, { + status: 500, + }), + ); + const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); + const post = solidLdoDataset.createData( + PostShShapeType, + "https://example.com/subject", + resource, + ); + post.type = { "@id": "CreativeWork" }; + expect(post.type["@id"]).toBe("CreativeWork"); + const result = await commitData(post); + expect(result.isError).toBe(true); + }); + + it("uses changeData to start a transaction", async () => { + const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); solidLdoDataset.add( createQuad( namedNode("https://example.com/subject"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), namedNode("http://schema.org/CreativeWork"), - namedNode("https://example.com/resource.ttl"), + namedNode(SAMPLE_DATA_URI), ), ); const post = solidLdoDataset @@ -1437,14 +1485,15 @@ describe("Integration", () => { const cPost = changeData(post, resource); cPost.type = { "@id": "SocialMediaPosting" }; expect(cPost.type["@id"]).toBe("SocialMediaPosting"); - commitData(cPost); + const result = await commitData(cPost); + expect(result.isError).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode("https://example.com/subject"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), namedNode("http://schema.org/SocialMediaPosting"), - namedNode("https://example.com/resource.ttl"), + namedNode(SAMPLE_DATA_URI), ), ), ).toBe(true); diff --git a/packages/solid/test/solidServer.helper.ts b/packages/solid/test/solidServer.helper.ts index 01ec04d..3447f96 100644 --- a/packages/solid/test/solidServer.helper.ts +++ b/packages/solid/test/solidServer.helper.ts @@ -21,8 +21,10 @@ const config = [ ]; export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/"; -export const ROOT_ROUTE = process.env.ROOT_CONTAINER || "example/"; +export const ROOT_ROUTE = process.env.ROOT_CONTAINER || ""; export const ROOT_CONTAINER = `${SERVER_DOMAIN}${ROOT_ROUTE}`; +export const WEB_ID = + process.env.WEB_ID || `${SERVER_DOMAIN}example/profile/card#me`; // Use an increased timeout, since the CSS server takes too much setup time. jest.setTimeout(40_000);