diff --git a/packages/connected-solid/src/requester/requests/createDataResource.ts b/packages/connected-solid/src/requester/requests/createDataResource.ts index 090e8f36..e031d513 100644 --- a/packages/connected-solid/src/requester/requests/createDataResource.ts +++ b/packages/connected-solid/src/requester/requests/createDataResource.ts @@ -5,8 +5,6 @@ import { UnexpectedResourceError } from "@ldo/connected"; import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; import { HttpErrorResult } from "../results/error/HttpErrorResult"; import { CreateSuccess } from "../results/success/CreateSuccess"; -import type { DeleteResultError } from "./deleteResource"; -import { deleteResource } from "./deleteResource"; import type { ReadContainerResult, ReadLeafResult, @@ -16,11 +14,8 @@ import { readResource } from "./readResource"; import type { DatasetRequestOptions } from "./requestOptions"; import type { SolidLeaf } from "../../resources/SolidLeaf"; import type { SolidContainer } from "../../resources/SolidContainer"; -import { - addResourceRdfToContainer, - getParentUri, - getSlug, -} from "../../util/rdfUtils"; +import { addResourceRdfToContainer } from "../../util/rdfUtils"; +import { NoncompliantPodError } from "../results/error/NoncompliantPodError"; /** * All possible return values when creating and overwriting a container @@ -56,8 +51,7 @@ export type LeafCreateIfAbsentResult = * All possible errors returned by creating and overwriting a resource */ export type CreateAndOverwriteResultErrors = - | DeleteResultError - | CreateErrors; + CreateErrors; /** * All possible errors returned by creating a resource if absent @@ -91,27 +85,27 @@ export function createDataResource( options?: DatasetRequestOptions, ): Promise; export function createDataResource( - resouce: SolidLeaf, + resource: SolidLeaf, overwrite: true, options?: DatasetRequestOptions, ): Promise; export function createDataResource( - resouce: SolidContainer, + resource: SolidContainer, overwrite?: false, options?: DatasetRequestOptions, ): Promise; export function createDataResource( - resouce: SolidLeaf, + resource: SolidLeaf, overwrite?: false, options?: DatasetRequestOptions, ): Promise; export function createDataResource( - resouce: SolidContainer, + resource: SolidContainer, overwrite?: boolean, options?: DatasetRequestOptions, ): Promise; export function createDataResource( - resouce: SolidLeaf, + resource: SolidLeaf, overwrite?: boolean, options?: DatasetRequestOptions, ): Promise; @@ -147,44 +141,46 @@ export async function createDataResource( > { try { const fetch = guaranteeFetch(options?.fetch); - let didOverwrite = false; - if (overwrite) { - const deleteResult = await deleteResource(resource, options); - // Return if it wasn't deleted - if (deleteResult.isError) - return deleteResult as - | DeleteResultError - | DeleteResultError; - didOverwrite = deleteResult.resourceExisted; - } else { - // Perform a read to check if it exists - const readResult = await readResource(resource, options); - // If it does exist stop and return. - if (readResult.type !== "absentReadSuccess") { - return readResult; - } - } // Create the document - const parentUri = getParentUri(resource.uri)!; - const headers: RequestInit["headers"] = { + const headers: HeadersInit = { "content-type": "text/turtle", - slug: getSlug(resource.uri), }; if (resource.type === "SolidContainer") { headers.link = '; rel="type"'; } - const response = await fetch(parentUri, { - method: "post", + if (!overwrite) { + // https://solidproject.org/TR/protocol#conditional-update + // https://www.rfc-editor.org/info/rfc9110/#section-13.1.2 + headers["If-None-Match"] = "*"; + } + const response = await fetch(resource.uri, { + method: "PUT", headers, }); + // Check whether If-None-Match: "*" precondition failed. + // That means we tried to overwrite existing resource when overwriting is forbidden. + if (response.status === 412) { + const result = await readResource(resource, options); + if (result.type === "absentReadSuccess") + return new NoncompliantPodError( + resource, + `Server returned conflicting states: Response status 412 implies the resource ${resource.uri} exists, but it is absent.`, + ) as + | NoncompliantPodError + | NoncompliantPodError; + return result; + } + const httpError = HttpErrorResult.checkResponse(resource, response); if (httpError) return httpError as | HttpErrorResultType | HttpErrorResultType; + const didOverwrite = response.status !== 201; + if (options?.dataset) { addResourceRdfToContainer(resource.uri, options.dataset); } diff --git a/packages/connected-solid/src/requester/requests/uploadResource.ts b/packages/connected-solid/src/requester/requests/uploadResource.ts index 2f976255..e56d7fbe 100644 --- a/packages/connected-solid/src/requester/requests/uploadResource.ts +++ b/packages/connected-solid/src/requester/requests/uploadResource.ts @@ -5,16 +5,12 @@ import type { LeafCreateAndOverwriteResult, LeafCreateIfAbsentResult, } from "./createDataResource"; -import { deleteResource } from "./deleteResource"; import { readResource } from "./readResource"; import type { DatasetRequestOptions } from "./requestOptions"; import type { SolidLeaf } from "../../resources/SolidLeaf"; import { CreateSuccess } from "../results/success/CreateSuccess"; -import { - addResourceRdfToContainer, - getParentUri, - getSlug, -} from "../../util/rdfUtils"; +import { addResourceRdfToContainer } from "../../util/rdfUtils"; +import { NoncompliantPodError } from "../results/error/NoncompliantPodError"; /** * @internal @@ -50,34 +46,40 @@ export async function uploadResource( ): Promise { try { const fetch = guaranteeFetch(options?.fetch); - let didOverwrite = false; - if (overwrite) { - const deleteResult = await deleteResource(resource, options); - // Return if it wasn't deleted - if (deleteResult.isError) return deleteResult; - didOverwrite = deleteResult.resourceExisted; - } else { - // Perform a read to check if it exists - const readResult = await readResource(resource, options); - // If it does exist stop and return. - if (readResult.type !== "absentReadSuccess") { - return readResult; - } + const headers: HeadersInit = { + "content-type": mimeType, + }; + + if (!overwrite) { + // https://solidproject.org/TR/protocol#conditional-update + // https://www.rfc-editor.org/info/rfc9110/#section-13.1.2 + headers["If-None-Match"] = "*"; } + // Create the document - const parentUri = getParentUri(resource.uri)!; - const response = await fetch(parentUri, { - method: "post", - headers: { - "content-type": mimeType, - slug: getSlug(resource.uri), - }, + const response = await fetch(resource.uri, { + method: "PUT", + headers, body: blob, }); + // Check whether If-None-Match: "*" precondition failed. + // That means we tried to overwrite existing resource when overwriting is forbidden. + if (response.status === 412) { + const result = await readResource(resource, options); + if (result.type === "absentReadSuccess") + return new NoncompliantPodError( + resource, + `Server returned conflicting states: Response status 412 implies the resource ${resource.uri} exists, but it is absent.`, + ); + return result; + } + const httpError = HttpErrorResult.checkResponse(resource, response); if (httpError) return httpError; + const didOverwrite = response.status !== 201; + if (options?.dataset) { addResourceRdfToContainer(resource.uri, options.dataset); } diff --git a/packages/connected-solid/test/Integration.test.ts b/packages/connected-solid/test/Integration.test.ts index 3af51168..e1abcc91 100644 --- a/packages/connected-solid/test/Integration.test.ts +++ b/packages/connected-solid/test/Integration.test.ts @@ -520,7 +520,8 @@ describe("Integration", () => { resource.read(), ]); - expect(s.fetchMock).toHaveBeenCalledTimes(3); + // one for create, one for read + expect(s.fetchMock).toHaveBeenCalledTimes(2); expect(result.type).toBe("dataReadSuccess"); expect(result1.type).toBe("dataReadSuccess"); }); @@ -925,9 +926,6 @@ describe("Integration", () => { it("returns an error if the create fetch fails", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - s.fetchMock.mockImplementationOnce(async (...args) => { - return s.authFetch(...args); - }); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, @@ -940,9 +938,6 @@ describe("Integration", () => { it("returns an unexpected error if some unknown error is triggered", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - s.fetchMock.mockImplementationOnce(async (...args) => { - return s.authFetch(...args); - }); s.fetchMock.mockImplementationOnce(async () => { throw new Error("Some Unknown"); }); @@ -961,8 +956,8 @@ describe("Integration", () => { expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); - // 1 for read, 1 for delete in createAndOverwrite, 1 for create - expect(s.fetchMock).toHaveBeenCalledTimes(3); + // 1 for read, 1 for create + expect(s.fetchMock).toHaveBeenCalledTimes(2); }); it("batches the create request while waiting on a similar request", async () => { @@ -974,8 +969,8 @@ describe("Integration", () => { expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); - // 1 for delete in createAndOverwrite, 1 for create - expect(s.fetchMock).toHaveBeenCalledTimes(2); + // 1 for create or update with PUT + expect(s.fetchMock).toHaveBeenCalledTimes(1); }); }); @@ -1068,10 +1063,10 @@ describe("Integration", () => { .children() .some((child) => child.uri === SAMPLE_CONTAINER_URI), ).toBe(true); - const postRequest = s.fetchMock.mock.calls.find( - (call) => call[1]?.method?.toLowerCase() === "post", + const putRequest = s.fetchMock.mock.calls.find( + (call) => call[1]?.method?.toLowerCase() === "put", ); - expect(postRequest?.[1]?.headers).toHaveProperty( + expect(putRequest?.[1]?.headers).toHaveProperty( "link", '; rel="type"', ); @@ -1489,9 +1484,6 @@ describe("Integration", () => { it("returns an error if the create fetch fails", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); - s.fetchMock.mockImplementationOnce(async (...args) => { - return s.authFetch(...args); - }); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, @@ -1507,9 +1499,6 @@ describe("Integration", () => { it("returns an unexpected error if some unknown error is triggered", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); - s.fetchMock.mockImplementationOnce(async (...args) => { - return s.authFetch(...args); - }); s.fetchMock.mockImplementationOnce(async () => { throw new Error("Some Unknown"); }); @@ -1537,8 +1526,8 @@ describe("Integration", () => { expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); - // 1 for read, 1 for delete in createAndOverwrite, 1 for create - expect(s.fetchMock).toHaveBeenCalledTimes(3); + // 1 for read, 1 for create + expect(s.fetchMock).toHaveBeenCalledTimes(2); expect(resource.getBlob()?.toString()).toBe("some text 2."); }); @@ -1557,8 +1546,8 @@ describe("Integration", () => { expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); - // 1 for delete in createAndOverwrite, 1 for create - expect(s.fetchMock).toHaveBeenCalledTimes(2); + // 1 for create + expect(s.fetchMock).toHaveBeenCalledTimes(1); expect(resource.getBlob()?.toString()).toBe("some text 2."); }); }); diff --git a/packages/connected/src/results/error/ErrorResult.ts b/packages/connected/src/results/error/ErrorResult.ts index c92038d7..7fc69a50 100644 --- a/packages/connected/src/results/error/ErrorResult.ts +++ b/packages/connected/src/results/error/ErrorResult.ts @@ -19,7 +19,7 @@ export abstract class ErrorResult extends Error implements ConnectedResult { * @param message - a custom message for the error */ constructor(message?: string) { - super(message || "An unkown error was encountered."); + super(message || "An unknown error was encountered."); } } @@ -44,7 +44,7 @@ export abstract class ResourceError< * @param message - A custom message for the error */ constructor(resource: ResourceType, message?: string) { - super(message || `An unkown error for ${resource.uri}`); + super(message || `An unknown error for ${resource.uri}`); this.uri = resource.uri; this.resource = resource; } diff --git a/packages/connected/test/ErrorResult.test.ts b/packages/connected/test/ErrorResult.test.ts index f29af2b3..07f45fbb 100644 --- a/packages/connected/test/ErrorResult.test.ts +++ b/packages/connected/test/ErrorResult.test.ts @@ -18,7 +18,7 @@ describe("ErrorResult", () => { ).toBe("hello"); }); - it("returns an UnexpecteResourceError if an odd valud is provided", () => { + it("returns an UnexpectedResourceError if an odd value is provided", () => { expect(UnexpectedResourceError.fromThrown(mockResource, 5).message).toBe( "Error of type number thrown: 5", ); @@ -46,19 +46,19 @@ describe("ErrorResult", () => { readonly type = "concreteErrorResult" as const; } - it("ResourceError fallsback to a default message if none is provided", () => { + it("ResourceError falls back to a default message if none is provided", () => { expect(new ConcreteResourceError(mockResource).message).toBe( - "An unkown error for https://example.com/", + "An unknown error for https://example.com/", ); }); - it("ErrorResult fallsback to a default message if none is provided", () => { + it("ErrorResult falls back to a default message if none is provided", () => { expect(new ConcreteErrorResult().message).toBe( - "An unkown error was encountered.", + "An unknown error was encountered.", ); }); - it("InvalidUriError fallsback to a default message if none is provided", () => { + it("InvalidUriError falls back to a default message if none is provided", () => { expect(new InvalidUriError(mockResource).message).toBe( "https://example.com/ is an invalid uri.", );