From 50c44354dd1e47217f84c6daeec81766587e7313 Mon Sep 17 00:00:00 2001 From: Ailin Luca Date: Tue, 19 Sep 2023 16:13:15 -0400 Subject: [PATCH] Before interface refactor --- .../src/dashboard/BuildMainContainer.tsx | 10 +- packages/solid/src/SolidLdoDataset.ts | 33 ++- .../solid/src/requester/ContainerRequester.ts | 37 ++- packages/solid/src/requester/LeafRequester.ts | 93 +++++++- packages/solid/src/requester/Requester.ts | 151 +++++-------- .../requester/requestResults/AbsentResult.ts | 9 - .../requester/requestResults/AccessRule.ts | 51 ----- .../requester/requestResults/BinaryResult.ts | 18 -- .../requestResults/CommitChangesSuccess.ts | 12 - .../requester/requestResults/DataResult.ts | 27 --- .../requestResults/RequesterResult.ts | 7 - .../requester/requests/checkRootContainer.ts | 69 +++--- .../requester/requests/createDataResource.ts | 128 ++++++++--- .../src/requester/requests/deleteResource.ts | 48 ++-- .../src/requester/requests/getAccessRules.ts | 10 +- .../src/requester/requests/readResource.ts | 105 ++++++--- .../src/requester/requests/requestOptions.ts | 9 + .../src/requester/requests/requestParams.ts | 10 - .../src/requester/requests/setAccessRules.ts | 28 +-- .../requester/requests/updateDataResource.ts | 47 ++-- .../src/requester/requests/uploadResource.ts | 73 +++--- .../src/requester/results/RequesterResult.ts | 8 + .../error}/ErrorResult.ts | 68 +++--- .../error}/HttpErrorResult.ts | 10 +- .../results/error/InvalidUriError.ts | 5 + .../results/error/NoncompliantPodError.ts | 11 + .../requester/results/success/AccessRule.ts | 20 ++ .../success/CheckRootContainerSuccess.ts | 11 + .../results/success/CreateSuccess.ts | 11 + .../results/success/DeleteSuccess.ts | 11 + .../requester/results/success/ReadSuccess.ts | 56 +++++ .../results/success/SuccessResult.ts | 30 +++ .../requester/results/success/Unfetched.ts | 5 + .../results/success/UpdateSuccess.ts | 5 + packages/solid/src/resource/Container.ts | 212 ++++++++++-------- packages/solid/src/resource/Leaf.ts | 157 +++++++++---- packages/solid/src/resource/Resource.ts | 150 ++++++++----- packages/solid/src/resource/ResourceResult.ts | 10 + .../resourceResults/CreateResourceSuccess.ts | 14 ++ .../resourceResults/DeleteResourceSuccess.ts | 14 ++ .../GetRootContainerSuccess.ts | 12 + .../resourceResults/ReadResourceSuccess.ts | 72 ++++++ .../resourceResults/UpdateResourceSuccess.ts | 11 + packages/solid/src/util/guaranteeFetch.ts | 5 + packages/solid/src/util/rdfUtils.ts | 12 +- 45 files changed, 1224 insertions(+), 671 deletions(-) delete mode 100644 packages/solid/src/requester/requestResults/AbsentResult.ts delete mode 100644 packages/solid/src/requester/requestResults/AccessRule.ts delete mode 100644 packages/solid/src/requester/requestResults/BinaryResult.ts delete mode 100644 packages/solid/src/requester/requestResults/CommitChangesSuccess.ts delete mode 100644 packages/solid/src/requester/requestResults/DataResult.ts delete mode 100644 packages/solid/src/requester/requestResults/RequesterResult.ts create mode 100644 packages/solid/src/requester/requests/requestOptions.ts delete mode 100644 packages/solid/src/requester/requests/requestParams.ts create mode 100644 packages/solid/src/requester/results/RequesterResult.ts rename packages/solid/src/requester/{requestResults => results/error}/ErrorResult.ts (56%) rename packages/solid/src/requester/{requestResults => results/error}/HttpErrorResult.ts (86%) create mode 100644 packages/solid/src/requester/results/error/InvalidUriError.ts create mode 100644 packages/solid/src/requester/results/error/NoncompliantPodError.ts create mode 100644 packages/solid/src/requester/results/success/AccessRule.ts create mode 100644 packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts create mode 100644 packages/solid/src/requester/results/success/CreateSuccess.ts create mode 100644 packages/solid/src/requester/results/success/DeleteSuccess.ts create mode 100644 packages/solid/src/requester/results/success/ReadSuccess.ts create mode 100644 packages/solid/src/requester/results/success/SuccessResult.ts create mode 100644 packages/solid/src/requester/results/success/Unfetched.ts create mode 100644 packages/solid/src/requester/results/success/UpdateSuccess.ts create mode 100644 packages/solid/src/resource/ResourceResult.ts create mode 100644 packages/solid/src/resource/resourceResults/CreateResourceSuccess.ts create mode 100644 packages/solid/src/resource/resourceResults/DeleteResourceSuccess.ts create mode 100644 packages/solid/src/resource/resourceResults/GetRootContainerSuccess.ts create mode 100644 packages/solid/src/resource/resourceResults/ReadResourceSuccess.ts create mode 100644 packages/solid/src/resource/resourceResults/UpdateResourceSuccess.ts create mode 100644 packages/solid/src/util/guaranteeFetch.ts diff --git a/packages/demo-react/src/dashboard/BuildMainContainer.tsx b/packages/demo-react/src/dashboard/BuildMainContainer.tsx index 2429325..611bd40 100644 --- a/packages/demo-react/src/dashboard/BuildMainContainer.tsx +++ b/packages/demo-react/src/dashboard/BuildMainContainer.tsx @@ -18,12 +18,14 @@ export const BuildMainContainer: FunctionComponent<{ useEffect(() => { if (session.webId) { const webIdResource = getResource(session.webId as LeafUri); - webIdResource.getRootContainer().then(async (rootContainer) => { - if (rootContainer.type === "error") { - alert(rootContainer.message); + webIdResource.getRootContainer().then(async (rootContainerResult) => { + if (rootContainerResult.isError) { + alert(rootContainerResult.message); return; } - const mainContainer = getResource(`${rootContainer.uri}demo-react/`); + const mainContainer = getResource( + `${rootContainerResult.rootContainer.uri}demo-react/`, + ); setMainContainer(mainContainer); await mainContainer.read(); if (mainContainer.isAbsent()) { diff --git a/packages/solid/src/SolidLdoDataset.ts b/packages/solid/src/SolidLdoDataset.ts index 78437b8..e464642 100644 --- a/packages/solid/src/SolidLdoDataset.ts +++ b/packages/solid/src/SolidLdoDataset.ts @@ -1,10 +1,14 @@ import { LdoDataset } from "@ldo/ldo"; import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils"; import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types"; -import { CommitChangesSuccess } from "./requester/requestResults/CommitChangesSuccess"; -import { InvalidUriError } from "./requester/requestResults/DataResult"; -import { AggregateError } from "./requester/requestResults/ErrorResult"; -import type { UpdateResultError } from "./requester/requests/updateDataResource"; +import type { + UpdateResult, + UpdateResultError, +} from "./requester/requests/updateDataResource"; +import { AggregateError } from "./requester/results/error/ErrorResult"; +import { InvalidUriError } from "./requester/results/error/InvalidUriError"; +import { AggregateSuccess } from "./requester/results/success/SuccessResult"; +import type { UpdateSuccess } from "./requester/results/success/UpdateSuccess"; import type { Container } from "./resource/Container"; import type { Leaf } from "./resource/Leaf"; import type { ResourceGetterOptions } from "./ResourceStore"; @@ -35,20 +39,25 @@ export class SolidLdoDataset extends LdoDataset { async commitChangesToPod( changes: DatasetChanges, ): Promise< - CommitChangesSuccess | AggregateError + | AggregateSuccess + | AggregateError > { const changesByGraph = splitChangesByGraph(changes); const results: [ GraphNode, DatasetChanges, - UpdateResultError | InvalidUriError | Leaf | { type: "defaultGraph" }, + UpdateResult | InvalidUriError | { type: "defaultGraph"; isError: false }, ][] = await Promise.all( Array.from(changesByGraph.entries()).map( async ([graph, datasetChanges]) => { if (graph.termType === "DefaultGraph") { // Undefined means that this is the default graph this.bulk(datasetChanges); - return [graph, datasetChanges, { type: "defaultGraph" }]; + return [ + graph, + datasetChanges, + { type: "defaultGraph", isError: false }, + ]; } if (isContainerUri(graph.value)) { return [ @@ -67,20 +76,20 @@ export class SolidLdoDataset extends LdoDataset { ); // If one has errored, return error - const errors = results.filter((result) => result[2].type === "error"); + const errors = results.filter((result) => result[2].isError); if (errors.length > 0) { return new AggregateError( - "", errors.map( (result) => result[2] as UpdateResultError | InvalidUriError, ), ); } - return new CommitChangesSuccess( - "", + return new AggregateSuccess( results .map((result) => result[2]) - .filter((result): result is Leaf => result.type === "leaf"), + .filter( + (result): result is UpdateSuccess => result.type === "updateSuccess", + ), ); } } diff --git a/packages/solid/src/requester/ContainerRequester.ts b/packages/solid/src/requester/ContainerRequester.ts index 5ea63a0..ea780dc 100644 --- a/packages/solid/src/requester/ContainerRequester.ts +++ b/packages/solid/src/requester/ContainerRequester.ts @@ -1,14 +1,49 @@ +import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; +import type { ContainerUri } from "../util/uriTypes"; import { Requester } from "./Requester"; import type { CheckRootResult } from "./requests/checkRootContainer"; import { checkRootContainer } from "./requests/checkRootContainer"; +import type { + ContainerCreateAndOverwriteResult, + ContainerCreateIfAbsentResult, +} from "./requests/createDataResource"; +import type { ReadContainerResult } from "./requests/readResource"; export const IS_ROOT_CONTAINER_KEY = "isRootContainer"; export class ContainerRequester extends Requester { + readonly uri: ContainerUri; + + constructor(uri: ContainerUri, context: SolidLdoDatasetContext) { + super(context); + this.uri = uri; + } + + read(): Promise { + return super.read() as Promise; + } + + createDataResource( + overwrite: true, + ): Promise; + createDataResource(overwrite?: false): Promise; + createDataResource( + overwrite?: boolean, + ): Promise; + createDataResource( + overwrite?: boolean, + ): Promise< + ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult + > { + return super.createDataResource(overwrite) as Promise< + ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult + >; + } + async isRootContainer(): Promise { return this.requestBatcher.queueProcess({ name: IS_ROOT_CONTAINER_KEY, - args: [{ uri: this.uri, fetch: this.context.fetch }], + args: [this.uri as ContainerUri, { fetch: this.context.fetch }], perform: checkRootContainer, modifyQueue: (queue, isLoading) => { if (queue.length === 0) { diff --git a/packages/solid/src/requester/LeafRequester.ts b/packages/solid/src/requester/LeafRequester.ts index b35cfb4..9ca93eb 100644 --- a/packages/solid/src/requester/LeafRequester.ts +++ b/packages/solid/src/requester/LeafRequester.ts @@ -1,17 +1,54 @@ import type { DatasetChanges } from "@ldo/rdf-utils"; import { mergeDatasetChanges } from "@ldo/subscribable-dataset"; import type { Quad } from "@rdfjs/types"; +import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; +import type { LeafUri } from "../util/uriTypes"; import { Requester } from "./Requester"; +import type { + LeafCreateAndOverwriteResult, + LeafCreateIfAbsentResult, +} from "./requests/createDataResource"; +import type { ReadLeafResult } from "./requests/readResource"; import type { UpdateResult } from "./requests/updateDataResource"; import { updateDataResource } from "./requests/updateDataResource"; +import { uploadResource } from "./requests/uploadResource"; export const UPDATE_KEY = "update"; +export const UPLOAD_KEY = "upload"; export class LeafRequester extends Requester { + readonly uri: LeafUri; + + constructor(uri: LeafUri, context: SolidLdoDatasetContext) { + super(context); + this.uri = uri; + } + isUpdating(): boolean { return this.requestBatcher.isLoading(UPDATE_KEY); } + isUploading(): boolean { + return this.requestBatcher.isLoading(UPLOAD_KEY); + } + + async read(): Promise { + return super.read() as Promise; + } + + createDataResource(overwrite: true): Promise; + createDataResource(overwrite?: false): Promise; + createDataResource( + overwrite?: boolean, + ): Promise; + createDataResource( + overwrite?: boolean, + ): Promise { + return super.createDataResource(overwrite) as Promise< + LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult + >; + } + /** * Update the data on this resource * @param changes @@ -22,9 +59,9 @@ export class LeafRequester extends Requester { const result = await this.requestBatcher.queueProcess({ name: UPDATE_KEY, args: [ - { uri: this.uri, fetch: this.context.fetch }, + this.uri, changes, - this.context.solidLdoDataset, + { fetch: this.context.fetch, dataset: this.context.solidLdoDataset }, ], perform: updateDataResource, modifyQueue: (queue, isLoading, [, changes]) => { @@ -39,4 +76,56 @@ export class LeafRequester extends Requester { }); return result; } + + /** + * Upload a binary + * @param blob + * @param mimeType + * @param overwrite: If true, will overwrite an existing file + */ + upload( + blob: Blob, + mimeType: string, + overwrite: true, + ): Promise; + upload( + blob: Blob, + mimeType: string, + overwrite?: false, + ): Promise; + upload( + blob: Blob, + mimeType: string, + overwrite?: boolean, + ): Promise; + async upload( + blob: Blob, + mimeType: string, + overwrite?: boolean, + ): Promise { + const transaction = this.context.solidLdoDataset.startTransaction(); + const result = await this.requestBatcher.queueProcess({ + name: UPLOAD_KEY, + args: [ + this.uri, + blob, + mimeType, + overwrite, + { dataset: transaction, fetch: this.context.fetch }, + ], + perform: uploadResource, + modifyQueue: (queue, isLoading, args) => { + const lastElementInQueue = queue[queue.length - 1]; + return ( + lastElementInQueue && + lastElementInQueue.name === UPLOAD_KEY && + !!lastElementInQueue.args[3] === !!args[3] + ); + }, + }); + if (!result.isError) { + transaction.commit(); + } + return result; + } } diff --git a/packages/solid/src/requester/Requester.ts b/packages/solid/src/requester/Requester.ts index 64e871a..7babc92 100644 --- a/packages/solid/src/requester/Requester.ts +++ b/packages/solid/src/requester/Requester.ts @@ -1,34 +1,32 @@ import { ANY_KEY, RequestBatcher } from "../util/RequestBatcher"; import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; import type { - CreateResult, - CreateResultWithoutOverwrite, + ContainerCreateAndOverwriteResult, + ContainerCreateIfAbsentResult, + LeafCreateAndOverwriteResult, + LeafCreateIfAbsentResult, } from "./requests/createDataResource"; import { createDataResource } from "./requests/createDataResource"; -import type { ReadResult } from "./requests/readResource"; -import { readResource } from "./requests/readResource"; import type { - UploadResult, - UploadResultWithoutOverwrite, -} from "./requests/uploadResource"; -import { uploadResource } from "./requests/uploadResource"; + ReadContainerResult, + ReadLeafResult, +} from "./requests/readResource"; +import { readResource } from "./requests/readResource"; import type { DeleteResult } from "./requests/deleteResource"; import { deleteResource } from "./requests/deleteResource"; const READ_KEY = "read"; const CREATE_KEY = "createDataResource"; -const UPLOAD_KEY = "upload"; const DELETE_KEY = "delete"; export abstract class Requester { protected readonly requestBatcher = new RequestBatcher(); // All intance variables - readonly uri: string; + abstract readonly uri: string; protected context: SolidLdoDatasetContext; - constructor(uri: string, context: SolidLdoDatasetContext) { - this.uri = uri; + constructor(context: SolidLdoDatasetContext) { this.context = context; } @@ -38,9 +36,6 @@ export abstract class Requester { isCreating(): boolean { return this.requestBatcher.isLoading(CREATE_KEY); } - isUploading(): boolean { - return this.requestBatcher.isLoading(UPLOAD_KEY); - } isReading(): boolean { return this.requestBatcher.isLoading(READ_KEY); } @@ -51,11 +46,11 @@ export abstract class Requester { /** * Read this resource. */ - async read(): Promise { + async read(): Promise { const transaction = this.context.solidLdoDataset.startTransaction(); const result = await this.requestBatcher.queueProcess({ name: READ_KEY, - args: [{ uri: this.uri, transaction, fetch: this.context.fetch }], + args: [this.uri, { dataset: transaction, fetch: this.context.fetch }], perform: readResource, modifyQueue: (queue, isLoading) => { if (queue.length === 0) { @@ -65,119 +60,81 @@ export abstract class Requester { } }, }); - if (result.type !== "error") { + if (!result.isError) { transaction.commit(); } return result; } /** - * Creates a Resource - * @param overwrite: If true, this will orverwrite the resource if it already - * exists + * Delete this resource */ - async createDataResource( - overwrite?: false, - ): Promise; - async createDataResource(overwrite: true): Promise; - async createDataResource( - overwrite?: boolean, - ): Promise; - async createDataResource( - overwrite?: boolean, - ): Promise { + async delete(): Promise { const transaction = this.context.solidLdoDataset.startTransaction(); const result = await this.requestBatcher.queueProcess({ - name: CREATE_KEY, - args: [ - { uri: this.uri, transaction, fetch: this.context.fetch }, - overwrite, - ], - perform: createDataResource, - modifyQueue: (queue, isLoading, args) => { - const lastElementInQueue = queue[queue.length - 1]; - return ( - lastElementInQueue && - lastElementInQueue.name === CREATE_KEY && - !!lastElementInQueue.args[1] === !!args[1] - ); + name: DELETE_KEY, + args: [this.uri, { dataset: transaction, fetch: this.context.fetch }], + perform: deleteResource, + modifyQueue: (queue, isLoading) => { + if (queue.length === 0) { + return isLoading[DELETE_KEY]; + } else { + return queue[queue.length - 1].name === DELETE_KEY; + } }, }); - if (result.type !== "error") { + if (!result.isError) { transaction.commit(); } return result; } /** - * Upload a binary - * @param blob - * @param mimeType - * @param overwrite: If true, will overwrite an existing file + * Creates a Resource + * @param overwrite: If true, this will orverwrite the resource if it already + * exists */ - async upload( - blob: Blob, - mimeType: string, - overwrite?: false, - ): Promise; - async upload( - blob: Blob, - mimeType: string, + createDataResource( overwrite: true, - ): Promise; - async upload( - blob: Blob, - mimeType: string, + ): Promise; + createDataResource( + overwrite?: false, + ): Promise; + createDataResource( overwrite?: boolean, - ): Promise; - async upload( - blob: Blob, - mimeType: string, + ): Promise< + | ContainerCreateAndOverwriteResult + | LeafCreateAndOverwriteResult + | ContainerCreateIfAbsentResult + | LeafCreateIfAbsentResult + >; + async createDataResource( overwrite?: boolean, - ): Promise { + ): Promise< + | ContainerCreateAndOverwriteResult + | LeafCreateAndOverwriteResult + | ContainerCreateIfAbsentResult + | LeafCreateIfAbsentResult + > { const transaction = this.context.solidLdoDataset.startTransaction(); const result = await this.requestBatcher.queueProcess({ - name: UPLOAD_KEY, + name: CREATE_KEY, args: [ - { uri: this.uri, transaction, fetch: this.context.fetch }, - blob, - mimeType, + this.uri, overwrite, + { dataset: transaction, fetch: this.context.fetch }, ], - perform: uploadResource, + perform: createDataResource, modifyQueue: (queue, isLoading, args) => { const lastElementInQueue = queue[queue.length - 1]; return ( lastElementInQueue && - lastElementInQueue.name === UPLOAD_KEY && - !!lastElementInQueue.args[3] === !!args[3] + lastElementInQueue.name === CREATE_KEY && + !!lastElementInQueue.args[1] === !!args[1] ); }, }); - if (result.type !== "error") { - transaction.commit(); - } - return result; - } - - /** - * Delete this resource - */ - async delete(): Promise { - const transaction = this.context.solidLdoDataset.startTransaction(); - const result = await this.requestBatcher.queueProcess({ - name: DELETE_KEY, - args: [{ uri: this.uri, transaction, fetch: this.context.fetch }], - perform: deleteResource, - modifyQueue: (queue, isLoading) => { - if (queue.length === 0) { - return isLoading[DELETE_KEY]; - } else { - return queue[queue.length - 1].name === DELETE_KEY; - } - }, - }); - if (result.type !== "error") { + if (!result.isError) { transaction.commit(); } return result; diff --git a/packages/solid/src/requester/requestResults/AbsentResult.ts b/packages/solid/src/requester/requestResults/AbsentResult.ts deleted file mode 100644 index ca67ade..0000000 --- a/packages/solid/src/requester/requestResults/AbsentResult.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RequesterResult } from "./RequesterResult"; - -export class AbsentResult extends RequesterResult { - type = "absent" as const; - - static is(response: Response): boolean { - return response.status === 404; - } -} diff --git a/packages/solid/src/requester/requestResults/AccessRule.ts b/packages/solid/src/requester/requestResults/AccessRule.ts deleted file mode 100644 index 06290b4..0000000 --- a/packages/solid/src/requester/requestResults/AccessRule.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Access } from "@inrupt/solid-client"; -import { ErrorResult } from "./ErrorResult"; -import { RequesterResult } from "./RequesterResult"; - -export interface AccessRule { - public?: Access; - agent?: Record; -} - -export class AccessRuleChangeResult - extends RequesterResult - implements AccessRule -{ - type = "accessRuleChange" as const; - readonly public?: Access; - readonly agent?: Record; - - constructor( - uri: string, - publicRules?: Access, - agentRules?: Record, - ) { - super(uri); - this.public = publicRules; - this.agent = agentRules; - } -} - -export class AccessRuleResult extends RequesterResult implements AccessRule { - type = "accessRule" as const; - readonly public: Access; - readonly agent: Record; - - constructor( - uri: string, - publicRules: Access, - agentRules: Record, - ) { - super(uri); - this.public = publicRules; - this.agent = agentRules; - } -} - -export class AccessRuleFetchError extends ErrorResult { - readonly errorType = "accessRuleFetch" as const; - - constructor(uri: string, message?: string) { - super(uri, message || `Cannot get access rules for ${uri}.`); - } -} diff --git a/packages/solid/src/requester/requestResults/BinaryResult.ts b/packages/solid/src/requester/requestResults/BinaryResult.ts deleted file mode 100644 index 3444122..0000000 --- a/packages/solid/src/requester/requestResults/BinaryResult.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RequesterResult } from "./RequesterResult"; - -export class BinaryResult extends RequesterResult { - type = "binary" as const; - readonly blob: Blob; - readonly mimeType: string; - - constructor(uri: string, blob: Blob, mimeType: string) { - super(uri); - this.blob = blob; - this.mimeType = mimeType; - } - - static is(response: Response): boolean { - const contentType = response.headers.get("content-type"); - return !contentType || contentType !== "text/turtle"; - } -} diff --git a/packages/solid/src/requester/requestResults/CommitChangesSuccess.ts b/packages/solid/src/requester/requestResults/CommitChangesSuccess.ts deleted file mode 100644 index 8a1cf4d..0000000 --- a/packages/solid/src/requester/requestResults/CommitChangesSuccess.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Container } from "../../resource/Container"; -import type { Leaf } from "../../resource/Leaf"; -import { RequesterResult } from "./RequesterResult"; - -export class CommitChangesSuccess extends RequesterResult { - readonly type = "commitChangesSuccess" as const; - readonly affectedResources: (Leaf | Container)[]; - constructor(uri: string, affectedResources: (Leaf | Container)[]) { - super(uri); - this.affectedResources = affectedResources; - } -} diff --git a/packages/solid/src/requester/requestResults/DataResult.ts b/packages/solid/src/requester/requestResults/DataResult.ts deleted file mode 100644 index bd031c9..0000000 --- a/packages/solid/src/requester/requestResults/DataResult.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ErrorResult } from "./ErrorResult"; -import { RequesterResult } from "./RequesterResult"; - -export class DataResult extends RequesterResult { - type = "data" as const; - - static is(response: Response): boolean { - const contentType = response.headers.get("content-type"); - return !!contentType && contentType === "text/turtle"; - } -} - -export class TurtleFormattingError extends ErrorResult { - errorType = "turtleFormatting" as const; - - constructor(uri: string, message?: string) { - super(uri, message || `Problem parsing turtle for ${uri}`); - } -} - -export class InvalidUriError extends ErrorResult { - errorType = "invalidUri" as const; - - constructor(uri: string, message?: string) { - super(uri, message || `${uri} is not a valid uri.`); - } -} diff --git a/packages/solid/src/requester/requestResults/RequesterResult.ts b/packages/solid/src/requester/requestResults/RequesterResult.ts deleted file mode 100644 index f98debf..0000000 --- a/packages/solid/src/requester/requestResults/RequesterResult.ts +++ /dev/null @@ -1,7 +0,0 @@ -export abstract class RequesterResult { - readonly uri: string; - abstract readonly type: string; - constructor(uri: string) { - this.uri = uri; - } -} diff --git a/packages/solid/src/requester/requests/checkRootContainer.ts b/packages/solid/src/requester/requests/checkRootContainer.ts index 0f38e3a..0269178 100644 --- a/packages/solid/src/requester/requests/checkRootContainer.ts +++ b/packages/solid/src/requester/requests/checkRootContainer.ts @@ -1,35 +1,52 @@ -import { - UnexpectedHttpError, - type HttpErrorResultType, -} from "../requestResults/HttpErrorResult"; -import { UnexpectedError } from "../requestResults/ErrorResult"; -import type { SimpleRequestParams } from "./requestParams"; +import type { BasicRequestOptions } from "./requestOptions"; import { parse as parseLinkHeader } from "http-link-header"; +import { NoncompliantPodError } from "../results/error/NoncompliantPodError"; +import { CheckRootContainerSuccess } from "../results/success/CheckRootContainerSuccess"; +import type { + HttpErrorResultType, + UnexpectedHttpError, +} from "../results/error/HttpErrorResult"; +import { HttpErrorResult } from "../results/error/HttpErrorResult"; +import { UnexpectedResourceError } from "../results/error/ErrorResult"; +import { guaranteeFetch } from "../../util/guaranteeFetch"; +import type { ContainerUri } from "../../util/uriTypes"; + +export type CheckRootResult = CheckRootContainerSuccess | CheckRootResultError; +export type CheckRootResultError = + | HttpErrorResultType + | NoncompliantPodError + | UnexpectedHttpError + | UnexpectedResourceError; -export type CheckRootResult = boolean | CheckRootResultError; -export type CheckRootResultError = HttpErrorResultType | UnexpectedError; +export function checkHeadersForRootContainer( + uri: ContainerUri, + headers: Headers, +): CheckRootContainerSuccess | NoncompliantPodError { + const linkHeader = headers.get("link"); + if (!linkHeader) { + return new NoncompliantPodError(uri, "No link header present in request."); + } + const parsedLinkHeader = parseLinkHeader(linkHeader); + const types = parsedLinkHeader.get("rel", "type"); + const isRoot = types.some( + (type) => type.uri === "http://www.w3.org/ns/pim/space#Storage", + ); + return new CheckRootContainerSuccess(uri, isRoot); +} -export async function checkRootContainer({ - uri, - fetch, -}: SimpleRequestParams): Promise { +export async function checkRootContainer( + uri: ContainerUri, + options?: BasicRequestOptions, +): Promise { try { + const fetch = guaranteeFetch(options?.fetch); // Fetch options to determine the document type const response = await fetch(uri, { method: "HEAD" }); - const linkHeader = response.headers.get("link"); - if (!linkHeader) { - return new UnexpectedHttpError( - uri, - response, - "No link header present in request.", - ); - } - const parsedLinkHeader = parseLinkHeader(linkHeader); - const types = parsedLinkHeader.get("rel", "type"); - return types.some( - (type) => type.uri === "http://www.w3.org/ns/pim/space#Storage", - ); + const httpErrorResult = HttpErrorResult.checkResponse(uri, response); + if (httpErrorResult) return httpErrorResult; + + return checkHeadersForRootContainer(uri, response.headers); } catch (err) { - return UnexpectedError.fromThrown(uri, err); + return UnexpectedResourceError.fromThrown(uri, err); } } diff --git a/packages/solid/src/requester/requests/createDataResource.ts b/packages/solid/src/requester/requests/createDataResource.ts index 207d2ad..95dab55 100644 --- a/packages/solid/src/requester/requests/createDataResource.ts +++ b/packages/solid/src/requester/requests/createDataResource.ts @@ -1,58 +1,116 @@ +import { guaranteeFetch } from "../../util/guaranteeFetch"; import { addResourceRdfToContainer, getParentUri, getSlug, } from "../../util/rdfUtils"; +import type { ContainerUri, LeafUri } from "../../util/uriTypes"; import { isContainerUri } from "../../util/uriTypes"; -import type { BinaryResult } from "../requestResults/BinaryResult"; -import type { TurtleFormattingError } from "../requestResults/DataResult"; -import { DataResult } from "../requestResults/DataResult"; -import { UnexpectedError } from "../requestResults/ErrorResult"; -import type { HttpErrorResultType } from "../requestResults/HttpErrorResult"; -import { HttpErrorResult } from "../requestResults/HttpErrorResult"; +import { UnexpectedResourceError } from "../results/error/ErrorResult"; +import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; +import { HttpErrorResult } from "../results/error/HttpErrorResult"; +import { CreateSuccess } from "../results/success/CreateSuccess"; +import type { AbsentReadSuccess } from "../results/success/ReadSuccess"; +import type { DeleteResultError } from "./deleteResource"; import { deleteResource } from "./deleteResource"; +import type { + ReadContainerResult, + ReadLeafResult, + ReadResultError, +} from "./readResource"; import { readResource } from "./readResource"; -import type { RequestParams } from "./requestParams"; +import type { DatasetRequestOptions } from "./requestOptions"; -export type CreateResult = DataResult | CreateResultErrors; -export type CreateResultErrors = HttpErrorResultType | UnexpectedError; -export type CreateResultWithoutOverwrite = - | CreateResult - | CreateResultWithoutOverwriteErrors - | BinaryResult; -export type CreateResultWithoutOverwriteErrors = - | TurtleFormattingError - | CreateResultErrors; +export type ContainerCreateAndOverwriteResult = + | CreateSuccess + | CreateAndOverwriteResultErrors; +export type LeafCreateAndOverwriteResult = + | CreateSuccess + | CreateAndOverwriteResultErrors; +export type ContainerCreateIfAbsentResult = + | CreateSuccess + | Exclude + | CreateIfAbsentResultErrors; +export type LeafCreateIfAbsentResult = + | CreateSuccess + | Exclude + | CreateIfAbsentResultErrors; +export type CreateAndOverwriteResultErrors = DeleteResultError | CreateErrors; +export type CreateIfAbsentResultErrors = ReadResultError | CreateErrors; +export type CreateErrors = HttpErrorResultType | UnexpectedResourceError; + +export function createDataResource( + uri: ContainerUri, + overwrite: true, + options?: DatasetRequestOptions, +): Promise; +export function createDataResource( + uri: LeafUri, + overwrite: true, + options?: DatasetRequestOptions, +): Promise; +export function createDataResource( + uri: ContainerUri, + overwrite?: false, + options?: DatasetRequestOptions, +): Promise; export function createDataResource( - params: RequestParams, + uri: LeafUri, overwrite?: false, -): Promise; + options?: DatasetRequestOptions, +): Promise; export function createDataResource( - params: RequestParams, + uri: ContainerUri, + overwrite?: boolean, + options?: DatasetRequestOptions, +): Promise; +export function createDataResource( + uri: LeafUri, + overwrite?: boolean, + options?: DatasetRequestOptions, +): Promise; +export function createDataResource( + uri: string, overwrite: true, -): Promise; + options?: DatasetRequestOptions, +): Promise; export function createDataResource( - params: RequestParams, + uri: string, + overwrite?: false, + options?: DatasetRequestOptions, +): Promise; +export function createDataResource( + uri: string, overwrite?: boolean, -): Promise; + options?: DatasetRequestOptions, +): Promise< + | ContainerCreateAndOverwriteResult + | LeafCreateAndOverwriteResult + | ContainerCreateIfAbsentResult + | LeafCreateIfAbsentResult +>; export async function createDataResource( - params: RequestParams, + uri: string, overwrite?: boolean, -): Promise { - const { uri, transaction, fetch } = params; + options?: DatasetRequestOptions, +): Promise< + | ContainerCreateAndOverwriteResult + | LeafCreateAndOverwriteResult + | ContainerCreateIfAbsentResult + | LeafCreateIfAbsentResult +> { try { + const fetch = guaranteeFetch(options?.fetch); if (overwrite) { - const deleteResult = await deleteResource(params); + const deleteResult = await deleteResource(uri, options); // Return if it wasn't deleted - if (deleteResult.type !== "absent") { - return deleteResult; - } + if (deleteResult.isError) return deleteResult; } else { // Perform a read to check if it exists - const readResult = await readResource(params); + const readResult = await readResource(uri, options); // If it does exist stop and return. - if (readResult.type !== "absent") { + if (readResult.type !== "absentReadSuccess") { return readResult; } } @@ -73,9 +131,11 @@ export async function createDataResource( const httpError = HttpErrorResult.checkResponse(uri, response); if (httpError) return httpError; - addResourceRdfToContainer(uri, transaction); - return new DataResult(uri); + if (options?.dataset) { + addResourceRdfToContainer(uri, options.dataset); + } + return new CreateSuccess(uri, !!overwrite); } catch (err) { - return UnexpectedError.fromThrown(uri, err); + return UnexpectedResourceError.fromThrown(uri, err); } } diff --git a/packages/solid/src/requester/requests/deleteResource.ts b/packages/solid/src/requester/requests/deleteResource.ts index 6cdabcf..1e6b0e7 100644 --- a/packages/solid/src/requester/requests/deleteResource.ts +++ b/packages/solid/src/requester/requests/deleteResource.ts @@ -1,39 +1,45 @@ import { namedNode } from "@rdfjs/data-model"; -import { AbsentResult } from "../requestResults/AbsentResult"; -import { UnexpectedError } from "../requestResults/ErrorResult"; -import type { HttpErrorResultType } from "../requestResults/HttpErrorResult"; -import { UnexpectedHttpError } from "../requestResults/HttpErrorResult"; -import type { RequestParams } from "./requestParams"; +import { guaranteeFetch } from "../../util/guaranteeFetch"; import { deleteResourceRdfFromContainer } from "../../util/rdfUtils"; +import { UnexpectedResourceError } from "../results/error/ErrorResult"; +import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; +import { UnexpectedHttpError } from "../results/error/HttpErrorResult"; +import { HttpErrorResult } from "../results/error/HttpErrorResult"; +import { DeleteSuccess } from "../results/success/DeleteSuccess"; +import type { DatasetRequestOptions } from "./requestOptions"; -export type DeleteResult = AbsentResult | DeleteResultError; -export type DeleteResultError = HttpErrorResultType | UnexpectedError; +export type DeleteResult = DeleteSuccess | DeleteResultError; +export type DeleteResultError = HttpErrorResultType | UnexpectedResourceError; -export async function deleteResource({ - uri, - fetch, - transaction, -}: RequestParams): Promise { +export async function deleteResource( + uri: string, + options?: DatasetRequestOptions, +): Promise { try { + const fetch = guaranteeFetch(options?.fetch); const response = await fetch(uri, { method: "delete", }); + const errorResult = HttpErrorResult.checkResponse(uri, response); + if (errorResult) return errorResult; // Specifically check for a 205. Annoyingly, the server will return 200 even // if it hasn't been deleted when you're unauthenticated. 404 happens when // the document never existed if (response.status === 205 || response.status === 404) { - transaction.deleteMatches( - undefined, - undefined, - undefined, - namedNode(uri), - ); - deleteResourceRdfFromContainer(uri, transaction); - return new AbsentResult(uri); + if (options?.dataset) { + options.dataset.deleteMatches( + undefined, + undefined, + undefined, + namedNode(uri), + ); + deleteResourceRdfFromContainer(uri, options.dataset); + } + return new DeleteSuccess(uri, response.status === 205); } return new UnexpectedHttpError(uri, response); } catch (err) { - return UnexpectedError.fromThrown(uri, err); + return UnexpectedResourceError.fromThrown(uri, err); } } diff --git a/packages/solid/src/requester/requests/getAccessRules.ts b/packages/solid/src/requester/requests/getAccessRules.ts index 144501d..834bd88 100644 --- a/packages/solid/src/requester/requests/getAccessRules.ts +++ b/packages/solid/src/requester/requests/getAccessRules.ts @@ -1,12 +1,6 @@ -import type { - AccessRuleFetchError, - AccessRuleResult, -} from "../requestResults/AccessRule"; -import type { SimpleRequestParams } from "./requestParams"; +import type { AccessRuleFetchError } from "../results/success/AccessRule"; -export async function getAccessRules( - _params: SimpleRequestParams, -): Promise { +export async function getAccessRules(): Promise { throw new Error("Not Implemented"); // const [publicAccess, agentAccess] = await Promise.all([ // universalAccess.getPublicAccess(uri, { fetch }), diff --git a/packages/solid/src/requester/requests/readResource.ts b/packages/solid/src/requester/requests/readResource.ts index d46aa49..4b82d87 100644 --- a/packages/solid/src/requester/requests/readResource.ts +++ b/packages/solid/src/requester/requests/readResource.ts @@ -1,64 +1,103 @@ -import { DataResult } from "../requestResults/DataResult"; -import type { TurtleFormattingError } from "../requestResults/DataResult"; +import type { UnexpectedHttpError } from "../results/error/HttpErrorResult"; import { HttpErrorResult, - ServerHttpError, type HttpErrorResultType, -} from "../requestResults/HttpErrorResult"; -import { UnexpectedError } from "../requestResults/ErrorResult"; -import { AbsentResult } from "../requestResults/AbsentResult"; -import { BinaryResult } from "../requestResults/BinaryResult"; +} from "../results/error/HttpErrorResult"; import { addRawTurtleToDataset, addResourceRdfToContainer, } from "../../util/rdfUtils"; -import type { RequestParams } from "./requestParams"; +import type { DatasetRequestOptions } from "./requestOptions"; +import type { ContainerUri, LeafUri } from "../../util/uriTypes"; +import { isContainerUri } from "../../util/uriTypes"; +import { BinaryReadSuccess } from "../results/success/ReadSuccess"; +import { + ContainerReadSuccess, + DataReadSuccess, +} from "../results/success/ReadSuccess"; +import { AbsentReadSuccess } from "../results/success/ReadSuccess"; +import { NoncompliantPodError } from "../results/error/NoncompliantPodError"; +import { guaranteeFetch } from "../../util/guaranteeFetch"; +import { UnexpectedResourceError } from "../results/error/ErrorResult"; +import { checkHeadersForRootContainer } from "./checkRootContainer"; -export type ReadResult = - | AbsentResult - | DataResult - | BinaryResult +export type ReadLeafResult = + | BinaryReadSuccess + | DataReadSuccess + | AbsentReadSuccess + | ReadResultError; +export type ReadContainerResult = + | ContainerReadSuccess + | AbsentReadSuccess | ReadResultError; export type ReadResultError = | HttpErrorResultType - | TurtleFormattingError - | UnexpectedError; + | NoncompliantPodError + | UnexpectedHttpError + | UnexpectedResourceError; -export async function readResource({ - uri, - fetch, - transaction, -}: RequestParams): Promise { +export async function readResource( + uri: LeafUri, + options?: DatasetRequestOptions, +): Promise; +export async function readResource( + uri: ContainerUri, + options?: DatasetRequestOptions, +): Promise; +export async function readResource( + uri: string, + options?: DatasetRequestOptions, +): Promise; +export async function readResource( + uri: string, + options?: DatasetRequestOptions, +): Promise { try { + const fetch = guaranteeFetch(options?.fetch); // Fetch options to determine the document type const response = await fetch(uri); - if (AbsentResult.is(response)) { - return new AbsentResult(uri); + if (response.status === 404) { + return new AbsentReadSuccess(uri, false); } const httpErrorResult = HttpErrorResult.checkResponse(uri, response); if (httpErrorResult) return httpErrorResult; // Add this resource to the container - addResourceRdfToContainer(uri, transaction); + if (options?.dataset) { + addResourceRdfToContainer(uri, options.dataset); + } + + const contentType = response.headers.get("content-type"); + if (!contentType) { + return new NoncompliantPodError( + uri, + "Resource requests must return a content-type header.", + ); + } - if (DataResult.is(response)) { + if (contentType === "text/turtle") { // Parse Turtle const rawTurtle = await response.text(); - return addRawTurtleToDataset(rawTurtle, transaction, uri); - } else { - // Load Blob - const contentType = response.headers.get("content-type"); - if (!contentType) { - return new ServerHttpError( + if (options?.dataset) { + const result = await addRawTurtleToDataset( + rawTurtle, + options.dataset, uri, - response, - "Server provided no content-type", ); + if (result) return result; + } + if (isContainerUri(uri)) { + const result = checkHeadersForRootContainer(uri, response.headers); + if (result.isError) return result; + return new ContainerReadSuccess(uri, false, result.isRootContainer); } + return new DataReadSuccess(uri, false); + } else { + // Load Blob const blob = await response.blob(); - return new BinaryResult(uri, blob, contentType); + return new BinaryReadSuccess(uri, false, blob, contentType); } } catch (err) { - return UnexpectedError.fromThrown(uri, err); + return UnexpectedResourceError.fromThrown(uri, err); } } diff --git a/packages/solid/src/requester/requests/requestOptions.ts b/packages/solid/src/requester/requests/requestOptions.ts new file mode 100644 index 0000000..89626ae --- /dev/null +++ b/packages/solid/src/requester/requests/requestOptions.ts @@ -0,0 +1,9 @@ +import type { Dataset, Quad } from "@rdfjs/types"; + +export interface BasicRequestOptions { + fetch?: typeof fetch; +} + +export interface DatasetRequestOptions extends BasicRequestOptions { + dataset?: Dataset; +} diff --git a/packages/solid/src/requester/requests/requestParams.ts b/packages/solid/src/requester/requests/requestParams.ts deleted file mode 100644 index 33f8eda..0000000 --- a/packages/solid/src/requester/requests/requestParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TransactionalDataset } from "@ldo/subscribable-dataset"; -import type { Quad } from "@rdfjs/types"; - -export interface RequestParams { - uri: string; - fetch: typeof fetch; - transaction: TransactionalDataset; -} - -export type SimpleRequestParams = Omit; diff --git a/packages/solid/src/requester/requests/setAccessRules.ts b/packages/solid/src/requester/requests/setAccessRules.ts index 9b5baa7..4202492 100644 --- a/packages/solid/src/requester/requests/setAccessRules.ts +++ b/packages/solid/src/requester/requests/setAccessRules.ts @@ -1,6 +1,4 @@ import type { AclDataset, WithChangeLog } from "@inrupt/solid-client"; -import { getAgentAccessAll } from "@inrupt/solid-client"; -import { getPublicAccess } from "@inrupt/solid-client"; import { getSolidDatasetWithAcl, hasResourceAcl, @@ -14,17 +12,25 @@ import { setPublicResourceAccess, setAgentDefaultAccess, } from "@inrupt/solid-client"; +import { guaranteeFetch } from "../../util/guaranteeFetch"; import { isContainerUri } from "../../util/uriTypes"; -import type { AccessRule } from "../requestResults/AccessRule"; -import { AccessRuleChangeResult } from "../requestResults/AccessRule"; -import { AccessRuleFetchError } from "../requestResults/AccessRule"; -import type { SimpleRequestParams } from "./requestParams"; +import type { AccessRule } from "../results/success/AccessRule"; +import { SetAccessRuleSuccess } from "../results/success/AccessRule"; +import { AccessRuleFetchError } from "../results/success/AccessRule"; +import type { BasicRequestOptions } from "./requestOptions"; + +export type SetAccessRulesResult = + | SetAccessRuleSuccess + | SetAccessRulesResultError; +export type SetAccessRulesResultError = AccessRuleFetchError; export async function setAccessRules( - { uri, fetch }: SimpleRequestParams, + uri: string, newAccessRules: AccessRule, -): Promise { + options?: BasicRequestOptions, +): Promise { console.warn("Access Control is stil underdeveloped. Use with caution."); + const fetch = guaranteeFetch(options?.fetch); const isContainer = isContainerUri(uri); // Code Copied from https://docs.inrupt.com/developer-tools/javascript/client-libraries/tutorial/manage-wac/ @@ -72,9 +78,5 @@ export async function setAccessRules( // Now save the ACL: await saveAclFor(myDatasetWithAcl, updatedAcl, { fetch }); - return new AccessRuleChangeResult( - uri, - getPublicAccess(myDatasetWithAcl) || undefined, - getAgentAccessAll(myDatasetWithAcl) || undefined, - ); + return new SetAccessRuleSuccess(uri); } diff --git a/packages/solid/src/requester/requests/updateDataResource.ts b/packages/solid/src/requester/requests/updateDataResource.ts index 3671342..e1ffc2a 100644 --- a/packages/solid/src/requester/requests/updateDataResource.ts +++ b/packages/solid/src/requester/requests/updateDataResource.ts @@ -1,28 +1,37 @@ import type { DatasetChanges } from "@ldo/rdf-utils"; import { changesToSparqlUpdate } from "@ldo/rdf-utils"; -import { DataResult } from "../requestResults/DataResult"; -import type { HttpErrorResultType } from "../requestResults/HttpErrorResult"; -import { HttpErrorResult } from "../requestResults/HttpErrorResult"; -import { UnexpectedError } from "../requestResults/ErrorResult"; -import type { SimpleRequestParams } from "./requestParams"; -import type { SubscribableDataset } from "@ldo/subscribable-dataset"; +import type { + SubscribableDataset, + TransactionalDataset, +} from "@ldo/subscribable-dataset"; import type { Quad } from "@rdfjs/types"; +import { guaranteeFetch } from "../../util/guaranteeFetch"; +import type { LeafUri } from "../../util/uriTypes"; +import { UnexpectedResourceError } from "../results/error/ErrorResult"; +import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; +import { HttpErrorResult } from "../results/error/HttpErrorResult"; +import { UpdateSuccess } from "../results/success/UpdateSuccess"; +import type { BasicRequestOptions } from "./requestOptions"; -export type UpdateResult = DataResult | UpdateResultError; -export type UpdateResultError = HttpErrorResultType | UnexpectedError; +export type UpdateResult = UpdateSuccess | UpdateResultError; +export type UpdateResultError = HttpErrorResultType | UnexpectedResourceError; export async function updateDataResource( - { uri, fetch }: SimpleRequestParams, + uri: LeafUri, datasetChanges: DatasetChanges, - mainDataset: SubscribableDataset, + options?: BasicRequestOptions & { dataset?: SubscribableDataset }, ): Promise { try { + const fetch = guaranteeFetch(options?.fetch); // Put Changes in transactional dataset - const transaction = mainDataset.startTransaction(); - transaction.addAll(datasetChanges.added || []); - datasetChanges.removed?.forEach((quad) => transaction.delete(quad)); - // Commit data optimistically - transaction.commit(); + let transaction: TransactionalDataset | undefined; + if (options?.dataset) { + transaction = options.dataset.startTransaction(); + transaction.addAll(datasetChanges.added || []); + datasetChanges.removed?.forEach((quad) => transaction!.delete(quad)); + // Commit data optimistically + transaction.commit(); + } // Make request const sparqlUpdate = await changesToSparqlUpdate(datasetChanges); const response = await fetch(uri, { @@ -35,11 +44,13 @@ export async function updateDataResource( const httpError = HttpErrorResult.checkResponse(uri, response); if (httpError) { // Handle error rollback - transaction.rollback(); + if (transaction) { + transaction.rollback(); + } return httpError; } - return new DataResult(uri); + return new UpdateSuccess(uri); } catch (err) { - return UnexpectedError.fromThrown(uri, err); + return UnexpectedResourceError.fromThrown(uri, err); } } diff --git a/packages/solid/src/requester/requests/uploadResource.ts b/packages/solid/src/requester/requests/uploadResource.ts index b52ad84..b6d8b24 100644 --- a/packages/solid/src/requester/requests/uploadResource.ts +++ b/packages/solid/src/requester/requests/uploadResource.ts @@ -1,69 +1,60 @@ +import { guaranteeFetch } from "../../util/guaranteeFetch"; import { addResourceRdfToContainer, getParentUri, getSlug, } from "../../util/rdfUtils"; -import { BinaryResult } from "../requestResults/BinaryResult"; +import type { LeafUri } from "../../util/uriTypes"; +import { UnexpectedResourceError } from "../results/error/ErrorResult"; +import { HttpErrorResult } from "../results/error/HttpErrorResult"; +import { CreateSuccess } from "../results/success/CreateSuccess"; import type { - DataResult, - TurtleFormattingError, -} from "../requestResults/DataResult"; -import { UnexpectedError } from "../requestResults/ErrorResult"; -import { - HttpErrorResult, - type HttpErrorResultType, -} from "../requestResults/HttpErrorResult"; + LeafCreateAndOverwriteResult, + LeafCreateIfAbsentResult, +} from "./createDataResource"; import { deleteResource } from "./deleteResource"; import { readResource } from "./readResource"; -import type { RequestParams } from "./requestParams"; - -export type UploadResult = BinaryResult | UploadResultError; -export type UploadResultError = HttpErrorResultType | UnexpectedError; -export type UploadResultWithoutOverwrite = - | UploadResult - | UploadResultWithoutOverwriteError - | DataResult; -export type UploadResultWithoutOverwriteError = - | UploadResultError - | TurtleFormattingError; +import type { DatasetRequestOptions } from "./requestOptions"; export function uploadResource( - params: RequestParams, + uri: LeafUri, blob: Blob, mimeType: string, - overwrite?: false, -): Promise; + overwrite: true, + options?: DatasetRequestOptions, +): Promise; export function uploadResource( - params: RequestParams, + uri: LeafUri, blob: Blob, mimeType: string, - overwrite: true, -): Promise; + overwrite?: false, + options?: DatasetRequestOptions, +): Promise; export function uploadResource( - params: RequestParams, + uri: LeafUri, blob: Blob, mimeType: string, overwrite?: boolean, -): Promise; + options?: DatasetRequestOptions, +): Promise; export async function uploadResource( - params: RequestParams, + uri: LeafUri, blob: Blob, mimeType: string, overwrite?: boolean, -): Promise { - const { uri, transaction, fetch } = params; + options?: DatasetRequestOptions, +): Promise { try { + const fetch = guaranteeFetch(options?.fetch); if (overwrite) { - const deleteResult = await deleteResource(params); + const deleteResult = await deleteResource(uri, options); // Return if it wasn't deleted - if (deleteResult.type !== "absent") { - return deleteResult; - } + if (deleteResult.isError) return deleteResult; } else { // Perform a read to check if it exists - const readResult = await readResource(params); + const readResult = await readResource(uri, options); // If it does exist stop and return. - if (readResult.type !== "absent") { + if (readResult.type !== "absentReadSuccess") { return readResult; } } @@ -81,9 +72,11 @@ export async function uploadResource( const httpError = HttpErrorResult.checkResponse(uri, response); if (httpError) return httpError; - addResourceRdfToContainer(uri, transaction); - return new BinaryResult(uri, blob, mimeType); + if (options?.dataset) { + addResourceRdfToContainer(uri, options.dataset); + } + return new CreateSuccess(uri, !!overwrite); } catch (err) { - return UnexpectedError.fromThrown(uri, err); + return UnexpectedResourceError.fromThrown(uri, err); } } diff --git a/packages/solid/src/requester/results/RequesterResult.ts b/packages/solid/src/requester/results/RequesterResult.ts new file mode 100644 index 0000000..1517c23 --- /dev/null +++ b/packages/solid/src/requester/results/RequesterResult.ts @@ -0,0 +1,8 @@ +import type { Container } from "../../resource/Container"; +import type { Leaf } from "../../resource/Leaf"; + +export interface RequesterResult { + type: string; + isError: boolean; + resource?: Leaf | Container; +} diff --git a/packages/solid/src/requester/requestResults/ErrorResult.ts b/packages/solid/src/requester/results/error/ErrorResult.ts similarity index 56% rename from packages/solid/src/requester/requestResults/ErrorResult.ts rename to packages/solid/src/requester/results/error/ErrorResult.ts index 79aa760..e8fa15e 100644 --- a/packages/solid/src/requester/requestResults/ErrorResult.ts +++ b/packages/solid/src/requester/results/error/ErrorResult.ts @@ -1,43 +1,31 @@ -export abstract class ErrorResult extends Error { - readonly type = "error" as const; - readonly uri: string; - abstract readonly errorType: string; +import type { Container } from "../../../resource/Container"; +import type { Leaf } from "../../../resource/Leaf"; +import type { RequesterResult } from "../RequesterResult"; - constructor(uri: string, message?: string) { - super(message || "An error unkown error was encountered during a request."); - this.uri = uri; +export abstract class ErrorResult extends Error implements RequesterResult { + abstract type: string; + readonly isError = true as const; + resource?: Leaf | Container; + + constructor(message?: string) { + super(message || "An error unkown error was encountered."); } } -export class UnexpectedError extends ErrorResult { - error: Error; - readonly errorType = "unexpected" as const; - - constructor(uri: string, error: Error) { - super(uri, error.message); - this.error = error; - } +export abstract class ResourceError extends ErrorResult { + readonly uri: string; - static fromThrown(uri: string, err: unknown) { - if (err instanceof Error) { - return new UnexpectedError(uri, err); - } else if (typeof err === "string") { - return new UnexpectedError(uri, new Error(err)); - } else { - return new UnexpectedError( - uri, - new Error(`Error of type ${typeof err} thrown: ${err}`), - ); - } + constructor(uri: string, message?: string) { + super(message || `An error unkown error for ${uri}`); + this.uri = uri; } } export class AggregateError extends ErrorResult { - readonly errorType = "aggregate" as const; + readonly type = "aggregateError" as const; readonly errors: ErrorType[]; constructor( - uri: string, errors: (ErrorType | AggregateError)[], message?: string, ) { @@ -52,7 +40,6 @@ export class AggregateError extends ErrorResult { } }); super( - uri, message || `Encountered multiple errors:${allErrors.reduce( (agg, cur) => `${agg}\n${cur}`, @@ -62,3 +49,26 @@ export class AggregateError extends ErrorResult { this.errors = allErrors; } } + +export class UnexpectedResourceError extends ResourceError { + readonly type = "unexpectedResourceError" as const; + error: Error; + + constructor(uri: string, error: Error) { + super(uri, error.message); + this.error = error; + } + + static fromThrown(uri: string, err: unknown) { + if (err instanceof Error) { + return new UnexpectedResourceError(uri, err); + } else if (typeof err === "string") { + return new UnexpectedResourceError(uri, new Error(err)); + } else { + return new UnexpectedResourceError( + uri, + new Error(`Error of type ${typeof err} thrown: ${err}`), + ); + } + } +} diff --git a/packages/solid/src/requester/requestResults/HttpErrorResult.ts b/packages/solid/src/requester/results/error/HttpErrorResult.ts similarity index 86% rename from packages/solid/src/requester/requestResults/HttpErrorResult.ts rename to packages/solid/src/requester/results/error/HttpErrorResult.ts index c11df5f..f8bfebb 100644 --- a/packages/solid/src/requester/requestResults/HttpErrorResult.ts +++ b/packages/solid/src/requester/results/error/HttpErrorResult.ts @@ -1,11 +1,11 @@ -import { ErrorResult } from "./ErrorResult"; +import { ResourceError } from "./ErrorResult"; export type HttpErrorResultType = | ServerHttpError | UnexpectedHttpError | UnauthenticatedHttpError; -export abstract class HttpErrorResult extends ErrorResult { +export abstract class HttpErrorResult extends ResourceError { public readonly status: number; public readonly headers: Headers; public readonly response: Response; @@ -47,11 +47,11 @@ export abstract class HttpErrorResult extends ErrorResult { } export class UnexpectedHttpError extends HttpErrorResult { - errorType = "unexpectedHttp" as const; + readonly type = "unexpectedHttpError" as const; } export class UnauthenticatedHttpError extends HttpErrorResult { - errorType = "unauthenticated" as const; + readonly type = "unauthenticatedError" as const; static is(response: Response) { return response.status === 401; @@ -59,7 +59,7 @@ export class UnauthenticatedHttpError extends HttpErrorResult { } export class ServerHttpError extends HttpErrorResult { - errorType = "server" as const; + readonly type = "serverError" as const; static is(response: Response) { return response.status >= 500 && response.status < 600; diff --git a/packages/solid/src/requester/results/error/InvalidUriError.ts b/packages/solid/src/requester/results/error/InvalidUriError.ts new file mode 100644 index 0000000..477721c --- /dev/null +++ b/packages/solid/src/requester/results/error/InvalidUriError.ts @@ -0,0 +1,5 @@ +import { ResourceError } from "./ErrorResult"; + +export class InvalidUriError extends ResourceError { + readonly type = "invalidUriError" as const; +} diff --git a/packages/solid/src/requester/results/error/NoncompliantPodError.ts b/packages/solid/src/requester/results/error/NoncompliantPodError.ts new file mode 100644 index 0000000..df87f5b --- /dev/null +++ b/packages/solid/src/requester/results/error/NoncompliantPodError.ts @@ -0,0 +1,11 @@ +import { ResourceError } from "./ErrorResult"; + +export class NoncompliantPodError extends ResourceError { + readonly type = "noncompliantPodError" as const; + constructor(uri: string, message?: string) { + super( + uri, + `Response from ${uri} is not compliant with the Solid Specification: ${message}`, + ); + } +} diff --git a/packages/solid/src/requester/results/success/AccessRule.ts b/packages/solid/src/requester/results/success/AccessRule.ts new file mode 100644 index 0000000..7090c18 --- /dev/null +++ b/packages/solid/src/requester/results/success/AccessRule.ts @@ -0,0 +1,20 @@ +import type { Access } from "@inrupt/solid-client"; +import { ResourceError } from "../error/ErrorResult"; +import { ResourceSuccess } from "./SuccessResult"; + +export interface AccessRule { + public?: Access; + agent?: Record; +} + +export class SetAccessRuleSuccess extends ResourceSuccess { + type = "setAccessRuleSuccess" as const; +} + +export class AccessRuleFetchError extends ResourceError { + readonly type = "accessRuleFetchError" as const; + + constructor(uri: string, message?: string) { + super(uri, message || `Cannot get access rules for ${uri}.`); + } +} diff --git a/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts b/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts new file mode 100644 index 0000000..a4aad08 --- /dev/null +++ b/packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts @@ -0,0 +1,11 @@ +import { ResourceSuccess } from "./SuccessResult"; + +export class CheckRootContainerSuccess extends ResourceSuccess { + readonly type = "checkRootContainerSuccess" as const; + readonly isRootContainer: boolean; + + constructor(uri: string, isRootContainer: boolean) { + super(uri); + this.isRootContainer = isRootContainer; + } +} diff --git a/packages/solid/src/requester/results/success/CreateSuccess.ts b/packages/solid/src/requester/results/success/CreateSuccess.ts new file mode 100644 index 0000000..c6de236 --- /dev/null +++ b/packages/solid/src/requester/results/success/CreateSuccess.ts @@ -0,0 +1,11 @@ +import { ResourceSuccess } from "./SuccessResult"; + +export class CreateSuccess extends ResourceSuccess { + readonly type = "createSuccess"; + readonly didOverwrite: boolean; + + constructor(uri: string, didOverwrite: boolean) { + super(uri); + this.didOverwrite = didOverwrite; + } +} diff --git a/packages/solid/src/requester/results/success/DeleteSuccess.ts b/packages/solid/src/requester/results/success/DeleteSuccess.ts new file mode 100644 index 0000000..f7fd259 --- /dev/null +++ b/packages/solid/src/requester/results/success/DeleteSuccess.ts @@ -0,0 +1,11 @@ +import { ResourceSuccess } from "./SuccessResult"; + +export class DeleteSuccess extends ResourceSuccess { + readonly type = "deleteSuccess" as const; + readonly resourceExisted: boolean; + + constructor(uri: string, resourceExisted: boolean) { + super(uri); + this.resourceExisted = resourceExisted; + } +} diff --git a/packages/solid/src/requester/results/success/ReadSuccess.ts b/packages/solid/src/requester/results/success/ReadSuccess.ts new file mode 100644 index 0000000..65c5bb1 --- /dev/null +++ b/packages/solid/src/requester/results/success/ReadSuccess.ts @@ -0,0 +1,56 @@ +import { ResourceSuccess } from "./SuccessResult"; + +export abstract class ReadSuccess extends ResourceSuccess { + recalledFromMemory: boolean; + constructor(uri: string, recalledFromMemory: boolean) { + super(uri); + this.recalledFromMemory = recalledFromMemory; + } +} + +export class BinaryReadSuccess extends ReadSuccess { + readonly type = "binaryReadSuccess" as const; + readonly blob: Blob; + readonly mimeType: string; + + constructor( + uri: string, + recalledFromMemory: boolean, + blob: Blob, + mimeType: string, + ) { + super(uri, recalledFromMemory); + this.blob = blob; + this.mimeType = mimeType; + } +} + +export class DataReadSuccess extends ReadSuccess { + readonly type = "dataReadSuccess" as const; + + constructor(uri: string, recalledFromMemory: boolean) { + super(uri, recalledFromMemory); + } +} + +export class ContainerReadSuccess extends ReadSuccess { + readonly type = "containerReadSuccess" as const; + readonly isRootContainer: boolean; + + constructor( + uri: string, + recalledFromMemory: boolean, + isRootContainer: boolean, + ) { + super(uri, recalledFromMemory); + this.isRootContainer = isRootContainer; + } +} + +export class AbsentReadSuccess extends ReadSuccess { + readonly type = "absentReadSuccess" as const; + + constructor(uri: string, recalledFromMemory: boolean) { + super(uri, recalledFromMemory); + } +} diff --git a/packages/solid/src/requester/results/success/SuccessResult.ts b/packages/solid/src/requester/results/success/SuccessResult.ts new file mode 100644 index 0000000..512812d --- /dev/null +++ b/packages/solid/src/requester/results/success/SuccessResult.ts @@ -0,0 +1,30 @@ +import type { Container } from "../../../resource/Container"; +import type { Leaf } from "../../../resource/Leaf"; +import type { RequesterResult } from "../RequesterResult"; + +export abstract class SuccessResult implements RequesterResult { + readonly isError = false as const; + abstract readonly type: string; + resource?: Leaf | Container; +} + +export abstract class ResourceSuccess extends SuccessResult { + readonly uri: string; + + constructor(uri: string) { + super(); + this.uri = uri; + } +} + +export class AggregateSuccess< + SuccessType extends SuccessResult, +> extends SuccessResult { + readonly type = "aggregateError" as const; + readonly results: SuccessType[]; + + constructor(results: SuccessType[]) { + super(); + this.results = results; + } +} diff --git a/packages/solid/src/requester/results/success/Unfetched.ts b/packages/solid/src/requester/results/success/Unfetched.ts new file mode 100644 index 0000000..ec4b6a7 --- /dev/null +++ b/packages/solid/src/requester/results/success/Unfetched.ts @@ -0,0 +1,5 @@ +import { ResourceSuccess } from "./SuccessResult"; + +export class Unfetched extends ResourceSuccess { + readonly type = "unfetched" as const; +} diff --git a/packages/solid/src/requester/results/success/UpdateSuccess.ts b/packages/solid/src/requester/results/success/UpdateSuccess.ts new file mode 100644 index 0000000..512a8c5 --- /dev/null +++ b/packages/solid/src/requester/results/success/UpdateSuccess.ts @@ -0,0 +1,5 @@ +import { ResourceSuccess } from "./SuccessResult"; + +export class UpdateSuccess extends ResourceSuccess { + readonly type = "updateSuccess"; +} diff --git a/packages/solid/src/resource/Container.ts b/packages/solid/src/resource/Container.ts index 301c040..96f6d0d 100644 --- a/packages/solid/src/resource/Container.ts +++ b/packages/solid/src/resource/Container.ts @@ -1,84 +1,131 @@ import { namedNode } from "@rdfjs/data-model"; import { ContainerRequester } from "../requester/ContainerRequester"; -import { - AggregateError, - UnexpectedError, -} from "../requester/requestResults/ErrorResult"; -import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; import type { - CreateResultErrors, - CreateResultWithoutOverwriteErrors, + CheckRootResult, + CheckRootResultError, +} from "../requester/requests/checkRootContainer"; +import type { + ContainerCreateAndOverwriteResult, + ContainerCreateIfAbsentResult, + LeafCreateAndOverwriteResult, + LeafCreateIfAbsentResult, } from "../requester/requests/createDataResource"; -import type { DeleteResultError } from "../requester/requests/deleteResource"; -import type { ReadResultError } from "../requester/requests/readResource"; import type { - UploadResultError, - UploadResultWithoutOverwriteError, -} from "../requester/requests/uploadResource"; + DeleteResult, + DeleteResultError, +} from "../requester/requests/deleteResource"; +import type { + ReadContainerResult, + ReadResultError, +} from "../requester/requests/readResource"; +import { AggregateError } from "../requester/results/error/ErrorResult"; +import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError"; +import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; +import { + AbsentReadSuccess, + ContainerReadSuccess, +} from "../requester/results/success/ReadSuccess"; +import { AggregateSuccess } from "../requester/results/success/SuccessResult"; +import { Unfetched } from "../requester/results/success/Unfetched"; import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; import { getParentUri, ldpContains } from "../util/rdfUtils"; import type { ContainerUri, LeafUri } from "../util/uriTypes"; import type { Leaf } from "./Leaf"; +import type { SharedStatuses } from "./Resource"; import { Resource } from "./Resource"; +import { GetRootContainerSuccess } from "./resourceResults/GetRootContainerSuccess"; export class Container extends Resource { readonly uri: ContainerUri; protected requester: ContainerRequester; protected rootContainer: boolean | undefined; readonly type = "container" as const; + readonly isError = false as const; + status: + | SharedStatuses + | ReadContainerResult + | ContainerCreateAndOverwriteResult + | ContainerCreateIfAbsentResult + | CheckRootResult; constructor(uri: ContainerUri, context: SolidLdoDatasetContext) { super(context); this.uri = uri; this.requester = new ContainerRequester(uri, context); + this.status = new Unfetched(this.uri); } isRootContainer(): boolean | undefined { return this.rootContainer; } - private async checkIfIsRootContainer(): Promise< - CheckRootResultError | undefined - > { - if (this.rootContainer === undefined) { - const rootContainerResult = await this.requester.isRootContainer(); - if (typeof rootContainerResult !== "boolean") { - return rootContainerResult; - } - this.rootContainer = rootContainerResult; + // Read Methods + protected updateWithReadSuccess( + result: ContainerReadSuccess | AbsentReadSuccess, + ): void { + if (result.type === "containerReadSuccess") { + this.rootContainer = result.isRootContainer; } } - async getParentContainer(): Promise< - Container | CheckRootResultError | undefined + async read(): Promise { + return (await super.read()) as ReadContainerResult; + } + + protected toReadResult(): ReadContainerResult { + if (this.isAbsent()) { + return new AbsentReadSuccess(this.uri, true); + } else { + return new ContainerReadSuccess(this.uri, true, this.isRootContainer()!); + } + } + + async readIfUnfetched(): Promise { + return super.readIfUnfetched() as Promise; + } + + // Parent Container Methods + private async checkIfIsRootContainer(): Promise { + const rootContainerResult = await this.requester.isRootContainer(); + this.status = rootContainerResult; + if (rootContainerResult.isError) return rootContainerResult; + this.rootContainer = rootContainerResult.isRootContainer; + this.emit("update"); + return rootContainerResult; + } + + async getRootContainer(): Promise< + GetRootContainerSuccess | CheckRootResultError > { const checkResult = await this.checkIfIsRootContainer(); - if (checkResult) return checkResult; - if (this.rootContainer) return undefined; + if (checkResult.isError) return checkResult; + if (this.rootContainer) { + return new GetRootContainerSuccess(this); + } const parentUri = getParentUri(this.uri); if (!parentUri) { - return new UnexpectedError( + return new NoncompliantPodError( this.uri, - new Error("Resource does not have a root container"), + "Resource does not have a root container", ); } - return this.context.resourceStore.get(parentUri); + return this.context.resourceStore.get(parentUri).getRootContainer(); } - async getRootContainer(): Promise { + async getParentContainer(): Promise< + Container | CheckRootResultError | undefined + > { const checkResult = await this.checkIfIsRootContainer(); - if (checkResult) return checkResult; - if (this.rootContainer) { - return this; - } + if (checkResult.isError) return checkResult; + if (this.rootContainer) return undefined; const parentUri = getParentUri(this.uri); if (!parentUri) { - return new UnexpectedError( + return new NoncompliantPodError( this.uri, - new Error("Resource does not have a root container"), + `${this.uri} is not root does not have a parent container`, ); } - return this.context.resourceStore.get(parentUri).getRootContainer(); + return this.context.resourceStore.get(parentUri); } children(): (Leaf | Container)[] { @@ -100,99 +147,78 @@ export class Container extends Resource { return this.context.resourceStore.get(`${this.uri}${slug}`); } + // Child Creators createChildAndOverwrite( slug: ContainerUri, - ): Promise; - createChildAndOverwrite(slug: LeafUri): Promise; - createChildAndOverwrite(slug: string): Promise; + ): Promise; + createChildAndOverwrite(slug: LeafUri): Promise; createChildAndOverwrite( slug: string, - ): Promise { + ): Promise; + createChildAndOverwrite( + slug: string, + ): Promise { return this.child(slug).createAndOverwrite(); } - createChildIfAbsent( - slug: ContainerUri, - ): Promise; - createChildIfAbsent( - slug: LeafUri, - ): Promise; + createChildIfAbsent(slug: ContainerUri): Promise; + createChildIfAbsent(slug: LeafUri): Promise; createChildIfAbsent( slug: string, - ): Promise; + ): Promise; createChildIfAbsent( slug: string, - ): Promise { + ): Promise { return this.child(slug).createIfAbsent(); } async uploadChildAndOverwrite( - slug: string, + slug: LeafUri, blob: Blob, mimeType: string, - ): Promise { - const child = this.child(slug); - if (child.type === "leaf") { - return child.uploadAndOverwrite(blob, mimeType); - } - return new UnexpectedError( - child.uri, - new Error(`${slug} is not a leaf uri.`), - ); + ): Promise { + return this.child(slug).uploadAndOverwrite(blob, mimeType); } - async uploadIfAbsent( - slug: string, + async uploadChildIfAbsent( + slug: LeafUri, blob: Blob, mimeType: string, - ): Promise { - const child = this.child(slug); - if (child.type === "leaf") { - return child.uploadIfAbsent(blob, mimeType); - } - return new UnexpectedError( - child.uri, - new Error(`${slug} is not a leaf uri.`), - ); + ): Promise { + return this.child(slug).uploadIfAbsent(blob, mimeType); } async clear(): Promise< - AggregateError | this + | AggregateSuccess + | AggregateError > { const readResult = await this.read(); - if (readResult.type === "error") - return new AggregateError(this.uri, [readResult]); - const errors = ( + if (readResult.isError) return new AggregateError([readResult]); + const results = ( await Promise.all( this.children().map(async (child) => { - const deleteError = await child.delete(); - if (deleteError.type === "error") return deleteError; + return child.delete(); }), ) - ) - .flat() - .filter( - ( - value, - ): value is - | DeleteResultError - | AggregateError => !!value, - ); + ).flat(); + const errors = results.filter( + ( + value, + ): value is + | DeleteResultError + | AggregateError => value.isError, + ); if (errors.length > 0) { - return new AggregateError(this.uri, errors); + return new AggregateError(errors); } - return this; + return new AggregateSuccess(results as DeleteSuccess[]); } async delete(): Promise< - | this - | AggregateError - | DeleteResultError + DeleteResult | AggregateError > { const clearResult = await this.clear(); - if (clearResult.type === "error") return clearResult; - return this.parseResult(await this.requester.delete()) as - | this - | DeleteResultError; + if (clearResult.isError) return clearResult; + return this.handleDelete(); } } diff --git a/packages/solid/src/resource/Leaf.ts b/packages/solid/src/resource/Leaf.ts index 012041a..e2f3405 100644 --- a/packages/solid/src/resource/Leaf.ts +++ b/packages/solid/src/resource/Leaf.ts @@ -1,63 +1,58 @@ import type { DatasetChanges } from "@ldo/rdf-utils"; import type { Quad } from "@rdfjs/types"; import { LeafRequester } from "../requester/LeafRequester"; -import type { AbsentResult } from "../requester/requestResults/AbsentResult"; -import type { BinaryResult } from "../requester/requestResults/BinaryResult"; -import type { DataResult } from "../requester/requestResults/DataResult"; -import type { ErrorResult } from "../requester/requestResults/ErrorResult"; import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; -import type { DeleteResultError } from "../requester/requests/deleteResource"; -import type { UpdateResultError } from "../requester/requests/updateDataResource"; import type { - UploadResultError, - UploadResultWithoutOverwriteError, -} from "../requester/requests/uploadResource"; + LeafCreateAndOverwriteResult, + LeafCreateIfAbsentResult, +} from "../requester/requests/createDataResource"; +import type { DeleteResult } from "../requester/requests/deleteResource"; +import type { ReadLeafResult } from "../requester/requests/readResource"; +import type { UpdateResult } from "../requester/requests/updateDataResource"; +import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; +import { + BinaryReadSuccess, + DataReadSuccess, + AbsentReadSuccess, +} from "../requester/results/success/ReadSuccess"; +import type { ResourceSuccess } from "../requester/results/success/SuccessResult"; +import { Unfetched } from "../requester/results/success/Unfetched"; import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; import { getParentUri } from "../util/rdfUtils"; import type { LeafUri } from "../util/uriTypes"; import type { Container } from "./Container"; +import type { SharedStatuses } from "./Resource"; import { Resource } from "./Resource"; +import type { GetRootContainerSuccess } from "./resourceResults/GetRootContainerSuccess"; export class Leaf extends Resource { readonly uri: LeafUri; protected requester: LeafRequester; readonly type = "leaf" as const; + readonly isError = false as const; + status: + | SharedStatuses + | ReadLeafResult + | LeafCreateAndOverwriteResult + | LeafCreateIfAbsentResult + | UpdateResult; - protected binaryData: { data: Blob; mimeType: string } | undefined; + protected binaryData: { blob: Blob; mimeType: string } | undefined; constructor(uri: LeafUri, context: SolidLdoDatasetContext) { super(context); this.uri = uri; this.requester = new LeafRequester(uri, context); + this.status = new Unfetched(this.uri); } + // Getters + isUploading(): boolean { + return this.requester.isUploading(); + } isUpdating(): boolean { return this.requester.isUpdating(); } - - protected parseResult( - result: AbsentResult | BinaryResult | DataResult | PossibleErrors, - ): this | PossibleErrors { - if (result.type === "binary") { - this.binaryData = { - data: result.blob, - mimeType: result.mimeType, - }; - } else { - delete this.binaryData; - } - return super.parseResult(result); - } - - getParentContainer(): Container { - const parentUri = getParentUri(this.uri)!; - return this.context.resourceStore.get(parentUri); - } - getRootContainer(): Promise { - const parentUri = getParentUri(this.uri)!; - const parent = this.context.resourceStore.get(parentUri); - return parent.getRootContainer(); - } getMimeType(): string | undefined { return this.binaryData?.mimeType; } @@ -74,28 +69,100 @@ export class Leaf extends Resource { return !this.binaryData; } + // Read Methods + protected updateWithReadSuccess( + result: BinaryReadSuccess | DataReadSuccess | AbsentReadSuccess, + ): void { + super.updateWithReadSuccess(result); + if (result.type === "binaryReadSuccess") { + this.binaryData = { blob: result.blob, mimeType: result.mimeType }; + } else { + this.binaryData = undefined; + } + } + + async read(): Promise { + return (await super.read()) as ReadLeafResult; + } + + protected toReadResult(): ReadLeafResult { + if (this.isAbsent()) { + return new AbsentReadSuccess(this.uri, true); + } else if (this.isBinary()) { + return new BinaryReadSuccess( + this.uri, + true, + this.binaryData!.blob, + this.binaryData!.mimeType, + ); + } else { + return new DataReadSuccess(this.uri, true); + } + } + + async readIfUnfetched(): Promise { + return super.readIfUnfetched() as Promise; + } + + // Parent Container Methods + getParentContainer(): Container { + const parentUri = getParentUri(this.uri)!; + return this.context.resourceStore.get(parentUri); + } + getRootContainer(): Promise { + const parentUri = getParentUri(this.uri)!; + const parent = this.context.resourceStore.get(parentUri); + return parent.getRootContainer(); + } + + // Delete Methods + protected updateWithDeleteSuccess(_result: DeleteSuccess) { + this.binaryData = undefined; + } + + // Create Methods + protected updateWithCreateSuccess(_result: ResourceSuccess): void { + this.binaryData = undefined; + } + + // Upload Methods async uploadAndOverwrite( blob: Blob, mimeType: string, - ): Promise { - return this.parseResult(await this.requester.upload(blob, mimeType, true)); + ): Promise { + const result = await this.requester.upload(blob, mimeType, true); + this.status = result; + if (result.isError) return result; + super.updateWithCreateSuccess(result); + this.binaryData = { blob, mimeType }; + this.emitThisAndParent(); + return result; } async uploadIfAbsent( blob: Blob, mimeType: string, - ): Promise { - return this.parseResult(await this.requester.upload(blob, mimeType)); + ): Promise { + const result = await this.requester.upload(blob, mimeType); + this.status = result; + if (result.isError) return result; + super.updateWithCreateSuccess(result); + this.binaryData = { blob, mimeType }; + this.emitThisAndParent(); + return result; } - async update( - changes: DatasetChanges, - ): Promise { - return this.parseResult(await this.requester.updateDataResource(changes)); + async update(changes: DatasetChanges): Promise { + const result = await this.requester.updateDataResource(changes); + this.status = result; + if (result.isError) return result; + this.binaryData = undefined; + this.absent = false; + this.emitThisAndParent(); + return result; } - // Delete Method - async delete(): Promise { - return this.parseResult(await this.requester.delete()); + async delete(): Promise { + return this.handleDelete(); } } diff --git a/packages/solid/src/resource/Resource.ts b/packages/solid/src/resource/Resource.ts index d9b00d1..b14aa3b 100644 --- a/packages/solid/src/resource/Resource.ts +++ b/packages/solid/src/resource/Resource.ts @@ -1,27 +1,36 @@ import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; -import type { AbsentResult } from "../requester/requestResults/AbsentResult"; -import type { BinaryResult } from "../requester/requestResults/BinaryResult"; -import type { DataResult } from "../requester/requestResults/DataResult"; -import { type ErrorResult } from "../requester/requestResults/ErrorResult"; import type { - CreateResultErrors, - CreateResultWithoutOverwriteErrors, + ContainerCreateAndOverwriteResult, + ContainerCreateIfAbsentResult, + LeafCreateAndOverwriteResult, + LeafCreateIfAbsentResult, } from "../requester/requests/createDataResource"; -import type { ReadResultError } from "../requester/requests/readResource"; -import type { Container } from "./Container"; +import type { + ReadContainerResult, + ReadLeafResult, +} from "../requester/requests/readResource"; import type { Requester } from "../requester/Requester"; import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; -import type { - AccessRule, - AccessRuleChangeResult, - AccessRuleFetchError, - AccessRuleResult, -} from "../requester/requestResults/AccessRule"; -import { getAccessRules } from "../requester/requests/getAccessRules"; +import type { AccessRule } from "../requester/results/success/AccessRule"; +import type { SetAccessRulesResult } from "../requester/requests/setAccessRules"; import { setAccessRules } from "../requester/requests/setAccessRules"; import type TypedEmitter from "typed-emitter"; import EventEmitter from "events"; import { getParentUri } from "../util/rdfUtils"; +import type { RequesterResult } from "../requester/results/RequesterResult"; +import type { DeleteResult } from "../requester/requests/deleteResource"; +import { ReadSuccess } from "../requester/results/success/ReadSuccess"; +import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; +import type { ResourceSuccess } from "../requester/results/success/SuccessResult"; +import type { Unfetched } from "../requester/results/success/Unfetched"; +import type { CreateSuccess } from "../requester/results/success/CreateSuccess"; +import type { GetRootContainerSuccess } from "./resourceResults/GetRootContainerSuccess"; +import type { + ReadResourceSuccessContainerTypes, + ReadResourceSuccessLeafTypes, +} from "./resourceResults/ReadResourceSuccess"; + +export type SharedStatuses = Unfetched | DeleteResult | CreateSuccess; export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ update: () => void; @@ -30,6 +39,7 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ protected readonly context: SolidLdoDatasetContext; abstract readonly uri: string; abstract readonly type: string; + abstract status: RequesterResult; protected abstract readonly requester: Requester; protected didInitialFetch: boolean = false; protected absent: boolean | undefined; @@ -46,9 +56,6 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ isCreating(): boolean { return this.requester.isCreating(); } - isUploading(): boolean { - return this.requester.isUploading(); - } isReading(): boolean { return this.requester.isReading(); } @@ -73,62 +80,105 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ return this.absent === undefined ? undefined : !this.absent; } - protected parseResult( - result: AbsentResult | BinaryResult | DataResult | PossibleErrors, - ): this | PossibleErrors { - if (result.type === "error") { - return result; - } - - if (result.type === "absent") { - this.didInitialFetch = true; - this.absent = true; - } else { - this.didInitialFetch = true; - this.absent = false; - } + // Helper Methods + protected emitThisAndParent() { this.emit("update"); const parentUri = getParentUri(this.uri); if (parentUri) { const parentContainer = this.context.resourceStore.get(parentUri); parentContainer.emit("update"); } - return this; } // Read Methods - async read(): Promise { - return this.parseResult(await this.requester.read()); + protected updateWithReadSuccess(result: ReadSuccess) { + this.absent = result.type === "absentReadSuccess"; + this.didInitialFetch = true; + } + + async read(): Promise< + ReadResourceSuccessContainerTypes | ReadResourceSuccessLeafTypes + > { + const result = await this.requester.read(); + this.status = result; + if (result.isError) return result; + this.updateWithReadSuccess(result); + this.emitThisAndParent(); + return result; } - async readIfUnfetched(): Promise { + + protected abstract toReadResult(): ReadContainerResult | ReadLeafResult; + + async readIfUnfetched(): Promise { if (this.didInitialFetch) { - return this; + const readResult = this.toReadResult(); + this.status = readResult; + return readResult; } return this.read(); } + // Delete Methods + protected updateWithDeleteSuccess(_result: DeleteSuccess) { + this.absent = true; + this.didInitialFetch = true; + } + + protected async handleDelete(): Promise { + const result = await this.requester.delete(); + this.status = result; + if (result.isError) return result; + this.updateWithDeleteSuccess(result); + this.emitThisAndParent(); + return result; + } + // Create Methods - async createAndOverwrite(): Promise { - return this.parseResult(await this.requester.createDataResource(true)); + protected updateWithCreateSuccess(result: ResourceSuccess) { + this.absent = false; + this.didInitialFetch = true; + if (result instanceof ReadSuccess) { + this.updateWithReadSuccess(result); + } } - async createIfAbsent(): Promise { - return this.parseResult(await this.requester.createDataResource()); + async createAndOverwrite(): Promise< + ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult + > { + const result = await this.requester.createDataResource(true); + this.status = result; + if (result.isError) return result; + this.updateWithCreateSuccess(result); + this.emitThisAndParent(); + return result; + } + + async createIfAbsent(): Promise< + ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult + > { + const result = await this.requester.createDataResource(true); + this.status = result; + if (result.isError) return result; + this.updateWithCreateSuccess(result); + this.emitThisAndParent(); + return result; } // Parent Container Methods -- Remember to change for Container - abstract getRootContainer(): Promise; + abstract getRootContainer(): Promise< + GetRootContainerSuccess | CheckRootResultError + >; - async getAccessRules(): Promise { - return getAccessRules({ uri: this.uri, fetch: this.context.fetch }); - } + // Access Rules Methods + // async getAccessRules(): Promise { + // return getAccessRules({ uri: this.uri, fetch: this.context.fetch }); + // } async setAccessRules( newAccessRules: AccessRule, - ): Promise { - return setAccessRules( - { uri: this.uri, fetch: this.context.fetch }, - newAccessRules, - ); + ): Promise { + return setAccessRules(this.uri, newAccessRules, { + fetch: this.context.fetch, + }); } } diff --git a/packages/solid/src/resource/ResourceResult.ts b/packages/solid/src/resource/ResourceResult.ts new file mode 100644 index 0000000..5b840cf --- /dev/null +++ b/packages/solid/src/resource/ResourceResult.ts @@ -0,0 +1,10 @@ +import type { LeafCreateAndOverwriteResult } from "../requester/requests/createDataResource"; +import { createDataResource } from "../requester/requests/createDataResource"; +import type { RequesterResult } from "../requester/results/RequesterResult"; +import type { Container } from "./Container"; +import type { Leaf } from "./Leaf"; + +export type ResourceResult< + Result extends RequesterResult, + ResourceType extends Leaf | Container, +> = Result & { resource: ResourceType }; diff --git a/packages/solid/src/resource/resourceResults/CreateResourceSuccess.ts b/packages/solid/src/resource/resourceResults/CreateResourceSuccess.ts new file mode 100644 index 0000000..d1962ef --- /dev/null +++ b/packages/solid/src/resource/resourceResults/CreateResourceSuccess.ts @@ -0,0 +1,14 @@ +import { CreateSuccess } from "../../requester/results/success/CreateSuccess"; +import type { Container } from "../Container"; +import type { Leaf } from "../Leaf"; + +export class CreateResourceSuccess< + ResourceType extends Leaf | Container, +> extends CreateSuccess { + readonly createdResource: ResourceType; + + constructor(uri: string, didOverwrite: boolean, resource: ResourceType) { + super(uri, didOverwrite); + this.createdResource = resource; + } +} diff --git a/packages/solid/src/resource/resourceResults/DeleteResourceSuccess.ts b/packages/solid/src/resource/resourceResults/DeleteResourceSuccess.ts new file mode 100644 index 0000000..17ac5a2 --- /dev/null +++ b/packages/solid/src/resource/resourceResults/DeleteResourceSuccess.ts @@ -0,0 +1,14 @@ +import { DeleteSuccess } from "../../requester/results/success/DeleteSuccess"; +import type { Container } from "../Container"; +import type { Leaf } from "../Leaf"; + +export class DeleteResourceSuccess< + ResourceType extends Leaf | Container, +> extends DeleteSuccess { + readonly deletedResource: ResourceType; + + constructor(uri: string, resourceExisted: boolean, resource: ResourceType) { + super(uri, resourceExisted); + this.deletedResource = resource; + } +} diff --git a/packages/solid/src/resource/resourceResults/GetRootContainerSuccess.ts b/packages/solid/src/resource/resourceResults/GetRootContainerSuccess.ts new file mode 100644 index 0000000..99a5c7b --- /dev/null +++ b/packages/solid/src/resource/resourceResults/GetRootContainerSuccess.ts @@ -0,0 +1,12 @@ +import { SuccessResult } from "../../requester/results/success/SuccessResult"; +import type { Container } from "../Container"; + +export class GetRootContainerSuccess extends SuccessResult { + readonly type = "getRootContainerSuccess" as const; + readonly rootContainer: Container; + + constructor(rootContainer: Container) { + super(); + this.rootContainer = rootContainer; + } +} diff --git a/packages/solid/src/resource/resourceResults/ReadResourceSuccess.ts b/packages/solid/src/resource/resourceResults/ReadResourceSuccess.ts new file mode 100644 index 0000000..4184070 --- /dev/null +++ b/packages/solid/src/resource/resourceResults/ReadResourceSuccess.ts @@ -0,0 +1,72 @@ +import type { ReadResultError } from "../../requester/requests/readResource"; +import { + AbsentReadSuccess, + BinaryReadSuccess, + ContainerReadSuccess, + DataReadSuccess, +} from "../../requester/results/success/ReadSuccess"; +import type { Container } from "../Container"; +import type { Leaf } from "../Leaf"; + +export type ReadResourceSuccessContainerTypes = + | ContainerResourceReadSuccess + | AbsentResourceReadSuccess + | ReadResultError; +export type ReadResourceSuccessLeafTypes = + | DataResourceReadSuccess + | BinaryResourceReadSuccess + | AbsentResourceReadSuccess + | ReadResultError; + +export class DataResourceReadSuccess extends DataReadSuccess { + readonly readResource: Leaf; + + constructor(uri: string, recalledFromMemory: boolean, resource: Leaf) { + super(uri, recalledFromMemory); + this.readResource = resource; + } +} + +export class BinaryResourceReadSuccess extends BinaryReadSuccess { + readonly readResource: Leaf; + + constructor( + uri: string, + recalledFromMemory: boolean, + blob: Blob, + mimeType: string, + resource: Leaf, + ) { + super(uri, recalledFromMemory, blob, mimeType); + this.readResource = resource; + } +} + +export class ContainerResourceReadSuccess extends ContainerReadSuccess { + readonly readResource: Container; + + constructor( + uri: string, + recalledFromMemory: boolean, + isRootContainer: boolean, + resource: Container, + ) { + super(uri, recalledFromMemory, isRootContainer); + this.readResource = resource; + } +} + +export class AbsentResourceReadSuccess< + ResourceType extends Leaf | Container, +> extends AbsentReadSuccess { + readonly readResource: ResourceType; + + constructor( + uri: string, + recalledFromMemory: boolean, + resource: ResourceType, + ) { + super(uri, recalledFromMemory); + this.readResource = resource; + } +} diff --git a/packages/solid/src/resource/resourceResults/UpdateResourceSuccess.ts b/packages/solid/src/resource/resourceResults/UpdateResourceSuccess.ts new file mode 100644 index 0000000..e6e1306 --- /dev/null +++ b/packages/solid/src/resource/resourceResults/UpdateResourceSuccess.ts @@ -0,0 +1,11 @@ +import { UpdateSuccess } from "../../requester/results/success/UpdateSuccess"; +import type { Leaf } from "../Leaf"; + +export class UpdateResourceSuccess extends UpdateSuccess { + readonly updatedResource: Leaf; + + constructor(uri: string, resource: Leaf) { + super(uri); + this.updatedResource = resource; + } +} diff --git a/packages/solid/src/util/guaranteeFetch.ts b/packages/solid/src/util/guaranteeFetch.ts new file mode 100644 index 0000000..3d210de --- /dev/null +++ b/packages/solid/src/util/guaranteeFetch.ts @@ -0,0 +1,5 @@ +import crossFetch from "cross-fetch"; + +export function guaranteeFetch(fetchInput?: typeof fetch): typeof fetch { + return fetchInput || crossFetch; +} diff --git a/packages/solid/src/util/rdfUtils.ts b/packages/solid/src/util/rdfUtils.ts index 75f3cb2..4a05276 100644 --- a/packages/solid/src/util/rdfUtils.ts +++ b/packages/solid/src/util/rdfUtils.ts @@ -1,10 +1,10 @@ import { parseRdf } from "@ldo/ldo"; import { namedNode, quad as createQuad } from "@rdfjs/data-model"; -import { DataResult } from "../requester/requestResults/DataResult"; -import { TurtleFormattingError } from "../requester/requestResults/DataResult"; import type { Dataset } from "@rdfjs/types"; import type { ContainerUri } from "./uriTypes"; import { isContainerUri } from "./uriTypes"; +import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError"; +import { UnexpectedResourceError } from "../requester/results/error/ErrorResult"; export const ldpContains = namedNode("http://www.w3.org/ns/ldp#contains"); export const rdfType = namedNode( @@ -78,16 +78,17 @@ export async function addRawTurtleToDataset( rawTurtle: string, dataset: Dataset, baseUri: string, -): Promise { +): Promise { let loadedDataset: Dataset; try { loadedDataset = await parseRdf(rawTurtle, { baseIRI: baseUri, }); } catch (err) { - return new TurtleFormattingError( + const error = UnexpectedResourceError.fromThrown(baseUri, err); + return new NoncompliantPodError( baseUri, - err instanceof Error ? err.message : "Failed to parse rdf", + `Request at ${baseUri} returned noncompliant turtle: ${error.message}`, ); } @@ -100,5 +101,4 @@ export async function addRawTurtleToDataset( createQuad(quad.subject, quad.predicate, quad.object, graphNode), ), ); - return new DataResult(baseUri); }