Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -56,8 +51,7 @@ export type LeafCreateIfAbsentResult =
* All possible errors returned by creating and overwriting a resource
*/
export type CreateAndOverwriteResultErrors<ResourceType extends Resource> =
| DeleteResultError<ResourceType>
| CreateErrors<ResourceType>;
CreateErrors<ResourceType>;

/**
* All possible errors returned by creating a resource if absent
Expand Down Expand Up @@ -91,27 +85,27 @@ export function createDataResource(
options?: DatasetRequestOptions,
): Promise<ContainerCreateAndOverwriteResult>;
export function createDataResource(
resouce: SolidLeaf,
resource: SolidLeaf,
overwrite: true,
options?: DatasetRequestOptions,
): Promise<LeafCreateAndOverwriteResult>;
export function createDataResource(
resouce: SolidContainer,
resource: SolidContainer,
overwrite?: false,
options?: DatasetRequestOptions,
): Promise<ContainerCreateIfAbsentResult>;
export function createDataResource(
resouce: SolidLeaf,
resource: SolidLeaf,
overwrite?: false,
options?: DatasetRequestOptions,
): Promise<LeafCreateIfAbsentResult>;
export function createDataResource(
resouce: SolidContainer,
resource: SolidContainer,
overwrite?: boolean,
options?: DatasetRequestOptions,
): Promise<ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult>;
export function createDataResource(
resouce: SolidLeaf,
resource: SolidLeaf,
overwrite?: boolean,
options?: DatasetRequestOptions,
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult>;
Expand Down Expand Up @@ -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<SolidLeaf>
| DeleteResultError<SolidContainer>;
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 = '<http://www.w3.org/ns/ldp#BasicContainer>; 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<SolidLeaf>
| NoncompliantPodError<SolidContainer>;
return result;
}

const httpError = HttpErrorResult.checkResponse(resource, response);
if (httpError)
return httpError as
| HttpErrorResultType<SolidContainer>
| HttpErrorResultType<SolidLeaf>;

const didOverwrite = response.status !== 201;

if (options?.dataset) {
addResourceRdfToContainer(resource.uri, options.dataset);
}
Expand Down
54 changes: 28 additions & 26 deletions packages/connected-solid/src/requester/requests/uploadResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,34 +46,40 @@ export async function uploadResource(
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult> {
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);
}
Expand Down
37 changes: 13 additions & 24 deletions packages/connected-solid/test/Integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down Expand Up @@ -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,
Expand All @@ -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");
});
Expand All @@ -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 () => {
Expand All @@ -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);
});
});

Expand Down Expand Up @@ -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",
'<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
);
Expand Down Expand Up @@ -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,
Expand All @@ -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");
});
Expand Down Expand Up @@ -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.");
});

Expand All @@ -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.");
});
});
Expand Down
4 changes: 2 additions & 2 deletions packages/connected/src/results/error/ErrorResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
}

Expand All @@ -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;
}
Expand Down
12 changes: 6 additions & 6 deletions packages/connected/test/ErrorResult.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
Expand Down Expand Up @@ -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.",
);
Expand Down
Loading