Before interface refactor

main
Ailin Luca 2 years ago
parent 8eceab9f3d
commit 50c44354dd
  1. 10
      packages/demo-react/src/dashboard/BuildMainContainer.tsx
  2. 33
      packages/solid/src/SolidLdoDataset.ts
  3. 37
      packages/solid/src/requester/ContainerRequester.ts
  4. 93
      packages/solid/src/requester/LeafRequester.ts
  5. 151
      packages/solid/src/requester/Requester.ts
  6. 9
      packages/solid/src/requester/requestResults/AbsentResult.ts
  7. 51
      packages/solid/src/requester/requestResults/AccessRule.ts
  8. 18
      packages/solid/src/requester/requestResults/BinaryResult.ts
  9. 12
      packages/solid/src/requester/requestResults/CommitChangesSuccess.ts
  10. 27
      packages/solid/src/requester/requestResults/DataResult.ts
  11. 7
      packages/solid/src/requester/requestResults/RequesterResult.ts
  12. 63
      packages/solid/src/requester/requests/checkRootContainer.ts
  13. 128
      packages/solid/src/requester/requests/createDataResource.ts
  14. 38
      packages/solid/src/requester/requests/deleteResource.ts
  15. 10
      packages/solid/src/requester/requests/getAccessRules.ts
  16. 105
      packages/solid/src/requester/requests/readResource.ts
  17. 9
      packages/solid/src/requester/requests/requestOptions.ts
  18. 10
      packages/solid/src/requester/requests/requestParams.ts
  19. 28
      packages/solid/src/requester/requests/setAccessRules.ts
  20. 39
      packages/solid/src/requester/requests/updateDataResource.ts
  21. 73
      packages/solid/src/requester/requests/uploadResource.ts
  22. 8
      packages/solid/src/requester/results/RequesterResult.ts
  23. 68
      packages/solid/src/requester/results/error/ErrorResult.ts
  24. 10
      packages/solid/src/requester/results/error/HttpErrorResult.ts
  25. 5
      packages/solid/src/requester/results/error/InvalidUriError.ts
  26. 11
      packages/solid/src/requester/results/error/NoncompliantPodError.ts
  27. 20
      packages/solid/src/requester/results/success/AccessRule.ts
  28. 11
      packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts
  29. 11
      packages/solid/src/requester/results/success/CreateSuccess.ts
  30. 11
      packages/solid/src/requester/results/success/DeleteSuccess.ts
  31. 56
      packages/solid/src/requester/results/success/ReadSuccess.ts
  32. 30
      packages/solid/src/requester/results/success/SuccessResult.ts
  33. 5
      packages/solid/src/requester/results/success/Unfetched.ts
  34. 5
      packages/solid/src/requester/results/success/UpdateSuccess.ts
  35. 200
      packages/solid/src/resource/Container.ts
  36. 157
      packages/solid/src/resource/Leaf.ts
  37. 150
      packages/solid/src/resource/Resource.ts
  38. 10
      packages/solid/src/resource/ResourceResult.ts
  39. 14
      packages/solid/src/resource/resourceResults/CreateResourceSuccess.ts
  40. 14
      packages/solid/src/resource/resourceResults/DeleteResourceSuccess.ts
  41. 12
      packages/solid/src/resource/resourceResults/GetRootContainerSuccess.ts
  42. 72
      packages/solid/src/resource/resourceResults/ReadResourceSuccess.ts
  43. 11
      packages/solid/src/resource/resourceResults/UpdateResourceSuccess.ts
  44. 5
      packages/solid/src/util/guaranteeFetch.ts
  45. 12
      packages/solid/src/util/rdfUtils.ts

@ -18,12 +18,14 @@ export const BuildMainContainer: FunctionComponent<{
useEffect(() => { useEffect(() => {
if (session.webId) { if (session.webId) {
const webIdResource = getResource(session.webId as LeafUri); const webIdResource = getResource(session.webId as LeafUri);
webIdResource.getRootContainer().then(async (rootContainer) => { webIdResource.getRootContainer().then(async (rootContainerResult) => {
if (rootContainer.type === "error") { if (rootContainerResult.isError) {
alert(rootContainer.message); alert(rootContainerResult.message);
return; return;
} }
const mainContainer = getResource(`${rootContainer.uri}demo-react/`); const mainContainer = getResource(
`${rootContainerResult.rootContainer.uri}demo-react/`,
);
setMainContainer(mainContainer); setMainContainer(mainContainer);
await mainContainer.read(); await mainContainer.read();
if (mainContainer.isAbsent()) { if (mainContainer.isAbsent()) {

@ -1,10 +1,14 @@
import { LdoDataset } from "@ldo/ldo"; import { LdoDataset } from "@ldo/ldo";
import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils"; import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils";
import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types"; import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types";
import { CommitChangesSuccess } from "./requester/requestResults/CommitChangesSuccess"; import type {
import { InvalidUriError } from "./requester/requestResults/DataResult"; UpdateResult,
import { AggregateError } from "./requester/requestResults/ErrorResult"; UpdateResultError,
import type { UpdateResultError } from "./requester/requests/updateDataResource"; } 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 { Container } from "./resource/Container";
import type { Leaf } from "./resource/Leaf"; import type { Leaf } from "./resource/Leaf";
import type { ResourceGetterOptions } from "./ResourceStore"; import type { ResourceGetterOptions } from "./ResourceStore";
@ -35,20 +39,25 @@ export class SolidLdoDataset extends LdoDataset {
async commitChangesToPod( async commitChangesToPod(
changes: DatasetChanges<Quad>, changes: DatasetChanges<Quad>,
): Promise< ): Promise<
CommitChangesSuccess | AggregateError<UpdateResultError | InvalidUriError> | AggregateSuccess<UpdateSuccess>
| AggregateError<UpdateResultError | InvalidUriError>
> { > {
const changesByGraph = splitChangesByGraph(changes); const changesByGraph = splitChangesByGraph(changes);
const results: [ const results: [
GraphNode, GraphNode,
DatasetChanges<Quad>, DatasetChanges<Quad>,
UpdateResultError | InvalidUriError | Leaf | { type: "defaultGraph" }, UpdateResult | InvalidUriError | { type: "defaultGraph"; isError: false },
][] = await Promise.all( ][] = await Promise.all(
Array.from(changesByGraph.entries()).map( Array.from(changesByGraph.entries()).map(
async ([graph, datasetChanges]) => { async ([graph, datasetChanges]) => {
if (graph.termType === "DefaultGraph") { if (graph.termType === "DefaultGraph") {
// Undefined means that this is the default graph // Undefined means that this is the default graph
this.bulk(datasetChanges); this.bulk(datasetChanges);
return [graph, datasetChanges, { type: "defaultGraph" }]; return [
graph,
datasetChanges,
{ type: "defaultGraph", isError: false },
];
} }
if (isContainerUri(graph.value)) { if (isContainerUri(graph.value)) {
return [ return [
@ -67,20 +76,20 @@ export class SolidLdoDataset extends LdoDataset {
); );
// If one has errored, return error // 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) { if (errors.length > 0) {
return new AggregateError( return new AggregateError(
"",
errors.map( errors.map(
(result) => result[2] as UpdateResultError | InvalidUriError, (result) => result[2] as UpdateResultError | InvalidUriError,
), ),
); );
} }
return new CommitChangesSuccess( return new AggregateSuccess(
"",
results results
.map((result) => result[2]) .map((result) => result[2])
.filter((result): result is Leaf => result.type === "leaf"), .filter(
(result): result is UpdateSuccess => result.type === "updateSuccess",
),
); );
} }
} }

@ -1,14 +1,49 @@
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import type { ContainerUri } from "../util/uriTypes";
import { Requester } from "./Requester"; import { Requester } from "./Requester";
import type { CheckRootResult } from "./requests/checkRootContainer"; import type { CheckRootResult } from "./requests/checkRootContainer";
import { checkRootContainer } 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 const IS_ROOT_CONTAINER_KEY = "isRootContainer";
export class ContainerRequester extends Requester { export class ContainerRequester extends Requester {
readonly uri: ContainerUri;
constructor(uri: ContainerUri, context: SolidLdoDatasetContext) {
super(context);
this.uri = uri;
}
read(): Promise<ReadContainerResult> {
return super.read() as Promise<ReadContainerResult>;
}
createDataResource(
overwrite: true,
): Promise<ContainerCreateAndOverwriteResult>;
createDataResource(overwrite?: false): Promise<ContainerCreateIfAbsentResult>;
createDataResource(
overwrite?: boolean,
): Promise<ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult>;
createDataResource(
overwrite?: boolean,
): Promise<
ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult
> {
return super.createDataResource(overwrite) as Promise<
ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult
>;
}
async isRootContainer(): Promise<CheckRootResult> { async isRootContainer(): Promise<CheckRootResult> {
return this.requestBatcher.queueProcess({ return this.requestBatcher.queueProcess({
name: IS_ROOT_CONTAINER_KEY, name: IS_ROOT_CONTAINER_KEY,
args: [{ uri: this.uri, fetch: this.context.fetch }], args: [this.uri as ContainerUri, { fetch: this.context.fetch }],
perform: checkRootContainer, perform: checkRootContainer,
modifyQueue: (queue, isLoading) => { modifyQueue: (queue, isLoading) => {
if (queue.length === 0) { if (queue.length === 0) {

@ -1,17 +1,54 @@
import type { DatasetChanges } from "@ldo/rdf-utils"; import type { DatasetChanges } from "@ldo/rdf-utils";
import { mergeDatasetChanges } from "@ldo/subscribable-dataset"; import { mergeDatasetChanges } from "@ldo/subscribable-dataset";
import type { Quad } from "@rdfjs/types"; import type { Quad } from "@rdfjs/types";
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import type { LeafUri } from "../util/uriTypes";
import { Requester } from "./Requester"; import { Requester } from "./Requester";
import type {
LeafCreateAndOverwriteResult,
LeafCreateIfAbsentResult,
} from "./requests/createDataResource";
import type { ReadLeafResult } from "./requests/readResource";
import type { UpdateResult } from "./requests/updateDataResource"; import type { UpdateResult } from "./requests/updateDataResource";
import { updateDataResource } from "./requests/updateDataResource"; import { updateDataResource } from "./requests/updateDataResource";
import { uploadResource } from "./requests/uploadResource";
export const UPDATE_KEY = "update"; export const UPDATE_KEY = "update";
export const UPLOAD_KEY = "upload";
export class LeafRequester extends Requester { export class LeafRequester extends Requester {
readonly uri: LeafUri;
constructor(uri: LeafUri, context: SolidLdoDatasetContext) {
super(context);
this.uri = uri;
}
isUpdating(): boolean { isUpdating(): boolean {
return this.requestBatcher.isLoading(UPDATE_KEY); return this.requestBatcher.isLoading(UPDATE_KEY);
} }
isUploading(): boolean {
return this.requestBatcher.isLoading(UPLOAD_KEY);
}
async read(): Promise<ReadLeafResult> {
return super.read() as Promise<ReadLeafResult>;
}
createDataResource(overwrite: true): Promise<LeafCreateAndOverwriteResult>;
createDataResource(overwrite?: false): Promise<LeafCreateIfAbsentResult>;
createDataResource(
overwrite?: boolean,
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult>;
createDataResource(
overwrite?: boolean,
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult> {
return super.createDataResource(overwrite) as Promise<
LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult
>;
}
/** /**
* Update the data on this resource * Update the data on this resource
* @param changes * @param changes
@ -22,9 +59,9 @@ export class LeafRequester extends Requester {
const result = await this.requestBatcher.queueProcess({ const result = await this.requestBatcher.queueProcess({
name: UPDATE_KEY, name: UPDATE_KEY,
args: [ args: [
{ uri: this.uri, fetch: this.context.fetch }, this.uri,
changes, changes,
this.context.solidLdoDataset, { fetch: this.context.fetch, dataset: this.context.solidLdoDataset },
], ],
perform: updateDataResource, perform: updateDataResource,
modifyQueue: (queue, isLoading, [, changes]) => { modifyQueue: (queue, isLoading, [, changes]) => {
@ -39,4 +76,56 @@ export class LeafRequester extends Requester {
}); });
return result; 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<LeafCreateAndOverwriteResult>;
upload(
blob: Blob,
mimeType: string,
overwrite?: false,
): Promise<LeafCreateIfAbsentResult>;
upload(
blob: Blob,
mimeType: string,
overwrite?: boolean,
): Promise<LeafCreateAndOverwriteResult | LeafCreateIfAbsentResult>;
async upload(
blob: Blob,
mimeType: string,
overwrite?: boolean,
): Promise<LeafCreateAndOverwriteResult | LeafCreateIfAbsentResult> {
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;
}
} }

@ -1,34 +1,32 @@
import { ANY_KEY, RequestBatcher } from "../util/RequestBatcher"; import { ANY_KEY, RequestBatcher } from "../util/RequestBatcher";
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import type { import type {
CreateResult, ContainerCreateAndOverwriteResult,
CreateResultWithoutOverwrite, ContainerCreateIfAbsentResult,
LeafCreateAndOverwriteResult,
LeafCreateIfAbsentResult,
} from "./requests/createDataResource"; } from "./requests/createDataResource";
import { createDataResource } from "./requests/createDataResource"; import { createDataResource } from "./requests/createDataResource";
import type { ReadResult } from "./requests/readResource";
import { readResource } from "./requests/readResource";
import type { import type {
UploadResult, ReadContainerResult,
UploadResultWithoutOverwrite, ReadLeafResult,
} from "./requests/uploadResource"; } from "./requests/readResource";
import { uploadResource } from "./requests/uploadResource"; import { readResource } from "./requests/readResource";
import type { DeleteResult } from "./requests/deleteResource"; import type { DeleteResult } from "./requests/deleteResource";
import { deleteResource } from "./requests/deleteResource"; import { deleteResource } from "./requests/deleteResource";
const READ_KEY = "read"; const READ_KEY = "read";
const CREATE_KEY = "createDataResource"; const CREATE_KEY = "createDataResource";
const UPLOAD_KEY = "upload";
const DELETE_KEY = "delete"; const DELETE_KEY = "delete";
export abstract class Requester { export abstract class Requester {
protected readonly requestBatcher = new RequestBatcher(); protected readonly requestBatcher = new RequestBatcher();
// All intance variables // All intance variables
readonly uri: string; abstract readonly uri: string;
protected context: SolidLdoDatasetContext; protected context: SolidLdoDatasetContext;
constructor(uri: string, context: SolidLdoDatasetContext) { constructor(context: SolidLdoDatasetContext) {
this.uri = uri;
this.context = context; this.context = context;
} }
@ -38,9 +36,6 @@ export abstract class Requester {
isCreating(): boolean { isCreating(): boolean {
return this.requestBatcher.isLoading(CREATE_KEY); return this.requestBatcher.isLoading(CREATE_KEY);
} }
isUploading(): boolean {
return this.requestBatcher.isLoading(UPLOAD_KEY);
}
isReading(): boolean { isReading(): boolean {
return this.requestBatcher.isLoading(READ_KEY); return this.requestBatcher.isLoading(READ_KEY);
} }
@ -51,11 +46,11 @@ export abstract class Requester {
/** /**
* Read this resource. * Read this resource.
*/ */
async read(): Promise<ReadResult> { async read(): Promise<ReadLeafResult | ReadContainerResult> {
const transaction = this.context.solidLdoDataset.startTransaction(); const transaction = this.context.solidLdoDataset.startTransaction();
const result = await this.requestBatcher.queueProcess({ const result = await this.requestBatcher.queueProcess({
name: READ_KEY, name: READ_KEY,
args: [{ uri: this.uri, transaction, fetch: this.context.fetch }], args: [this.uri, { dataset: transaction, fetch: this.context.fetch }],
perform: readResource, perform: readResource,
modifyQueue: (queue, isLoading) => { modifyQueue: (queue, isLoading) => {
if (queue.length === 0) { if (queue.length === 0) {
@ -65,119 +60,81 @@ export abstract class Requester {
} }
}, },
}); });
if (result.type !== "error") { if (!result.isError) {
transaction.commit(); transaction.commit();
} }
return result; return result;
} }
/** /**
* Creates a Resource * Delete this resource
* @param overwrite: If true, this will orverwrite the resource if it already
* exists
*/ */
async createDataResource( async delete(): Promise<DeleteResult> {
overwrite?: false,
): Promise<CreateResultWithoutOverwrite>;
async createDataResource(overwrite: true): Promise<CreateResult>;
async createDataResource(
overwrite?: boolean,
): Promise<CreateResultWithoutOverwrite | CreateResult>;
async createDataResource(
overwrite?: boolean,
): Promise<CreateResultWithoutOverwrite> {
const transaction = this.context.solidLdoDataset.startTransaction(); const transaction = this.context.solidLdoDataset.startTransaction();
const result = await this.requestBatcher.queueProcess({ const result = await this.requestBatcher.queueProcess({
name: CREATE_KEY, name: DELETE_KEY,
args: [ args: [this.uri, { dataset: transaction, fetch: this.context.fetch }],
{ uri: this.uri, transaction, fetch: this.context.fetch }, perform: deleteResource,
overwrite, modifyQueue: (queue, isLoading) => {
], if (queue.length === 0) {
perform: createDataResource, return isLoading[DELETE_KEY];
modifyQueue: (queue, isLoading, args) => { } else {
const lastElementInQueue = queue[queue.length - 1]; return queue[queue.length - 1].name === DELETE_KEY;
return ( }
lastElementInQueue &&
lastElementInQueue.name === CREATE_KEY &&
!!lastElementInQueue.args[1] === !!args[1]
);
}, },
}); });
if (result.type !== "error") { if (!result.isError) {
transaction.commit(); transaction.commit();
} }
return result; return result;
} }
/** /**
* Upload a binary * Creates a Resource
* @param blob * @param overwrite: If true, this will orverwrite the resource if it already
* @param mimeType * exists
* @param overwrite: If true, will overwrite an existing file
*/ */
async upload( createDataResource(
blob: Blob,
mimeType: string,
overwrite?: false,
): Promise<UploadResultWithoutOverwrite>;
async upload(
blob: Blob,
mimeType: string,
overwrite: true, overwrite: true,
): Promise<UploadResult>; ): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult>;
async upload( createDataResource(
blob: Blob, overwrite?: false,
mimeType: string, ): Promise<ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult>;
createDataResource(
overwrite?: boolean, overwrite?: boolean,
): Promise<UploadResultWithoutOverwrite | UploadResult>; ): Promise<
async upload( | ContainerCreateAndOverwriteResult
blob: Blob, | LeafCreateAndOverwriteResult
mimeType: string, | ContainerCreateIfAbsentResult
| LeafCreateIfAbsentResult
>;
async createDataResource(
overwrite?: boolean, overwrite?: boolean,
): Promise<UploadResultWithoutOverwrite | UploadResult> { ): Promise<
| ContainerCreateAndOverwriteResult
| LeafCreateAndOverwriteResult
| ContainerCreateIfAbsentResult
| LeafCreateIfAbsentResult
> {
const transaction = this.context.solidLdoDataset.startTransaction(); const transaction = this.context.solidLdoDataset.startTransaction();
const result = await this.requestBatcher.queueProcess({ const result = await this.requestBatcher.queueProcess({
name: UPLOAD_KEY, name: CREATE_KEY,
args: [ args: [
{ uri: this.uri, transaction, fetch: this.context.fetch }, this.uri,
blob,
mimeType,
overwrite, overwrite,
{ dataset: transaction, fetch: this.context.fetch },
], ],
perform: uploadResource, perform: createDataResource,
modifyQueue: (queue, isLoading, args) => { modifyQueue: (queue, isLoading, args) => {
const lastElementInQueue = queue[queue.length - 1]; const lastElementInQueue = queue[queue.length - 1];
return ( return (
lastElementInQueue && lastElementInQueue &&
lastElementInQueue.name === UPLOAD_KEY && lastElementInQueue.name === CREATE_KEY &&
!!lastElementInQueue.args[3] === !!args[3] !!lastElementInQueue.args[1] === !!args[1]
); );
}, },
}); });
if (result.type !== "error") { if (!result.isError) {
transaction.commit();
}
return result;
}
/**
* Delete this resource
*/
async delete(): Promise<DeleteResult> {
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") {
transaction.commit(); transaction.commit();
} }
return result; return result;

@ -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;
}
}

@ -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<string, Access>;
}
export class AccessRuleChangeResult
extends RequesterResult
implements AccessRule
{
type = "accessRuleChange" as const;
readonly public?: Access;
readonly agent?: Record<string, Access>;
constructor(
uri: string,
publicRules?: Access,
agentRules?: Record<string, Access>,
) {
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<string, Access>;
constructor(
uri: string,
publicRules: Access,
agentRules: Record<string, Access>,
) {
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}.`);
}
}

@ -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";
}
}

@ -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;
}
}

@ -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.`);
}
}

@ -1,7 +0,0 @@
export abstract class RequesterResult {
readonly uri: string;
abstract readonly type: string;
constructor(uri: string) {
this.uri = uri;
}
}

@ -1,35 +1,52 @@
import { import type { BasicRequestOptions } from "./requestOptions";
UnexpectedHttpError,
type HttpErrorResultType,
} from "../requestResults/HttpErrorResult";
import { UnexpectedError } from "../requestResults/ErrorResult";
import type { SimpleRequestParams } from "./requestParams";
import { parse as parseLinkHeader } from "http-link-header"; 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 = boolean | CheckRootResultError; export type CheckRootResult = CheckRootContainerSuccess | CheckRootResultError;
export type CheckRootResultError = HttpErrorResultType | UnexpectedError; export type CheckRootResultError =
| HttpErrorResultType
| NoncompliantPodError
| UnexpectedHttpError
| UnexpectedResourceError;
export async function checkRootContainer({ export function checkHeadersForRootContainer(
uri, uri: ContainerUri,
fetch, headers: Headers,
}: SimpleRequestParams): Promise<CheckRootResult> { ): CheckRootContainerSuccess | NoncompliantPodError {
try { const linkHeader = headers.get("link");
// Fetch options to determine the document type
const response = await fetch(uri, { method: "HEAD" });
const linkHeader = response.headers.get("link");
if (!linkHeader) { if (!linkHeader) {
return new UnexpectedHttpError( return new NoncompliantPodError(uri, "No link header present in request.");
uri,
response,
"No link header present in request.",
);
} }
const parsedLinkHeader = parseLinkHeader(linkHeader); const parsedLinkHeader = parseLinkHeader(linkHeader);
const types = parsedLinkHeader.get("rel", "type"); const types = parsedLinkHeader.get("rel", "type");
return types.some( const isRoot = types.some(
(type) => type.uri === "http://www.w3.org/ns/pim/space#Storage", (type) => type.uri === "http://www.w3.org/ns/pim/space#Storage",
); );
return new CheckRootContainerSuccess(uri, isRoot);
}
export async function checkRootContainer(
uri: ContainerUri,
options?: BasicRequestOptions,
): Promise<CheckRootResult> {
try {
const fetch = guaranteeFetch(options?.fetch);
// Fetch options to determine the document type
const response = await fetch(uri, { method: "HEAD" });
const httpErrorResult = HttpErrorResult.checkResponse(uri, response);
if (httpErrorResult) return httpErrorResult;
return checkHeadersForRootContainer(uri, response.headers);
} catch (err) { } catch (err) {
return UnexpectedError.fromThrown(uri, err); return UnexpectedResourceError.fromThrown(uri, err);
} }
} }

@ -1,58 +1,116 @@
import { guaranteeFetch } from "../../util/guaranteeFetch";
import { import {
addResourceRdfToContainer, addResourceRdfToContainer,
getParentUri, getParentUri,
getSlug, getSlug,
} from "../../util/rdfUtils"; } from "../../util/rdfUtils";
import type { ContainerUri, LeafUri } from "../../util/uriTypes";
import { isContainerUri } from "../../util/uriTypes"; import { isContainerUri } from "../../util/uriTypes";
import type { BinaryResult } from "../requestResults/BinaryResult"; import { UnexpectedResourceError } from "../results/error/ErrorResult";
import type { TurtleFormattingError } from "../requestResults/DataResult"; import type { HttpErrorResultType } from "../results/error/HttpErrorResult";
import { DataResult } from "../requestResults/DataResult"; import { HttpErrorResult } from "../results/error/HttpErrorResult";
import { UnexpectedError } from "../requestResults/ErrorResult"; import { CreateSuccess } from "../results/success/CreateSuccess";
import type { HttpErrorResultType } from "../requestResults/HttpErrorResult"; import type { AbsentReadSuccess } from "../results/success/ReadSuccess";
import { HttpErrorResult } from "../requestResults/HttpErrorResult"; import type { DeleteResultError } from "./deleteResource";
import { deleteResource } from "./deleteResource"; import { deleteResource } from "./deleteResource";
import type {
ReadContainerResult,
ReadLeafResult,
ReadResultError,
} from "./readResource";
import { readResource } from "./readResource"; import { readResource } from "./readResource";
import type { RequestParams } from "./requestParams"; import type { DatasetRequestOptions } from "./requestOptions";
export type CreateResult = DataResult | CreateResultErrors; export type ContainerCreateAndOverwriteResult =
export type CreateResultErrors = HttpErrorResultType | UnexpectedError; | CreateSuccess
export type CreateResultWithoutOverwrite = | CreateAndOverwriteResultErrors;
| CreateResult export type LeafCreateAndOverwriteResult =
| CreateResultWithoutOverwriteErrors | CreateSuccess
| BinaryResult; | CreateAndOverwriteResultErrors;
export type CreateResultWithoutOverwriteErrors = export type ContainerCreateIfAbsentResult =
| TurtleFormattingError | CreateSuccess
| CreateResultErrors; | Exclude<ReadContainerResult, AbsentReadSuccess>
| CreateIfAbsentResultErrors;
export type LeafCreateIfAbsentResult =
| CreateSuccess
| Exclude<ReadLeafResult, AbsentReadSuccess>
| 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<ContainerCreateAndOverwriteResult>;
export function createDataResource(
uri: LeafUri,
overwrite: true,
options?: DatasetRequestOptions,
): Promise<LeafCreateAndOverwriteResult>;
export function createDataResource(
uri: ContainerUri,
overwrite?: false,
options?: DatasetRequestOptions,
): Promise<ContainerCreateIfAbsentResult>;
export function createDataResource( export function createDataResource(
params: RequestParams, uri: LeafUri,
overwrite?: false, overwrite?: false,
): Promise<CreateResultWithoutOverwrite>; options?: DatasetRequestOptions,
): Promise<LeafCreateIfAbsentResult>;
export function createDataResource( export function createDataResource(
params: RequestParams, uri: ContainerUri,
overwrite?: boolean,
options?: DatasetRequestOptions,
): Promise<ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult>;
export function createDataResource(
uri: LeafUri,
overwrite?: boolean,
options?: DatasetRequestOptions,
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult>;
export function createDataResource(
uri: string,
overwrite: true, overwrite: true,
): Promise<CreateResult>; options?: DatasetRequestOptions,
): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult>;
export function createDataResource( export function createDataResource(
params: RequestParams, uri: string,
overwrite?: false,
options?: DatasetRequestOptions,
): Promise<LeafCreateIfAbsentResult | LeafCreateIfAbsentResult>;
export function createDataResource(
uri: string,
overwrite?: boolean, overwrite?: boolean,
): Promise<CreateResultWithoutOverwrite | CreateResult>; options?: DatasetRequestOptions,
): Promise<
| ContainerCreateAndOverwriteResult
| LeafCreateAndOverwriteResult
| ContainerCreateIfAbsentResult
| LeafCreateIfAbsentResult
>;
export async function createDataResource( export async function createDataResource(
params: RequestParams, uri: string,
overwrite?: boolean, overwrite?: boolean,
): Promise<CreateResultWithoutOverwrite> { options?: DatasetRequestOptions,
const { uri, transaction, fetch } = params; ): Promise<
| ContainerCreateAndOverwriteResult
| LeafCreateAndOverwriteResult
| ContainerCreateIfAbsentResult
| LeafCreateIfAbsentResult
> {
try { try {
const fetch = guaranteeFetch(options?.fetch);
if (overwrite) { if (overwrite) {
const deleteResult = await deleteResource(params); const deleteResult = await deleteResource(uri, options);
// Return if it wasn't deleted // Return if it wasn't deleted
if (deleteResult.type !== "absent") { if (deleteResult.isError) return deleteResult;
return deleteResult;
}
} else { } else {
// Perform a read to check if it exists // 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 it does exist stop and return.
if (readResult.type !== "absent") { if (readResult.type !== "absentReadSuccess") {
return readResult; return readResult;
} }
} }
@ -73,9 +131,11 @@ export async function createDataResource(
const httpError = HttpErrorResult.checkResponse(uri, response); const httpError = HttpErrorResult.checkResponse(uri, response);
if (httpError) return httpError; if (httpError) return httpError;
addResourceRdfToContainer(uri, transaction); if (options?.dataset) {
return new DataResult(uri); addResourceRdfToContainer(uri, options.dataset);
}
return new CreateSuccess(uri, !!overwrite);
} catch (err) { } catch (err) {
return UnexpectedError.fromThrown(uri, err); return UnexpectedResourceError.fromThrown(uri, err);
} }
} }

@ -1,39 +1,45 @@
import { namedNode } from "@rdfjs/data-model"; import { namedNode } from "@rdfjs/data-model";
import { AbsentResult } from "../requestResults/AbsentResult"; import { guaranteeFetch } from "../../util/guaranteeFetch";
import { UnexpectedError } from "../requestResults/ErrorResult";
import type { HttpErrorResultType } from "../requestResults/HttpErrorResult";
import { UnexpectedHttpError } from "../requestResults/HttpErrorResult";
import type { RequestParams } from "./requestParams";
import { deleteResourceRdfFromContainer } from "../../util/rdfUtils"; 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 DeleteResult = DeleteSuccess | DeleteResultError;
export type DeleteResultError = HttpErrorResultType | UnexpectedError; export type DeleteResultError = HttpErrorResultType | UnexpectedResourceError;
export async function deleteResource({ export async function deleteResource(
uri, uri: string,
fetch, options?: DatasetRequestOptions,
transaction, ): Promise<DeleteResult> {
}: RequestParams): Promise<DeleteResult> {
try { try {
const fetch = guaranteeFetch(options?.fetch);
const response = await fetch(uri, { const response = await fetch(uri, {
method: "delete", method: "delete",
}); });
const errorResult = HttpErrorResult.checkResponse(uri, response);
if (errorResult) return errorResult;
// Specifically check for a 205. Annoyingly, the server will return 200 even // 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 // if it hasn't been deleted when you're unauthenticated. 404 happens when
// the document never existed // the document never existed
if (response.status === 205 || response.status === 404) { if (response.status === 205 || response.status === 404) {
transaction.deleteMatches( if (options?.dataset) {
options.dataset.deleteMatches(
undefined, undefined,
undefined, undefined,
undefined, undefined,
namedNode(uri), namedNode(uri),
); );
deleteResourceRdfFromContainer(uri, transaction); deleteResourceRdfFromContainer(uri, options.dataset);
return new AbsentResult(uri); }
return new DeleteSuccess(uri, response.status === 205);
} }
return new UnexpectedHttpError(uri, response); return new UnexpectedHttpError(uri, response);
} catch (err) { } catch (err) {
return UnexpectedError.fromThrown(uri, err); return UnexpectedResourceError.fromThrown(uri, err);
} }
} }

@ -1,12 +1,6 @@
import type { import type { AccessRuleFetchError } from "../results/success/AccessRule";
AccessRuleFetchError,
AccessRuleResult,
} from "../requestResults/AccessRule";
import type { SimpleRequestParams } from "./requestParams";
export async function getAccessRules( export async function getAccessRules(): Promise<AccessRuleFetchError> {
_params: SimpleRequestParams,
): Promise<AccessRuleResult | AccessRuleFetchError> {
throw new Error("Not Implemented"); throw new Error("Not Implemented");
// const [publicAccess, agentAccess] = await Promise.all([ // const [publicAccess, agentAccess] = await Promise.all([
// universalAccess.getPublicAccess(uri, { fetch }), // universalAccess.getPublicAccess(uri, { fetch }),

@ -1,64 +1,103 @@
import { DataResult } from "../requestResults/DataResult"; import type { UnexpectedHttpError } from "../results/error/HttpErrorResult";
import type { TurtleFormattingError } from "../requestResults/DataResult";
import { import {
HttpErrorResult, HttpErrorResult,
ServerHttpError,
type HttpErrorResultType, type HttpErrorResultType,
} from "../requestResults/HttpErrorResult"; } from "../results/error/HttpErrorResult";
import { UnexpectedError } from "../requestResults/ErrorResult";
import { AbsentResult } from "../requestResults/AbsentResult";
import { BinaryResult } from "../requestResults/BinaryResult";
import { import {
addRawTurtleToDataset, addRawTurtleToDataset,
addResourceRdfToContainer, addResourceRdfToContainer,
} from "../../util/rdfUtils"; } 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 = export type ReadLeafResult =
| AbsentResult | BinaryReadSuccess
| DataResult | DataReadSuccess
| BinaryResult | AbsentReadSuccess
| ReadResultError;
export type ReadContainerResult =
| ContainerReadSuccess
| AbsentReadSuccess
| ReadResultError; | ReadResultError;
export type ReadResultError = export type ReadResultError =
| HttpErrorResultType | HttpErrorResultType
| TurtleFormattingError | NoncompliantPodError
| UnexpectedError; | UnexpectedHttpError
| UnexpectedResourceError;
export async function readResource({ export async function readResource(
uri, uri: LeafUri,
fetch, options?: DatasetRequestOptions,
transaction, ): Promise<ReadLeafResult>;
}: RequestParams): Promise<ReadResult> { export async function readResource(
uri: ContainerUri,
options?: DatasetRequestOptions,
): Promise<ReadContainerResult>;
export async function readResource(
uri: string,
options?: DatasetRequestOptions,
): Promise<ReadLeafResult | ReadContainerResult>;
export async function readResource(
uri: string,
options?: DatasetRequestOptions,
): Promise<ReadLeafResult | ReadContainerResult> {
try { try {
const fetch = guaranteeFetch(options?.fetch);
// Fetch options to determine the document type // Fetch options to determine the document type
const response = await fetch(uri); const response = await fetch(uri);
if (AbsentResult.is(response)) { if (response.status === 404) {
return new AbsentResult(uri); return new AbsentReadSuccess(uri, false);
} }
const httpErrorResult = HttpErrorResult.checkResponse(uri, response); const httpErrorResult = HttpErrorResult.checkResponse(uri, response);
if (httpErrorResult) return httpErrorResult; if (httpErrorResult) return httpErrorResult;
// Add this resource to the container // Add this resource to the container
addResourceRdfToContainer(uri, transaction); if (options?.dataset) {
addResourceRdfToContainer(uri, options.dataset);
}
if (DataResult.is(response)) {
// Parse Turtle
const rawTurtle = await response.text();
return addRawTurtleToDataset(rawTurtle, transaction, uri);
} else {
// Load Blob
const contentType = response.headers.get("content-type"); const contentType = response.headers.get("content-type");
if (!contentType) { if (!contentType) {
return new ServerHttpError( return new NoncompliantPodError(
uri, uri,
response, "Resource requests must return a content-type header.",
"Server provided no content-type",
); );
} }
if (contentType === "text/turtle") {
// Parse Turtle
const rawTurtle = await response.text();
if (options?.dataset) {
const result = await addRawTurtleToDataset(
rawTurtle,
options.dataset,
uri,
);
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(); const blob = await response.blob();
return new BinaryResult(uri, blob, contentType); return new BinaryReadSuccess(uri, false, blob, contentType);
} }
} catch (err) { } catch (err) {
return UnexpectedError.fromThrown(uri, err); return UnexpectedResourceError.fromThrown(uri, err);
} }
} }

@ -0,0 +1,9 @@
import type { Dataset, Quad } from "@rdfjs/types";
export interface BasicRequestOptions {
fetch?: typeof fetch;
}
export interface DatasetRequestOptions extends BasicRequestOptions {
dataset?: Dataset<Quad>;
}

@ -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<Quad>;
}
export type SimpleRequestParams = Omit<RequestParams, "transaction">;

@ -1,6 +1,4 @@
import type { AclDataset, WithChangeLog } from "@inrupt/solid-client"; import type { AclDataset, WithChangeLog } from "@inrupt/solid-client";
import { getAgentAccessAll } from "@inrupt/solid-client";
import { getPublicAccess } from "@inrupt/solid-client";
import { import {
getSolidDatasetWithAcl, getSolidDatasetWithAcl,
hasResourceAcl, hasResourceAcl,
@ -14,17 +12,25 @@ import {
setPublicResourceAccess, setPublicResourceAccess,
setAgentDefaultAccess, setAgentDefaultAccess,
} from "@inrupt/solid-client"; } from "@inrupt/solid-client";
import { guaranteeFetch } from "../../util/guaranteeFetch";
import { isContainerUri } from "../../util/uriTypes"; import { isContainerUri } from "../../util/uriTypes";
import type { AccessRule } from "../requestResults/AccessRule"; import type { AccessRule } from "../results/success/AccessRule";
import { AccessRuleChangeResult } from "../requestResults/AccessRule"; import { SetAccessRuleSuccess } from "../results/success/AccessRule";
import { AccessRuleFetchError } from "../requestResults/AccessRule"; import { AccessRuleFetchError } from "../results/success/AccessRule";
import type { SimpleRequestParams } from "./requestParams"; import type { BasicRequestOptions } from "./requestOptions";
export type SetAccessRulesResult =
| SetAccessRuleSuccess
| SetAccessRulesResultError;
export type SetAccessRulesResultError = AccessRuleFetchError;
export async function setAccessRules( export async function setAccessRules(
{ uri, fetch }: SimpleRequestParams, uri: string,
newAccessRules: AccessRule, newAccessRules: AccessRule,
): Promise<AccessRuleChangeResult | AccessRuleFetchError> { options?: BasicRequestOptions,
): Promise<SetAccessRulesResult> {
console.warn("Access Control is stil underdeveloped. Use with caution."); console.warn("Access Control is stil underdeveloped. Use with caution.");
const fetch = guaranteeFetch(options?.fetch);
const isContainer = isContainerUri(uri); const isContainer = isContainerUri(uri);
// Code Copied from https://docs.inrupt.com/developer-tools/javascript/client-libraries/tutorial/manage-wac/ // 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: // Now save the ACL:
await saveAclFor(myDatasetWithAcl, updatedAcl, { fetch }); await saveAclFor(myDatasetWithAcl, updatedAcl, { fetch });
return new AccessRuleChangeResult( return new SetAccessRuleSuccess(uri);
uri,
getPublicAccess(myDatasetWithAcl) || undefined,
getAgentAccessAll(myDatasetWithAcl) || undefined,
);
} }

@ -1,28 +1,37 @@
import type { DatasetChanges } from "@ldo/rdf-utils"; import type { DatasetChanges } from "@ldo/rdf-utils";
import { changesToSparqlUpdate } from "@ldo/rdf-utils"; import { changesToSparqlUpdate } from "@ldo/rdf-utils";
import { DataResult } from "../requestResults/DataResult"; import type {
import type { HttpErrorResultType } from "../requestResults/HttpErrorResult"; SubscribableDataset,
import { HttpErrorResult } from "../requestResults/HttpErrorResult"; TransactionalDataset,
import { UnexpectedError } from "../requestResults/ErrorResult"; } from "@ldo/subscribable-dataset";
import type { SimpleRequestParams } from "./requestParams";
import type { SubscribableDataset } from "@ldo/subscribable-dataset";
import type { Quad } from "@rdfjs/types"; 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 UpdateResult = UpdateSuccess | UpdateResultError;
export type UpdateResultError = HttpErrorResultType | UnexpectedError; export type UpdateResultError = HttpErrorResultType | UnexpectedResourceError;
export async function updateDataResource( export async function updateDataResource(
{ uri, fetch }: SimpleRequestParams, uri: LeafUri,
datasetChanges: DatasetChanges<Quad>, datasetChanges: DatasetChanges<Quad>,
mainDataset: SubscribableDataset<Quad>, options?: BasicRequestOptions & { dataset?: SubscribableDataset<Quad> },
): Promise<UpdateResult> { ): Promise<UpdateResult> {
try { try {
const fetch = guaranteeFetch(options?.fetch);
// Put Changes in transactional dataset // Put Changes in transactional dataset
const transaction = mainDataset.startTransaction(); let transaction: TransactionalDataset<Quad> | undefined;
if (options?.dataset) {
transaction = options.dataset.startTransaction();
transaction.addAll(datasetChanges.added || []); transaction.addAll(datasetChanges.added || []);
datasetChanges.removed?.forEach((quad) => transaction.delete(quad)); datasetChanges.removed?.forEach((quad) => transaction!.delete(quad));
// Commit data optimistically // Commit data optimistically
transaction.commit(); transaction.commit();
}
// Make request // Make request
const sparqlUpdate = await changesToSparqlUpdate(datasetChanges); const sparqlUpdate = await changesToSparqlUpdate(datasetChanges);
const response = await fetch(uri, { const response = await fetch(uri, {
@ -35,11 +44,13 @@ export async function updateDataResource(
const httpError = HttpErrorResult.checkResponse(uri, response); const httpError = HttpErrorResult.checkResponse(uri, response);
if (httpError) { if (httpError) {
// Handle error rollback // Handle error rollback
if (transaction) {
transaction.rollback(); transaction.rollback();
}
return httpError; return httpError;
} }
return new DataResult(uri); return new UpdateSuccess(uri);
} catch (err) { } catch (err) {
return UnexpectedError.fromThrown(uri, err); return UnexpectedResourceError.fromThrown(uri, err);
} }
} }

@ -1,69 +1,60 @@
import { guaranteeFetch } from "../../util/guaranteeFetch";
import { import {
addResourceRdfToContainer, addResourceRdfToContainer,
getParentUri, getParentUri,
getSlug, getSlug,
} from "../../util/rdfUtils"; } 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 { import type {
DataResult, LeafCreateAndOverwriteResult,
TurtleFormattingError, LeafCreateIfAbsentResult,
} from "../requestResults/DataResult"; } from "./createDataResource";
import { UnexpectedError } from "../requestResults/ErrorResult";
import {
HttpErrorResult,
type HttpErrorResultType,
} from "../requestResults/HttpErrorResult";
import { deleteResource } from "./deleteResource"; import { deleteResource } from "./deleteResource";
import { readResource } from "./readResource"; import { readResource } from "./readResource";
import type { RequestParams } from "./requestParams"; import type { DatasetRequestOptions } from "./requestOptions";
export type UploadResult = BinaryResult | UploadResultError;
export type UploadResultError = HttpErrorResultType | UnexpectedError;
export type UploadResultWithoutOverwrite =
| UploadResult
| UploadResultWithoutOverwriteError
| DataResult;
export type UploadResultWithoutOverwriteError =
| UploadResultError
| TurtleFormattingError;
export function uploadResource( export function uploadResource(
params: RequestParams, uri: LeafUri,
blob: Blob, blob: Blob,
mimeType: string, mimeType: string,
overwrite?: false, overwrite: true,
): Promise<UploadResultWithoutOverwrite>; options?: DatasetRequestOptions,
): Promise<LeafCreateAndOverwriteResult>;
export function uploadResource( export function uploadResource(
params: RequestParams, uri: LeafUri,
blob: Blob, blob: Blob,
mimeType: string, mimeType: string,
overwrite: true, overwrite?: false,
): Promise<UploadResult>; options?: DatasetRequestOptions,
): Promise<LeafCreateIfAbsentResult>;
export function uploadResource( export function uploadResource(
params: RequestParams, uri: LeafUri,
blob: Blob, blob: Blob,
mimeType: string, mimeType: string,
overwrite?: boolean, overwrite?: boolean,
): Promise<UploadResultWithoutOverwrite>; options?: DatasetRequestOptions,
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult>;
export async function uploadResource( export async function uploadResource(
params: RequestParams, uri: LeafUri,
blob: Blob, blob: Blob,
mimeType: string, mimeType: string,
overwrite?: boolean, overwrite?: boolean,
): Promise<UploadResultWithoutOverwrite> { options?: DatasetRequestOptions,
const { uri, transaction, fetch } = params; ): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult> {
try { try {
const fetch = guaranteeFetch(options?.fetch);
if (overwrite) { if (overwrite) {
const deleteResult = await deleteResource(params); const deleteResult = await deleteResource(uri, options);
// Return if it wasn't deleted // Return if it wasn't deleted
if (deleteResult.type !== "absent") { if (deleteResult.isError) return deleteResult;
return deleteResult;
}
} else { } else {
// Perform a read to check if it exists // 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 it does exist stop and return.
if (readResult.type !== "absent") { if (readResult.type !== "absentReadSuccess") {
return readResult; return readResult;
} }
} }
@ -81,9 +72,11 @@ export async function uploadResource(
const httpError = HttpErrorResult.checkResponse(uri, response); const httpError = HttpErrorResult.checkResponse(uri, response);
if (httpError) return httpError; if (httpError) return httpError;
addResourceRdfToContainer(uri, transaction); if (options?.dataset) {
return new BinaryResult(uri, blob, mimeType); addResourceRdfToContainer(uri, options.dataset);
}
return new CreateSuccess(uri, !!overwrite);
} catch (err) { } catch (err) {
return UnexpectedError.fromThrown(uri, err); return UnexpectedResourceError.fromThrown(uri, err);
} }
} }

@ -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;
}

@ -1,43 +1,31 @@
export abstract class ErrorResult extends Error { import type { Container } from "../../../resource/Container";
readonly type = "error" as const; import type { Leaf } from "../../../resource/Leaf";
readonly uri: string; import type { RequesterResult } from "../RequesterResult";
abstract readonly errorType: string;
constructor(uri: string, message?: string) { export abstract class ErrorResult extends Error implements RequesterResult {
super(message || "An error unkown error was encountered during a request."); abstract type: string;
this.uri = uri; readonly isError = true as const;
resource?: Leaf | Container;
constructor(message?: string) {
super(message || "An error unkown error was encountered.");
} }
} }
export class UnexpectedError extends ErrorResult { export abstract class ResourceError extends ErrorResult {
error: Error; readonly uri: string;
readonly errorType = "unexpected" as const;
constructor(uri: string, error: Error) {
super(uri, error.message);
this.error = error;
}
static fromThrown(uri: string, err: unknown) { constructor(uri: string, message?: string) {
if (err instanceof Error) { super(message || `An error unkown error for ${uri}`);
return new UnexpectedError(uri, err); this.uri = uri;
} 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}`),
);
}
} }
} }
export class AggregateError<ErrorType extends ErrorResult> extends ErrorResult { export class AggregateError<ErrorType extends ErrorResult> extends ErrorResult {
readonly errorType = "aggregate" as const; readonly type = "aggregateError" as const;
readonly errors: ErrorType[]; readonly errors: ErrorType[];
constructor( constructor(
uri: string,
errors: (ErrorType | AggregateError<ErrorType>)[], errors: (ErrorType | AggregateError<ErrorType>)[],
message?: string, message?: string,
) { ) {
@ -52,7 +40,6 @@ export class AggregateError<ErrorType extends ErrorResult> extends ErrorResult {
} }
}); });
super( super(
uri,
message || message ||
`Encountered multiple errors:${allErrors.reduce( `Encountered multiple errors:${allErrors.reduce(
(agg, cur) => `${agg}\n${cur}`, (agg, cur) => `${agg}\n${cur}`,
@ -62,3 +49,26 @@ export class AggregateError<ErrorType extends ErrorResult> extends ErrorResult {
this.errors = allErrors; 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}`),
);
}
}
}

@ -1,11 +1,11 @@
import { ErrorResult } from "./ErrorResult"; import { ResourceError } from "./ErrorResult";
export type HttpErrorResultType = export type HttpErrorResultType =
| ServerHttpError | ServerHttpError
| UnexpectedHttpError | UnexpectedHttpError
| UnauthenticatedHttpError; | UnauthenticatedHttpError;
export abstract class HttpErrorResult extends ErrorResult { export abstract class HttpErrorResult extends ResourceError {
public readonly status: number; public readonly status: number;
public readonly headers: Headers; public readonly headers: Headers;
public readonly response: Response; public readonly response: Response;
@ -47,11 +47,11 @@ export abstract class HttpErrorResult extends ErrorResult {
} }
export class UnexpectedHttpError extends HttpErrorResult { export class UnexpectedHttpError extends HttpErrorResult {
errorType = "unexpectedHttp" as const; readonly type = "unexpectedHttpError" as const;
} }
export class UnauthenticatedHttpError extends HttpErrorResult { export class UnauthenticatedHttpError extends HttpErrorResult {
errorType = "unauthenticated" as const; readonly type = "unauthenticatedError" as const;
static is(response: Response) { static is(response: Response) {
return response.status === 401; return response.status === 401;
@ -59,7 +59,7 @@ export class UnauthenticatedHttpError extends HttpErrorResult {
} }
export class ServerHttpError extends HttpErrorResult { export class ServerHttpError extends HttpErrorResult {
errorType = "server" as const; readonly type = "serverError" as const;
static is(response: Response) { static is(response: Response) {
return response.status >= 500 && response.status < 600; return response.status >= 500 && response.status < 600;

@ -0,0 +1,5 @@
import { ResourceError } from "./ErrorResult";
export class InvalidUriError extends ResourceError {
readonly type = "invalidUriError" as const;
}

@ -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}`,
);
}
}

@ -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<string, Access>;
}
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}.`);
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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);
}
}

@ -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;
}
}

@ -0,0 +1,5 @@
import { ResourceSuccess } from "./SuccessResult";
export class Unfetched extends ResourceSuccess {
readonly type = "unfetched" as const;
}

@ -0,0 +1,5 @@
import { ResourceSuccess } from "./SuccessResult";
export class UpdateSuccess extends ResourceSuccess {
readonly type = "updateSuccess";
}

@ -1,84 +1,131 @@
import { namedNode } from "@rdfjs/data-model"; import { namedNode } from "@rdfjs/data-model";
import { ContainerRequester } from "../requester/ContainerRequester"; import { ContainerRequester } from "../requester/ContainerRequester";
import {
AggregateError,
UnexpectedError,
} from "../requester/requestResults/ErrorResult";
import type { CheckRootResultError } from "../requester/requests/checkRootContainer";
import type { import type {
CreateResultErrors, CheckRootResult,
CreateResultWithoutOverwriteErrors, CheckRootResultError,
} from "../requester/requests/checkRootContainer";
import type {
ContainerCreateAndOverwriteResult,
ContainerCreateIfAbsentResult,
LeafCreateAndOverwriteResult,
LeafCreateIfAbsentResult,
} from "../requester/requests/createDataResource"; } from "../requester/requests/createDataResource";
import type { DeleteResultError } from "../requester/requests/deleteResource";
import type { ReadResultError } from "../requester/requests/readResource";
import type { import type {
UploadResultError, DeleteResult,
UploadResultWithoutOverwriteError, DeleteResultError,
} from "../requester/requests/uploadResource"; } 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 type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import { getParentUri, ldpContains } from "../util/rdfUtils"; import { getParentUri, ldpContains } from "../util/rdfUtils";
import type { ContainerUri, LeafUri } from "../util/uriTypes"; import type { ContainerUri, LeafUri } from "../util/uriTypes";
import type { Leaf } from "./Leaf"; import type { Leaf } from "./Leaf";
import type { SharedStatuses } from "./Resource";
import { Resource } from "./Resource"; import { Resource } from "./Resource";
import { GetRootContainerSuccess } from "./resourceResults/GetRootContainerSuccess";
export class Container extends Resource { export class Container extends Resource {
readonly uri: ContainerUri; readonly uri: ContainerUri;
protected requester: ContainerRequester; protected requester: ContainerRequester;
protected rootContainer: boolean | undefined; protected rootContainer: boolean | undefined;
readonly type = "container" as const; readonly type = "container" as const;
readonly isError = false as const;
status:
| SharedStatuses
| ReadContainerResult
| ContainerCreateAndOverwriteResult
| ContainerCreateIfAbsentResult
| CheckRootResult;
constructor(uri: ContainerUri, context: SolidLdoDatasetContext) { constructor(uri: ContainerUri, context: SolidLdoDatasetContext) {
super(context); super(context);
this.uri = uri; this.uri = uri;
this.requester = new ContainerRequester(uri, context); this.requester = new ContainerRequester(uri, context);
this.status = new Unfetched(this.uri);
} }
isRootContainer(): boolean | undefined { isRootContainer(): boolean | undefined {
return this.rootContainer; return this.rootContainer;
} }
private async checkIfIsRootContainer(): Promise< // Read Methods
CheckRootResultError | undefined protected updateWithReadSuccess(
> { result: ContainerReadSuccess | AbsentReadSuccess,
if (this.rootContainer === undefined) { ): void {
const rootContainerResult = await this.requester.isRootContainer(); if (result.type === "containerReadSuccess") {
if (typeof rootContainerResult !== "boolean") { this.rootContainer = result.isRootContainer;
return rootContainerResult;
} }
this.rootContainer = rootContainerResult;
} }
async read(): Promise<ReadContainerResult> {
return (await super.read()) as ReadContainerResult;
} }
async getParentContainer(): Promise< protected toReadResult(): ReadContainerResult {
Container | CheckRootResultError | undefined if (this.isAbsent()) {
return new AbsentReadSuccess(this.uri, true);
} else {
return new ContainerReadSuccess(this.uri, true, this.isRootContainer()!);
}
}
async readIfUnfetched(): Promise<ReadContainerResult> {
return super.readIfUnfetched() as Promise<ReadContainerResult>;
}
// Parent Container Methods
private async checkIfIsRootContainer(): Promise<CheckRootResult> {
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(); const checkResult = await this.checkIfIsRootContainer();
if (checkResult) return checkResult; if (checkResult.isError) return checkResult;
if (this.rootContainer) return undefined; if (this.rootContainer) {
return new GetRootContainerSuccess(this);
}
const parentUri = getParentUri(this.uri); const parentUri = getParentUri(this.uri);
if (!parentUri) { if (!parentUri) {
return new UnexpectedError( return new NoncompliantPodError(
this.uri, 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<Container | CheckRootResultError> { async getParentContainer(): Promise<
Container | CheckRootResultError | undefined
> {
const checkResult = await this.checkIfIsRootContainer(); const checkResult = await this.checkIfIsRootContainer();
if (checkResult) return checkResult; if (checkResult.isError) return checkResult;
if (this.rootContainer) { if (this.rootContainer) return undefined;
return this;
}
const parentUri = getParentUri(this.uri); const parentUri = getParentUri(this.uri);
if (!parentUri) { if (!parentUri) {
return new UnexpectedError( return new NoncompliantPodError(
this.uri, 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)[] { children(): (Leaf | Container)[] {
@ -100,99 +147,78 @@ export class Container extends Resource {
return this.context.resourceStore.get(`${this.uri}${slug}`); return this.context.resourceStore.get(`${this.uri}${slug}`);
} }
// Child Creators
createChildAndOverwrite( createChildAndOverwrite(
slug: ContainerUri, slug: ContainerUri,
): Promise<Container | CreateResultErrors>; ): Promise<ContainerCreateAndOverwriteResult>;
createChildAndOverwrite(slug: LeafUri): Promise<Leaf | CreateResultErrors>; createChildAndOverwrite(slug: LeafUri): Promise<LeafCreateAndOverwriteResult>;
createChildAndOverwrite(slug: string): Promise<Resource | CreateResultErrors>;
createChildAndOverwrite( createChildAndOverwrite(
slug: string, slug: string,
): Promise<Resource | CreateResultErrors> { ): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult>;
createChildAndOverwrite(
slug: string,
): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult> {
return this.child(slug).createAndOverwrite(); return this.child(slug).createAndOverwrite();
} }
createChildIfAbsent( createChildIfAbsent(slug: ContainerUri): Promise<LeafCreateIfAbsentResult>;
slug: ContainerUri, createChildIfAbsent(slug: LeafUri): Promise<LeafCreateIfAbsentResult>;
): Promise<Container | CreateResultWithoutOverwriteErrors>;
createChildIfAbsent(
slug: LeafUri,
): Promise<Leaf | CreateResultWithoutOverwriteErrors>;
createChildIfAbsent( createChildIfAbsent(
slug: string, slug: string,
): Promise<Container | Leaf | CreateResultWithoutOverwriteErrors>; ): Promise<ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult>;
createChildIfAbsent( createChildIfAbsent(
slug: string, slug: string,
): Promise<Container | Leaf | CreateResultWithoutOverwriteErrors> { ): Promise<ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult> {
return this.child(slug).createIfAbsent(); return this.child(slug).createIfAbsent();
} }
async uploadChildAndOverwrite( async uploadChildAndOverwrite(
slug: string, slug: LeafUri,
blob: Blob, blob: Blob,
mimeType: string, mimeType: string,
): Promise<Leaf | UploadResultError> { ): Promise<LeafCreateAndOverwriteResult> {
const child = this.child(slug); return this.child(slug).uploadAndOverwrite(blob, mimeType);
if (child.type === "leaf") {
return child.uploadAndOverwrite(blob, mimeType);
}
return new UnexpectedError(
child.uri,
new Error(`${slug} is not a leaf uri.`),
);
} }
async uploadIfAbsent( async uploadChildIfAbsent(
slug: string, slug: LeafUri,
blob: Blob, blob: Blob,
mimeType: string, mimeType: string,
): Promise<Leaf | UploadResultWithoutOverwriteError> { ): Promise<LeafCreateIfAbsentResult> {
const child = this.child(slug); return this.child(slug).uploadIfAbsent(blob, mimeType);
if (child.type === "leaf") {
return child.uploadIfAbsent(blob, mimeType);
}
return new UnexpectedError(
child.uri,
new Error(`${slug} is not a leaf uri.`),
);
} }
async clear(): Promise< async clear(): Promise<
AggregateError<DeleteResultError | ReadResultError> | this | AggregateSuccess<DeleteSuccess>
| AggregateError<DeleteResultError | ReadResultError>
> { > {
const readResult = await this.read(); const readResult = await this.read();
if (readResult.type === "error") if (readResult.isError) return new AggregateError([readResult]);
return new AggregateError(this.uri, [readResult]); const results = (
const errors = (
await Promise.all( await Promise.all(
this.children().map(async (child) => { this.children().map(async (child) => {
const deleteError = await child.delete(); return child.delete();
if (deleteError.type === "error") return deleteError;
}), }),
) )
) ).flat();
.flat() const errors = results.filter(
.filter(
( (
value, value,
): value is ): value is
| DeleteResultError | DeleteResultError
| AggregateError<DeleteResultError | ReadResultError> => !!value, | AggregateError<DeleteResultError | ReadResultError> => value.isError,
); );
if (errors.length > 0) { if (errors.length > 0) {
return new AggregateError(this.uri, errors); return new AggregateError(errors);
} }
return this; return new AggregateSuccess<DeleteSuccess>(results as DeleteSuccess[]);
} }
async delete(): Promise< async delete(): Promise<
| this DeleteResult | AggregateError<DeleteResultError | ReadResultError>
| AggregateError<ReadResultError | DeleteResultError>
| DeleteResultError
> { > {
const clearResult = await this.clear(); const clearResult = await this.clear();
if (clearResult.type === "error") return clearResult; if (clearResult.isError) return clearResult;
return this.parseResult(await this.requester.delete()) as return this.handleDelete();
| this
| DeleteResultError;
} }
} }

@ -1,63 +1,58 @@
import type { DatasetChanges } from "@ldo/rdf-utils"; import type { DatasetChanges } from "@ldo/rdf-utils";
import type { Quad } from "@rdfjs/types"; import type { Quad } from "@rdfjs/types";
import { LeafRequester } from "../requester/LeafRequester"; 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 { CheckRootResultError } from "../requester/requests/checkRootContainer";
import type { DeleteResultError } from "../requester/requests/deleteResource";
import type { UpdateResultError } from "../requester/requests/updateDataResource";
import type { import type {
UploadResultError, LeafCreateAndOverwriteResult,
UploadResultWithoutOverwriteError, LeafCreateIfAbsentResult,
} from "../requester/requests/uploadResource"; } 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 type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import { getParentUri } from "../util/rdfUtils"; import { getParentUri } from "../util/rdfUtils";
import type { LeafUri } from "../util/uriTypes"; import type { LeafUri } from "../util/uriTypes";
import type { Container } from "./Container"; import type { Container } from "./Container";
import type { SharedStatuses } from "./Resource";
import { Resource } from "./Resource"; import { Resource } from "./Resource";
import type { GetRootContainerSuccess } from "./resourceResults/GetRootContainerSuccess";
export class Leaf extends Resource { export class Leaf extends Resource {
readonly uri: LeafUri; readonly uri: LeafUri;
protected requester: LeafRequester; protected requester: LeafRequester;
readonly type = "leaf" as const; 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) { constructor(uri: LeafUri, context: SolidLdoDatasetContext) {
super(context); super(context);
this.uri = uri; this.uri = uri;
this.requester = new LeafRequester(uri, context); this.requester = new LeafRequester(uri, context);
this.status = new Unfetched(this.uri);
} }
// Getters
isUploading(): boolean {
return this.requester.isUploading();
}
isUpdating(): boolean { isUpdating(): boolean {
return this.requester.isUpdating(); return this.requester.isUpdating();
} }
protected parseResult<PossibleErrors extends ErrorResult>(
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<Container | CheckRootResultError> {
const parentUri = getParentUri(this.uri)!;
const parent = this.context.resourceStore.get(parentUri);
return parent.getRootContainer();
}
getMimeType(): string | undefined { getMimeType(): string | undefined {
return this.binaryData?.mimeType; return this.binaryData?.mimeType;
} }
@ -74,28 +69,100 @@ export class Leaf extends Resource {
return !this.binaryData; 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<ReadLeafResult> {
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<ReadLeafResult> {
return super.readIfUnfetched() as Promise<ReadLeafResult>;
}
// Parent Container Methods
getParentContainer(): Container {
const parentUri = getParentUri(this.uri)!;
return this.context.resourceStore.get(parentUri);
}
getRootContainer(): Promise<GetRootContainerSuccess | CheckRootResultError> {
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( async uploadAndOverwrite(
blob: Blob, blob: Blob,
mimeType: string, mimeType: string,
): Promise<this | UploadResultError> { ): Promise<LeafCreateAndOverwriteResult> {
return this.parseResult(await this.requester.upload(blob, mimeType, true)); 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( async uploadIfAbsent(
blob: Blob, blob: Blob,
mimeType: string, mimeType: string,
): Promise<this | UploadResultWithoutOverwriteError> { ): Promise<LeafCreateIfAbsentResult> {
return this.parseResult(await this.requester.upload(blob, mimeType)); 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( async update(changes: DatasetChanges<Quad>): Promise<UpdateResult> {
changes: DatasetChanges<Quad>, const result = await this.requester.updateDataResource(changes);
): Promise<this | UpdateResultError> { this.status = result;
return this.parseResult(await this.requester.updateDataResource(changes)); if (result.isError) return result;
this.binaryData = undefined;
this.absent = false;
this.emitThisAndParent();
return result;
} }
// Delete Method async delete(): Promise<DeleteResult> {
async delete(): Promise<this | DeleteResultError> { return this.handleDelete();
return this.parseResult(await this.requester.delete());
} }
} }

@ -1,27 +1,36 @@
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; 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 { import type {
CreateResultErrors, ContainerCreateAndOverwriteResult,
CreateResultWithoutOverwriteErrors, ContainerCreateIfAbsentResult,
LeafCreateAndOverwriteResult,
LeafCreateIfAbsentResult,
} from "../requester/requests/createDataResource"; } from "../requester/requests/createDataResource";
import type { ReadResultError } from "../requester/requests/readResource"; import type {
import type { Container } from "./Container"; ReadContainerResult,
ReadLeafResult,
} from "../requester/requests/readResource";
import type { Requester } from "../requester/Requester"; import type { Requester } from "../requester/Requester";
import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; import type { CheckRootResultError } from "../requester/requests/checkRootContainer";
import type { import type { AccessRule } from "../requester/results/success/AccessRule";
AccessRule, import type { SetAccessRulesResult } from "../requester/requests/setAccessRules";
AccessRuleChangeResult,
AccessRuleFetchError,
AccessRuleResult,
} from "../requester/requestResults/AccessRule";
import { getAccessRules } from "../requester/requests/getAccessRules";
import { setAccessRules } from "../requester/requests/setAccessRules"; import { setAccessRules } from "../requester/requests/setAccessRules";
import type TypedEmitter from "typed-emitter"; import type TypedEmitter from "typed-emitter";
import EventEmitter from "events"; import EventEmitter from "events";
import { getParentUri } from "../util/rdfUtils"; 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<{ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
update: () => void; update: () => void;
@ -30,6 +39,7 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
protected readonly context: SolidLdoDatasetContext; protected readonly context: SolidLdoDatasetContext;
abstract readonly uri: string; abstract readonly uri: string;
abstract readonly type: string; abstract readonly type: string;
abstract status: RequesterResult;
protected abstract readonly requester: Requester; protected abstract readonly requester: Requester;
protected didInitialFetch: boolean = false; protected didInitialFetch: boolean = false;
protected absent: boolean | undefined; protected absent: boolean | undefined;
@ -46,9 +56,6 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
isCreating(): boolean { isCreating(): boolean {
return this.requester.isCreating(); return this.requester.isCreating();
} }
isUploading(): boolean {
return this.requester.isUploading();
}
isReading(): boolean { isReading(): boolean {
return this.requester.isReading(); return this.requester.isReading();
} }
@ -73,62 +80,105 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
return this.absent === undefined ? undefined : !this.absent; return this.absent === undefined ? undefined : !this.absent;
} }
protected parseResult<PossibleErrors extends ErrorResult>( // Helper Methods
result: AbsentResult | BinaryResult | DataResult | PossibleErrors, protected emitThisAndParent() {
): 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;
}
this.emit("update"); this.emit("update");
const parentUri = getParentUri(this.uri); const parentUri = getParentUri(this.uri);
if (parentUri) { if (parentUri) {
const parentContainer = this.context.resourceStore.get(parentUri); const parentContainer = this.context.resourceStore.get(parentUri);
parentContainer.emit("update"); parentContainer.emit("update");
} }
return this;
} }
// Read Methods // Read Methods
async read(): Promise<this | ReadResultError> { protected updateWithReadSuccess(result: ReadSuccess) {
return this.parseResult(await this.requester.read()); 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<this | ReadResultError> {
protected abstract toReadResult(): ReadContainerResult | ReadLeafResult;
async readIfUnfetched(): Promise<ReadContainerResult | ReadLeafResult> {
if (this.didInitialFetch) { if (this.didInitialFetch) {
return this; const readResult = this.toReadResult();
this.status = readResult;
return readResult;
} }
return this.read(); return this.read();
} }
// Delete Methods
protected updateWithDeleteSuccess(_result: DeleteSuccess) {
this.absent = true;
this.didInitialFetch = true;
}
protected async handleDelete(): Promise<DeleteResult> {
const result = await this.requester.delete();
this.status = result;
if (result.isError) return result;
this.updateWithDeleteSuccess(result);
this.emitThisAndParent();
return result;
}
// Create Methods // Create Methods
async createAndOverwrite(): Promise<this | CreateResultErrors> { protected updateWithCreateSuccess(result: ResourceSuccess) {
return this.parseResult(await this.requester.createDataResource(true)); this.absent = false;
this.didInitialFetch = true;
if (result instanceof ReadSuccess) {
this.updateWithReadSuccess(result);
}
}
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<this | CreateResultWithoutOverwriteErrors> { async createIfAbsent(): Promise<
return this.parseResult(await this.requester.createDataResource()); 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 // Parent Container Methods -- Remember to change for Container
abstract getRootContainer(): Promise<Container | CheckRootResultError>; abstract getRootContainer(): Promise<
GetRootContainerSuccess | CheckRootResultError
>;
async getAccessRules(): Promise<AccessRuleResult | AccessRuleFetchError> { // Access Rules Methods
return getAccessRules({ uri: this.uri, fetch: this.context.fetch }); // async getAccessRules(): Promise<AccessRuleResult | AccessRuleFetchError> {
} // return getAccessRules({ uri: this.uri, fetch: this.context.fetch });
// }
async setAccessRules( async setAccessRules(
newAccessRules: AccessRule, newAccessRules: AccessRule,
): Promise<AccessRuleChangeResult | AccessRuleFetchError> { ): Promise<SetAccessRulesResult> {
return setAccessRules( return setAccessRules(this.uri, newAccessRules, {
{ uri: this.uri, fetch: this.context.fetch }, fetch: this.context.fetch,
newAccessRules, });
);
} }
} }

@ -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 };

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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<Container>
| ReadResultError;
export type ReadResourceSuccessLeafTypes =
| DataResourceReadSuccess
| BinaryResourceReadSuccess
| AbsentResourceReadSuccess<Leaf>
| 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;
}
}

@ -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;
}
}

@ -0,0 +1,5 @@
import crossFetch from "cross-fetch";
export function guaranteeFetch(fetchInput?: typeof fetch): typeof fetch {
return fetchInput || crossFetch;
}

@ -1,10 +1,10 @@
import { parseRdf } from "@ldo/ldo"; import { parseRdf } from "@ldo/ldo";
import { namedNode, quad as createQuad } from "@rdfjs/data-model"; 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 { Dataset } from "@rdfjs/types";
import type { ContainerUri } from "./uriTypes"; import type { ContainerUri } from "./uriTypes";
import { isContainerUri } 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 ldpContains = namedNode("http://www.w3.org/ns/ldp#contains");
export const rdfType = namedNode( export const rdfType = namedNode(
@ -78,16 +78,17 @@ export async function addRawTurtleToDataset(
rawTurtle: string, rawTurtle: string,
dataset: Dataset, dataset: Dataset,
baseUri: string, baseUri: string,
): Promise<DataResult | TurtleFormattingError> { ): Promise<undefined | NoncompliantPodError> {
let loadedDataset: Dataset; let loadedDataset: Dataset;
try { try {
loadedDataset = await parseRdf(rawTurtle, { loadedDataset = await parseRdf(rawTurtle, {
baseIRI: baseUri, baseIRI: baseUri,
}); });
} catch (err) { } catch (err) {
return new TurtleFormattingError( const error = UnexpectedResourceError.fromThrown(baseUri, err);
return new NoncompliantPodError(
baseUri, 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), createQuad(quad.subject, quad.predicate, quad.object, graphNode),
), ),
); );
return new DataResult(baseUri);
} }

Loading…
Cancel
Save