parent
356ecf8c67
commit
3cb56a083f
@ -1,33 +1,57 @@ |
||||
import type { ConnectedPlugin } from "@ldo/connected"; |
||||
import type { ConnectedContext, ConnectedPlugin } from "@ldo/connected"; |
||||
import type { SolidContainerUri, SolidLeafUri, SolidUri } from "./types"; |
||||
import type { SolidLeaf } from "./resources/SolidLeaf"; |
||||
import type { SolidContainer } from "./resources/SolidContainer"; |
||||
import { SolidLeaf } from "./resources/SolidLeaf"; |
||||
import { SolidContainer } from "./resources/SolidContainer"; |
||||
import { isSolidContainerUri, isSolidUri } from "./util/isSolidUri"; |
||||
|
||||
export interface SolidConnectedPlugin extends ConnectedPlugin { |
||||
export interface SolidConnectedContext { |
||||
fetch?: typeof fetch; |
||||
} |
||||
export interface SolidConnectedPlugin |
||||
extends ConnectedPlugin< |
||||
"solid", |
||||
SolidUri, |
||||
SolidLeaf | SolidContainer, |
||||
SolidConnectedContext |
||||
> { |
||||
name: "solid"; |
||||
identifierType: SolidUri; |
||||
getResource: |
||||
| ((uri: SolidLeafUri) => SolidLeaf) |
||||
| ((uri: SolidContainerUri) => SolidContainer); |
||||
createResource(): Promise<SolidLeaf>; |
||||
isUriValid(uri: string): uri is SolidLeafUri | SolidContainerUri; |
||||
normalizeUri?: (uri: string) => SolidLeafUri | SolidContainerUri; |
||||
context: { |
||||
fetch?: typeof fetch; |
||||
}; |
||||
| ((uri: SolidLeafUri, context: ConnectedContext<this[]>) => SolidLeaf) |
||||
| (( |
||||
uri: SolidContainerUri, |
||||
context: ConnectedContext<this[]>, |
||||
) => SolidContainer); |
||||
createResource(context: ConnectedContext<this[]>): Promise<SolidLeaf>; |
||||
} |
||||
|
||||
export const solidConnectedPlugin: SolidConnectedPlugin = { |
||||
name: "solid", |
||||
identifierType: "https://example.com", |
||||
getResource(_uri: SolidUri): SolidContainer | SolidLeaf { |
||||
throw new Error("Not Implemented"); |
||||
|
||||
getResource: function ( |
||||
uri: SolidLeafUri | SolidContainerUri, |
||||
context: ConnectedContext<SolidConnectedPlugin[]>, |
||||
): SolidLeaf | SolidContainer { |
||||
if (isSolidContainerUri(uri)) { |
||||
return new SolidContainer(uri, context); |
||||
} else { |
||||
return new SolidLeaf(uri, context); |
||||
} |
||||
}, |
||||
|
||||
createResource: function (): Promise<SolidLeaf> { |
||||
throw new Error("Function not implemented."); |
||||
}, |
||||
isUriValid: function (uri: string): uri is SolidLeafUri | SolidContainerUri { |
||||
throw new Error("Function not implemented."); |
||||
|
||||
isUriValid: function ( |
||||
uri: SolidContainerUri | SolidLeafUri, |
||||
): uri is SolidLeafUri | SolidContainerUri { |
||||
return isSolidUri(uri); |
||||
}, |
||||
|
||||
initialContext: { |
||||
fetch: undefined, |
||||
}, |
||||
context: {}, |
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore "Types" only exists for the typing system
|
||||
types: {}, |
||||
}; |
||||
|
@ -0,0 +1,10 @@ |
||||
/** |
||||
* A message sent from the Pod as a notification |
||||
*/ |
||||
export interface SolidNotificationMessage { |
||||
"@context": string | string[]; |
||||
id: string; |
||||
type: "Update" | "Delete" | "Remove" | "Add"; |
||||
object: string; |
||||
published: string; |
||||
} |
@ -0,0 +1,146 @@ |
||||
import type { ConnectedContext } from "@ldo/connected"; |
||||
import type { NotificationCallbackError } from "./results/NotificationErrors"; |
||||
import { v4 } from "uuid"; |
||||
import type { SolidContainer } from "../resources/SolidContainer"; |
||||
import type { SolidLeaf } from "../resources/SolidLeaf"; |
||||
import type { SolidNotificationMessage } from "./SolidNotificationMessage"; |
||||
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||
|
||||
export interface SubscriptionCallbacks { |
||||
onNotification?: (message: SolidNotificationMessage) => void; |
||||
// TODO: make notification errors more specific
|
||||
onNotificationError?: (error: Error) => void; |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* Abstract class for notification subscription methods. |
||||
*/ |
||||
export abstract class SolidNotificationSubscription { |
||||
protected resource: SolidContainer | SolidLeaf; |
||||
protected parentSubscription: (message: SolidNotificationMessage) => void; |
||||
protected context: ConnectedContext<SolidConnectedPlugin[]>; |
||||
protected subscriptions: Record<string, SubscriptionCallbacks> = {}; |
||||
private isOpen: boolean = false; |
||||
|
||||
constructor( |
||||
resource: SolidContainer | SolidLeaf, |
||||
parentSubscription: (message: SolidNotificationMessage) => void, |
||||
context: ConnectedContext<SolidConnectedPlugin[]>, |
||||
) { |
||||
this.resource = resource; |
||||
this.parentSubscription = parentSubscription; |
||||
this.context = context; |
||||
} |
||||
|
||||
public isSubscribedToNotifications(): boolean { |
||||
return this.isOpen; |
||||
} |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* PUBLIC |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* @internal |
||||
* subscribeToNotifications |
||||
*/ |
||||
async subscribeToNotifications( |
||||
subscriptionCallbacks?: SubscriptionCallbacks, |
||||
): Promise<string> { |
||||
const subscriptionId = v4(); |
||||
this.subscriptions[subscriptionId] = subscriptionCallbacks ?? {}; |
||||
if (!this.isOpen) { |
||||
await this.open(); |
||||
this.setIsOpen(true); |
||||
} |
||||
return subscriptionId; |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* unsubscribeFromNotification |
||||
*/ |
||||
async unsubscribeFromNotification(subscriptionId: string): Promise<void> { |
||||
if ( |
||||
!!this.subscriptions[subscriptionId] && |
||||
Object.keys(this.subscriptions).length === 1 |
||||
) { |
||||
await this.close(); |
||||
this.setIsOpen(false); |
||||
} |
||||
delete this.subscriptions[subscriptionId]; |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* unsubscribeFromAllNotifications |
||||
*/ |
||||
async unsubscribeFromAllNotifications(): Promise<void> { |
||||
await Promise.all( |
||||
Object.keys(this.subscriptions).map((id) => |
||||
this.unsubscribeFromNotification(id), |
||||
), |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* HELPERS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* @internal |
||||
* Opens the subscription |
||||
*/ |
||||
protected abstract open(): Promise<void>; |
||||
|
||||
/** |
||||
* @internal |
||||
* Closes the subscription |
||||
*/ |
||||
protected abstract close(): Promise<void>; |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* CALLBACKS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* @internal |
||||
* onNotification |
||||
*/ |
||||
protected onNotification(message: SolidNotificationMessage): void { |
||||
this.parentSubscription(message); |
||||
Object.values(this.subscriptions).forEach(({ onNotification }) => { |
||||
onNotification?.(message); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* onNotificationError |
||||
*/ |
||||
protected onNotificationError(message: NotificationCallbackError): void { |
||||
Object.values(this.subscriptions).forEach(({ onNotificationError }) => { |
||||
onNotificationError?.(message); |
||||
}); |
||||
if (message.type === "disconnectedNotAttemptingReconnectError") { |
||||
this.setIsOpen(false); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* setIsOpen |
||||
*/ |
||||
protected setIsOpen(status: boolean) { |
||||
const shouldUpdate = status !== this.isOpen; |
||||
this.isOpen = status; |
||||
if (shouldUpdate) this.resource.emit("update"); |
||||
} |
||||
} |
@ -0,0 +1,134 @@ |
||||
import { UnexpectedResourceError } from "../../requester/results/error/ErrorResult"; |
||||
import { SubscriptionClient } from "@solid-notifications/subscription"; |
||||
import { WebSocket } from "ws"; |
||||
import { |
||||
DisconnectedAttemptingReconnectError, |
||||
DisconnectedNotAttemptingReconnectError, |
||||
UnsupportedNotificationError, |
||||
} from "./results/NotificationErrors"; |
||||
import type { NotificationMessage } from "./NotificationMessage"; |
||||
import type { Resource } from "../Resource"; |
||||
import type { SolidLdoDatasetContext } from "../../SolidLdoDatasetContext"; |
||||
import type { |
||||
ChannelType, |
||||
NotificationChannel, |
||||
} from "@solid-notifications/types"; |
||||
import { SolidNotificationSubscription } from "./SolidNotificationSubscription"; |
||||
|
||||
const CHANNEL_TYPE = |
||||
"http://www.w3.org/ns/solid/notifications#WebSocketChannel2023"; |
||||
|
||||
export class Websocket2023NotificationSubscription extends SolidNotificationSubscription { |
||||
private socket: WebSocket | undefined; |
||||
private createWebsocket: (address: string) => WebSocket; |
||||
|
||||
// Reconnection data
|
||||
// How often we should attempt a reconnection
|
||||
private reconnectInterval = 5000; |
||||
// How many attempts have already been tried for a reconnection
|
||||
private reconnectAttempts = 0; |
||||
// Whether or not the socket was manually closes
|
||||
private isManualClose = false; |
||||
// Maximum number of attempts to reconnect
|
||||
private maxReconnectAttempts = 6; |
||||
|
||||
constructor( |
||||
resource: Resource, |
||||
parentSubscription: (message: NotificationMessage) => void, |
||||
context: SolidLdoDatasetContext, |
||||
createWebsocket?: (address: string) => WebSocket, |
||||
) { |
||||
super(resource, parentSubscription, context); |
||||
this.createWebsocket = createWebsocket ?? createWebsocketDefault; |
||||
} |
||||
|
||||
async open(): Promise<void> { |
||||
try { |
||||
const notificationChannel = await this.discoverNotificationChannel(); |
||||
await this.subscribeToWebsocket(notificationChannel); |
||||
} catch (err) { |
||||
if ( |
||||
err instanceof Error && |
||||
err.message.startsWith("Discovery did not succeed") |
||||
) { |
||||
this.onNotificationError( |
||||
new UnsupportedNotificationError(this.resource.uri, err.message), |
||||
); |
||||
} else { |
||||
this.onNotificationError( |
||||
UnexpectedResourceError.fromThrown(this.resource.uri, err), |
||||
); |
||||
} |
||||
this.onClose(); |
||||
} |
||||
} |
||||
|
||||
public async discoverNotificationChannel(): Promise<NotificationChannel> { |
||||
const client = new SubscriptionClient(this.context.fetch); |
||||
return await client.subscribe( |
||||
this.resource.uri, |
||||
CHANNEL_TYPE as ChannelType, |
||||
); |
||||
} |
||||
|
||||
public async subscribeToWebsocket( |
||||
notificationChannel: NotificationChannel, |
||||
): Promise<void> { |
||||
this.socket = this.createWebsocket( |
||||
notificationChannel.receiveFrom as string, |
||||
); |
||||
|
||||
this.socket.onopen = () => { |
||||
this.reconnectAttempts = 0; // Reset attempts on successful connection
|
||||
this.isManualClose = false; // Reset manual close flag
|
||||
}; |
||||
|
||||
this.socket.onmessage = (message) => { |
||||
const messageData = message.data.toString(); |
||||
// TODO uncompliant Pod error on misformatted message
|
||||
this.onNotification(JSON.parse(messageData) as NotificationMessage); |
||||
}; |
||||
|
||||
this.socket.onclose = this.onClose.bind(this); |
||||
|
||||
this.socket.onerror = (err) => { |
||||
this.onNotificationError( |
||||
new UnexpectedResourceError(this.resource.uri, err.error), |
||||
); |
||||
}; |
||||
return; |
||||
} |
||||
|
||||
private onClose() { |
||||
if (!this.isManualClose) { |
||||
// Attempt to reconnect only if the disconnection was unintentional
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) { |
||||
this.reconnectAttempts++; |
||||
setTimeout(() => { |
||||
this.open(); |
||||
}, this.reconnectInterval); |
||||
this.onNotificationError( |
||||
new DisconnectedAttemptingReconnectError( |
||||
this.resource.uri, |
||||
`Attempting to reconnect to Websocket for ${this.resource.uri}.`, |
||||
), |
||||
); |
||||
} else { |
||||
this.onNotificationError( |
||||
new DisconnectedNotAttemptingReconnectError( |
||||
this.resource.uri, |
||||
`Lost connection to websocket for ${this.resource.uri}.`, |
||||
), |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected async close(): Promise<void> { |
||||
this.socket?.terminate(); |
||||
} |
||||
} |
||||
|
||||
function createWebsocketDefault(address: string) { |
||||
return new WebSocket(address); |
||||
} |
@ -0,0 +1,30 @@ |
||||
import type { UnexpectedResourceError } from "../../../requester/results/error/ErrorResult"; |
||||
import { ResourceError } from "../../../requester/results/error/ErrorResult"; |
||||
|
||||
export type NotificationCallbackError = |
||||
| DisconnectedAttemptingReconnectError |
||||
| DisconnectedNotAttemptingReconnectError |
||||
| UnsupportedNotificationError |
||||
| UnexpectedResourceError; |
||||
|
||||
/** |
||||
* Indicates that the requested method for receiving notifications is not |
||||
* supported by this Pod. |
||||
*/ |
||||
export class UnsupportedNotificationError extends ResourceError { |
||||
readonly type = "unsupportedNotificationError" as const; |
||||
} |
||||
|
||||
/** |
||||
* Indicates that the socket has disconnected and is attempting to reconnect. |
||||
*/ |
||||
export class DisconnectedAttemptingReconnectError extends ResourceError { |
||||
readonly type = "disconnectedAttemptingReconnectError" as const; |
||||
} |
||||
|
||||
/** |
||||
* Indicates that the socket has disconnected and is attempting to reconnect. |
||||
*/ |
||||
export class DisconnectedNotAttemptingReconnectError extends ResourceError { |
||||
readonly type = "disconnectedNotAttemptingReconnectError" as const; |
||||
} |
@ -0,0 +1,203 @@ |
||||
import { ANY_KEY, RequestBatcher } from "../util/RequestBatcher"; |
||||
import type { ConnectedContext } from "@ldo/connected"; |
||||
import type { |
||||
ContainerCreateAndOverwriteResult, |
||||
ContainerCreateIfAbsentResult, |
||||
LeafCreateAndOverwriteResult, |
||||
LeafCreateIfAbsentResult, |
||||
} from "./requests/createDataResource"; |
||||
import { createDataResource } from "./requests/createDataResource"; |
||||
import type { |
||||
ReadContainerResult, |
||||
ReadLeafResult, |
||||
} from "./requests/readResource"; |
||||
import { readResource } from "./requests/readResource"; |
||||
import type { DeleteResult } from "./requests/deleteResource"; |
||||
import { deleteResource } from "./requests/deleteResource"; |
||||
import { modifyQueueByMergingEventsWithTheSameKeys } from "./util/modifyQueueFuntions"; |
||||
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||
import type { SolidContainer } from "../resources/SolidContainer"; |
||||
import type { SolidLeaf } from "../resources/SolidLeaf"; |
||||
|
||||
const READ_KEY = "read"; |
||||
const CREATE_KEY = "createDataResource"; |
||||
const DELETE_KEY = "delete"; |
||||
|
||||
/** |
||||
* @internal |
||||
* |
||||
* A singleton for handling batched requests |
||||
*/ |
||||
export abstract class BatchedRequester< |
||||
ResourceType extends SolidContainer | SolidLeaf, |
||||
> { |
||||
/** |
||||
* @internal |
||||
* A request batcher to maintain state for ongoing requests |
||||
*/ |
||||
protected readonly requestBatcher = new RequestBatcher(); |
||||
|
||||
/** |
||||
* The uri of the resource |
||||
*/ |
||||
abstract readonly resource: ResourceType; |
||||
|
||||
/** |
||||
* @internal |
||||
* ConnectedContext for the parent Dataset |
||||
*/ |
||||
protected context: ConnectedContext<SolidConnectedPlugin[]>; |
||||
|
||||
/** |
||||
* @param context - SolidLdoDatasetContext for the parent SolidLdoDataset |
||||
*/ |
||||
constructor(context: ConnectedContext<SolidConnectedPlugin[]>) { |
||||
this.context = context; |
||||
} |
||||
|
||||
/** |
||||
* Checks if the resource is currently making any request |
||||
* @returns true if the resource is making any requests |
||||
*/ |
||||
isLoading(): boolean { |
||||
return this.requestBatcher.isLoading(ANY_KEY); |
||||
} |
||||
|
||||
/** |
||||
* Checks if the resource is currently executing a create request |
||||
* @returns true if the resource is currently executing a create request |
||||
*/ |
||||
isCreating(): boolean { |
||||
return this.requestBatcher.isLoading(CREATE_KEY); |
||||
} |
||||
|
||||
/** |
||||
* Checks if the resource is currently executing a read request |
||||
* @returns true if the resource is currently executing a read request |
||||
*/ |
||||
isReading(): boolean { |
||||
return this.requestBatcher.isLoading(READ_KEY); |
||||
} |
||||
|
||||
/** |
||||
* Checks if the resource is currently executing a delete request |
||||
* @returns true if the resource is currently executing a delete request |
||||
*/ |
||||
isDeletinng(): boolean { |
||||
return this.requestBatcher.isLoading(DELETE_KEY); |
||||
} |
||||
|
||||
/** |
||||
* Read this resource. |
||||
* @returns A ReadLeafResult or a ReadContainerResult depending on the uri of |
||||
* this resource |
||||
*/ |
||||
async read(): Promise<ReadLeafResult | ReadContainerResult> { |
||||
const transaction = this.context.dataset.startTransaction(); |
||||
const result = await this.requestBatcher.queueProcess({ |
||||
name: READ_KEY, |
||||
args: [ |
||||
this.resource, |
||||
{ dataset: transaction, fetch: this.context.solid.fetch }, |
||||
], |
||||
perform: readResource, |
||||
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys(READ_KEY), |
||||
after: (result) => { |
||||
if (!result.isError) { |
||||
transaction.commit(); |
||||
} |
||||
}, |
||||
}); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Delete this resource |
||||
* @returns A DeleteResult |
||||
*/ |
||||
async delete(): Promise<DeleteResult<ResourceType>> { |
||||
const transaction = this.context.dataset.startTransaction(); |
||||
const result = await this.requestBatcher.queueProcess({ |
||||
name: DELETE_KEY, |
||||
args: [ |
||||
this.resource, |
||||
{ dataset: transaction, fetch: this.context.solid.fetch }, |
||||
], |
||||
perform: deleteResource, |
||||
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys(DELETE_KEY), |
||||
after: (result) => { |
||||
if (!result.isError) { |
||||
transaction.commit(); |
||||
} |
||||
}, |
||||
}); |
||||
return result as DeleteResult<ResourceType>; |
||||
} |
||||
|
||||
/** |
||||
* Creates a Resource |
||||
* @param overwrite - If true, this will orverwrite the resource if it already |
||||
* exists |
||||
* @returns A ContainerCreateAndOverwriteResult or a |
||||
* LeafCreateAndOverwriteResult depending on this resource's URI |
||||
*/ |
||||
createDataResource( |
||||
overwrite: true, |
||||
): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult>; |
||||
createDataResource( |
||||
overwrite?: false, |
||||
): Promise<ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult>; |
||||
createDataResource( |
||||
overwrite?: boolean, |
||||
): Promise< |
||||
| ContainerCreateAndOverwriteResult |
||||
| LeafCreateAndOverwriteResult |
||||
| ContainerCreateIfAbsentResult |
||||
| LeafCreateIfAbsentResult |
||||
>; |
||||
async createDataResource( |
||||
overwrite?: boolean, |
||||
): Promise< |
||||
| ContainerCreateAndOverwriteResult |
||||
| LeafCreateAndOverwriteResult |
||||
| ContainerCreateIfAbsentResult |
||||
| LeafCreateIfAbsentResult |
||||
> { |
||||
const transaction = this.context.dataset.startTransaction(); |
||||
const result = await this.requestBatcher.queueProcess({ |
||||
name: CREATE_KEY, |
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Apparently this is a nasty type and I can't be bothered to fix it
|
||||
args: [ |
||||
this.resource, |
||||
overwrite ?? false, |
||||
{ dataset: transaction, fetch: this.context.solid.fetch }, |
||||
], |
||||
perform: createDataResource, |
||||
modifyQueue: (queue, currentlyLoading, args) => { |
||||
const lastElementInQueue = queue[queue.length - 1]; |
||||
if ( |
||||
lastElementInQueue && |
||||
lastElementInQueue.name === CREATE_KEY && |
||||
!!lastElementInQueue.args[1] === !!args[1] |
||||
) { |
||||
return lastElementInQueue; |
||||
} |
||||
if ( |
||||
currentlyLoading && |
||||
currentlyLoading.name === CREATE_KEY && |
||||
!!currentlyLoading.args[1] === !!args[1] |
||||
) { |
||||
return currentlyLoading; |
||||
} |
||||
return undefined; |
||||
}, |
||||
after: (result) => { |
||||
if (!result.isError) { |
||||
transaction.commit(); |
||||
} |
||||
}, |
||||
}); |
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,83 @@ |
||||
import type { ConnectedContext } from "@ldo/connected"; |
||||
import type { SolidContainer } from "../resources/SolidContainer"; |
||||
import { BatchedRequester } from "./BatchedRequester"; |
||||
import type { CheckRootResult } from "./requests/checkRootContainer"; |
||||
import { checkRootContainer } from "./requests/checkRootContainer"; |
||||
import type { |
||||
ContainerCreateAndOverwriteResult, |
||||
ContainerCreateIfAbsentResult, |
||||
} from "./requests/createDataResource"; |
||||
import type { ReadContainerResult } from "./requests/readResource"; |
||||
import { modifyQueueByMergingEventsWithTheSameKeys } from "./util/modifyQueueFuntions"; |
||||
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||
|
||||
export const IS_ROOT_CONTAINER_KEY = "isRootContainer"; |
||||
|
||||
/** |
||||
* @internal |
||||
* |
||||
* A singleton to handle batched requests for containers |
||||
*/ |
||||
export class ContainerBatchedRequester extends BatchedRequester<SolidContainer> { |
||||
/** |
||||
* The URI of the container |
||||
*/ |
||||
readonly resource: SolidContainer; |
||||
|
||||
/** |
||||
* @param uri - The URI of the container |
||||
* @param context - ConnectedContext of the parent dataset |
||||
*/ |
||||
constructor( |
||||
resource: SolidContainer, |
||||
context: ConnectedContext<SolidConnectedPlugin[]>, |
||||
) { |
||||
super(context); |
||||
this.resource = resource; |
||||
} |
||||
|
||||
/** |
||||
* Reads the container |
||||
* @returns A ReadContainerResult |
||||
*/ |
||||
read(): Promise<ReadContainerResult> { |
||||
return super.read() as Promise<ReadContainerResult>; |
||||
} |
||||
|
||||
/** |
||||
* Creates the container |
||||
* @param overwrite - If true, this will orverwrite the resource if it already |
||||
* exists |
||||
*/ |
||||
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 |
||||
>; |
||||
} |
||||
|
||||
/** |
||||
* Checks to see if this container is a root container |
||||
* @returns A CheckRootResult |
||||
*/ |
||||
async isRootContainer(): Promise<CheckRootResult> { |
||||
return this.requestBatcher.queueProcess({ |
||||
name: IS_ROOT_CONTAINER_KEY, |
||||
args: [this.resource, { fetch: this.context.solid.fetch }], |
||||
perform: checkRootContainer, |
||||
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys( |
||||
IS_ROOT_CONTAINER_KEY, |
||||
), |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,179 @@ |
||||
import type { DatasetChanges } from "@ldo/rdf-utils"; |
||||
import { mergeDatasetChanges } from "@ldo/subscribable-dataset"; |
||||
import type { Quad } from "@rdfjs/types"; |
||||
import { BatchedRequester } from "./BatchedRequester"; |
||||
import type { |
||||
LeafCreateAndOverwriteResult, |
||||
LeafCreateIfAbsentResult, |
||||
} from "./requests/createDataResource"; |
||||
import type { ReadLeafResult } from "./requests/readResource"; |
||||
import type { UpdateResult } from "./requests/updateDataResource"; |
||||
import { updateDataResource } from "./requests/updateDataResource"; |
||||
import { uploadResource } from "./requests/uploadResource"; |
||||
import type { SolidLeaf } from "../resources/SolidLeaf"; |
||||
import type { ConnectedContext } from "@ldo/connected"; |
||||
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||
|
||||
export const UPDATE_KEY = "update"; |
||||
export const UPLOAD_KEY = "upload"; |
||||
|
||||
/** |
||||
* @internal |
||||
* |
||||
* A singleton to handle batched requests for leafs |
||||
*/ |
||||
export class LeafBatchedRequester extends BatchedRequester<SolidLeaf> { |
||||
/** |
||||
* The URI of the leaf |
||||
*/ |
||||
readonly resource: SolidLeaf; |
||||
|
||||
/** |
||||
* @param uri - the URI of the leaf |
||||
* @param context - SolidLdoDatasetContext of the parent dataset |
||||
*/ |
||||
constructor( |
||||
resource: SolidLeaf, |
||||
context: ConnectedContext<SolidConnectedPlugin[]>, |
||||
) { |
||||
super(context); |
||||
this.resource = resource; |
||||
} |
||||
|
||||
/** |
||||
* Checks if the resource is currently executing an update request |
||||
* @returns true if the resource is currently executing an update request |
||||
*/ |
||||
isUpdating(): boolean { |
||||
return this.requestBatcher.isLoading(UPDATE_KEY); |
||||
} |
||||
|
||||
/** |
||||
* Checks if the resource is currently executing an upload request |
||||
* @returns true if the resource is currently executing an upload request |
||||
*/ |
||||
isUploading(): boolean { |
||||
return this.requestBatcher.isLoading(UPLOAD_KEY); |
||||
} |
||||
|
||||
/** |
||||
* Reads the leaf |
||||
* @returns A ReadLeafResult |
||||
*/ |
||||
async read(): Promise<ReadLeafResult> { |
||||
return super.read() as Promise<ReadLeafResult>; |
||||
} |
||||
|
||||
/** |
||||
* Creates the leaf as a data resource |
||||
* @param overwrite - If true, this will orverwrite the resource if it already |
||||
* exists |
||||
*/ |
||||
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 |
||||
* @param changes - DatasetChanges that should be applied to the Pod |
||||
*/ |
||||
async updateDataResource( |
||||
changes: DatasetChanges<Quad>, |
||||
): Promise<UpdateResult<SolidLeaf>> { |
||||
const result = await this.requestBatcher.queueProcess({ |
||||
name: UPDATE_KEY, |
||||
args: [ |
||||
this.resource, |
||||
changes, |
||||
{ |
||||
fetch: this.context.solid.fetch, |
||||
dataset: this.context.dataset, |
||||
}, |
||||
], |
||||
perform: updateDataResource, |
||||
modifyQueue: (queue, currentlyProcessing, [, changes]) => { |
||||
if (queue[queue.length - 1]?.name === UPDATE_KEY) { |
||||
// Merge Changes
|
||||
const originalChanges = queue[queue.length - 1].args[1]; |
||||
mergeDatasetChanges(originalChanges, changes); |
||||
return queue[queue.length - 1]; |
||||
} |
||||
return undefined; |
||||
}, |
||||
}); |
||||
return result as UpdateResult<SolidLeaf>; |
||||
} |
||||
|
||||
/** |
||||
* Upload a binary at this resource's URI |
||||
* @param blob - A binary blob |
||||
* @param mimeType - the mime type of the blob |
||||
* @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.dataset.startTransaction(); |
||||
const result = await this.requestBatcher.queueProcess({ |
||||
name: UPLOAD_KEY, |
||||
args: [ |
||||
this.resource, |
||||
blob, |
||||
mimeType, |
||||
overwrite, |
||||
{ dataset: transaction, fetch: this.context.solid.fetch }, |
||||
], |
||||
perform: uploadResource, |
||||
modifyQueue: (queue, currentlyLoading, args) => { |
||||
const lastElementInQueue = queue[queue.length - 1]; |
||||
if ( |
||||
lastElementInQueue && |
||||
lastElementInQueue.name === UPLOAD_KEY && |
||||
!!lastElementInQueue.args[3] === !!args[3] |
||||
) { |
||||
return lastElementInQueue; |
||||
} |
||||
if ( |
||||
currentlyLoading && |
||||
currentlyLoading.name === UPLOAD_KEY && |
||||
!!currentlyLoading.args[3] === !!args[3] |
||||
) { |
||||
return currentlyLoading; |
||||
} |
||||
return undefined; |
||||
}, |
||||
after: (result) => { |
||||
if (!result.isError) { |
||||
transaction.commit(); |
||||
} |
||||
}, |
||||
}); |
||||
return result; |
||||
} |
||||
} |
@ -0,0 +1,92 @@ |
||||
import type { BasicRequestOptions } from "./requestOptions"; |
||||
import { parse as parseLinkHeader } from "http-link-header"; |
||||
import { CheckRootContainerSuccess } from "../results/success/CheckRootContainerSuccess"; |
||||
import type { |
||||
HttpErrorResultType, |
||||
UnexpectedHttpError, |
||||
} from "../results/error/HttpErrorResult"; |
||||
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
||||
import { UnexpectedResourceError } from "@ldo/connected"; |
||||
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||
|
||||
/** |
||||
* checkRootContainer result |
||||
*/ |
||||
export type CheckRootResult = CheckRootContainerSuccess | CheckRootResultError; |
||||
|
||||
/** |
||||
* All possible errors checkRootResult can return |
||||
*/ |
||||
export type CheckRootResultError = |
||||
| HttpErrorResultType<SolidContainer> |
||||
| UnexpectedHttpError<SolidContainer> |
||||
| UnexpectedResourceError<SolidContainer>; |
||||
|
||||
/** |
||||
* @internal |
||||
* Checks provided headers to see if a given URI is a root container as defined |
||||
* in the [solid specification section 4.1](https://solidproject.org/TR/protocol#storage-resource)
|
||||
* |
||||
* @param uri - the URI of the container resource |
||||
* @param headers - headers returned when making a GET request to the resource |
||||
* @returns CheckRootContainerSuccess if there is not error |
||||
*/ |
||||
export function checkHeadersForRootContainer( |
||||
resource: SolidContainer, |
||||
headers: Headers, |
||||
): CheckRootContainerSuccess { |
||||
const linkHeader = headers.get("link"); |
||||
if (!linkHeader) { |
||||
return new CheckRootContainerSuccess(resource, false); |
||||
} |
||||
const parsedLinkHeader = parseLinkHeader(linkHeader); |
||||
const types = parsedLinkHeader.get("rel", "type"); |
||||
const isRootContainer = types.some( |
||||
(type) => type.uri === "http://www.w3.org/ns/pim/space#Storage", |
||||
); |
||||
return new CheckRootContainerSuccess(resource, isRootContainer); |
||||
} |
||||
|
||||
/** |
||||
* Performs a request to the Pod to check if the given URI is a root container |
||||
* as defined in the [solid specification section 4.1](https://solidproject.org/TR/protocol#storage-resource)
|
||||
* |
||||
* @param uri - the URI of the container resource |
||||
* @param options - options variable to pass a fetch function |
||||
* @returns CheckResourceSuccess if there is no error |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* import { checkRootContainer } from "@ldo/solid"; |
||||
* import { fetch } from "@inrupt/solid-client-authn-browser"; |
||||
* |
||||
* const result = await checkRootContainer("https://example.com/", { fetch }); |
||||
* if (!result.isError) { |
||||
* // true if the container is a root container
|
||||
* console.log(result.isRootContainer); |
||||
* } |
||||
* ``` |
||||
*/ |
||||
export async function checkRootContainer( |
||||
resource: SolidContainer, |
||||
options?: BasicRequestOptions, |
||||
): Promise<CheckRootResult> { |
||||
try { |
||||
const fetch = guaranteeFetch(options?.fetch); |
||||
// Fetch options to determine the document type
|
||||
// Note cache: "no-store": we don't want to depend on cached results because
|
||||
// web browsers do not cache link headers
|
||||
// https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1959
|
||||
const response = await fetch(resource.uri, { |
||||
method: "HEAD", |
||||
cache: "no-store", |
||||
}); |
||||
const httpErrorResult = HttpErrorResult.checkResponse(resource, response); |
||||
if (httpErrorResult) return httpErrorResult; |
||||
|
||||
return checkHeadersForRootContainer(resource, response.headers); |
||||
} catch (err) { |
||||
return UnexpectedResourceError.fromThrown(resource, err); |
||||
} |
||||
} |
@ -0,0 +1,243 @@ |
||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||
import { |
||||
addResourceRdfToContainer, |
||||
getParentUri, |
||||
getSlug, |
||||
} from "../../util/rdfUtils"; |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { UnexpectedResourceError } from "@ldo/connected"; |
||||
import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; |
||||
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
||||
import { CreateSuccess } from "../results/success/CreateSuccess"; |
||||
import type { AbsentReadSuccess } from "../results/success/ReadSuccess"; |
||||
import type { DeleteResultError } from "./deleteResource"; |
||||
import { deleteResource } from "./deleteResource"; |
||||
import type { |
||||
ReadContainerResult, |
||||
ReadLeafResult, |
||||
ReadResultError, |
||||
} from "./readResource"; |
||||
import { readResource } from "./readResource"; |
||||
import type { DatasetRequestOptions } from "./requestOptions"; |
||||
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||
|
||||
/** |
||||
* All possible return values when creating and overwriting a container |
||||
*/ |
||||
export type ContainerCreateAndOverwriteResult = |
||||
| CreateSuccess<SolidContainer> |
||||
| CreateAndOverwriteResultErrors<SolidContainer>; |
||||
|
||||
/** |
||||
* All possible return values when creating and overwriting a leaf |
||||
*/ |
||||
export type LeafCreateAndOverwriteResult = |
||||
| CreateSuccess<SolidLeaf> |
||||
| CreateAndOverwriteResultErrors<SolidLeaf>; |
||||
|
||||
/** |
||||
* All possible return values when creating a container if absent |
||||
*/ |
||||
export type ContainerCreateIfAbsentResult = |
||||
| CreateSuccess<SolidContainer> |
||||
| Exclude<ReadContainerResult, AbsentReadSuccess<Resource>> |
||||
| CreateIfAbsentResultErrors<SolidLeaf>; |
||||
|
||||
/** |
||||
* All possible return values when creating a leaf if absent |
||||
*/ |
||||
export type LeafCreateIfAbsentResult = |
||||
| CreateSuccess<SolidLeaf> |
||||
| Exclude<ReadLeafResult, AbsentReadSuccess<Resource>> |
||||
| CreateIfAbsentResultErrors<SolidLeaf>; |
||||
|
||||
/** |
||||
* All possible errors returned by creating and overwriting a resource |
||||
*/ |
||||
export type CreateAndOverwriteResultErrors<ResourceType extends Resource> = |
||||
| DeleteResultError<ResourceType> |
||||
| CreateErrors<ResourceType>; |
||||
|
||||
/** |
||||
* All possible errors returned by creating a resource if absent |
||||
*/ |
||||
export type CreateIfAbsentResultErrors<ResourceType extends Resource> = |
||||
| ReadResultError<ResourceType> |
||||
| CreateErrors<ResourceType>; |
||||
|
||||
/** |
||||
* All possible errors returned by creating a resource |
||||
*/ |
||||
export type CreateErrors<ResourceType extends Resource> = |
||||
| HttpErrorResultType<ResourceType> |
||||
| UnexpectedResourceError<ResourceType>; |
||||
|
||||
/** |
||||
* Creates a data resource (RDF resource) at the provided URI. This resource |
||||
* could also be a container. |
||||
* |
||||
* @param uri - The URI of the resource |
||||
* @param overwrite - If true, the request will overwrite any previous resource |
||||
* at this URI. |
||||
* @param options - Options to provide a fetch function and a local dataset to |
||||
* update. |
||||
* @returns One of many create results depending on the input |
||||
* |
||||
* @example |
||||
* `createDataResource` can be used to create containers. |
||||
* |
||||
* ```typescript
|
||||
* import { createDataResource } from "@ldo/solid"; |
||||
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||
* |
||||
* const result = await createDataResource( |
||||
* "https://example.com/container/", |
||||
* true, |
||||
* { fetch }, |
||||
* ); |
||||
* if (!result.isError) { |
||||
* // Do something
|
||||
* } |
||||
* ``` |
||||
* |
||||
* @example |
||||
* `createDataResource` can also create a blank data resource at the provided |
||||
* URI. |
||||
* |
||||
* ```typescript
|
||||
* import { createDataResource } from "@ldo/solid"; |
||||
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||
* |
||||
* const result = await createDataResource( |
||||
* "https://example.com/container/someResource.ttl", |
||||
* true, |
||||
* { fetch }, |
||||
* ); |
||||
* if (!result.isError) { |
||||
* // Do something
|
||||
* } |
||||
* ``` |
||||
* |
||||
* @example |
||||
* Any local RDFJS dataset passed to the `options` field will be updated with |
||||
* any new RDF data from the create process. |
||||
* |
||||
* ```typescript
|
||||
* import { createDataResource } from "@ldo/solid"; |
||||
* import { createDataset } from "@ldo/dataset" |
||||
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||
* |
||||
* const localDataset = createDataset(); |
||||
* const result = await createDataResource( |
||||
* "https://example.com/container/someResource.ttl", |
||||
* true, |
||||
* { fetch, dataset: localDataset }, |
||||
* ); |
||||
* if (!result.isError) { |
||||
* // Do something
|
||||
* } |
||||
* ``` |
||||
*/ |
||||
export function createDataResource( |
||||
resource: SolidLeaf, |
||||
overwrite: true, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<ContainerCreateAndOverwriteResult>; |
||||
export function createDataResource( |
||||
resouce: SolidLeaf, |
||||
overwrite: true, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<LeafCreateAndOverwriteResult>; |
||||
export function createDataResource( |
||||
resouce: SolidContainer, |
||||
overwrite?: false, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<ContainerCreateIfAbsentResult>; |
||||
export function createDataResource( |
||||
resouce: SolidLeaf, |
||||
overwrite?: false, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<LeafCreateIfAbsentResult>; |
||||
export function createDataResource( |
||||
resouce: SolidContainer, |
||||
overwrite?: boolean, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult>; |
||||
export function createDataResource( |
||||
resouce: SolidLeaf, |
||||
overwrite?: boolean, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult>; |
||||
export function createDataResource( |
||||
resource: SolidContainer | SolidLeaf, |
||||
overwrite: true, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult>; |
||||
export function createDataResource( |
||||
resource: SolidContainer | SolidLeaf, |
||||
overwrite?: false, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<LeafCreateIfAbsentResult | LeafCreateIfAbsentResult>; |
||||
export function createDataResource( |
||||
resource: SolidContainer | SolidLeaf, |
||||
overwrite?: boolean, |
||||
options?: DatasetRequestOptions, |
||||
): Promise< |
||||
| ContainerCreateAndOverwriteResult |
||||
| LeafCreateAndOverwriteResult |
||||
| ContainerCreateIfAbsentResult |
||||
| LeafCreateIfAbsentResult |
||||
>; |
||||
export async function createDataResource( |
||||
resource: SolidContainer | SolidLeaf, |
||||
overwrite?: boolean, |
||||
options?: DatasetRequestOptions, |
||||
): Promise< |
||||
| ContainerCreateAndOverwriteResult |
||||
| LeafCreateAndOverwriteResult |
||||
| ContainerCreateIfAbsentResult |
||||
| LeafCreateIfAbsentResult |
||||
> { |
||||
try { |
||||
const fetch = guaranteeFetch(options?.fetch); |
||||
let didOverwrite = false; |
||||
if (overwrite) { |
||||
const deleteResult = await deleteResource(resource, options); |
||||
// Return if it wasn't deleted
|
||||
if (deleteResult.isError) return deleteResult; |
||||
didOverwrite = deleteResult.resourceExisted; |
||||
} else { |
||||
// Perform a read to check if it exists
|
||||
const readResult = await readResource(resource, options); |
||||
|
||||
// If it does exist stop and return.
|
||||
if (readResult.type !== "absentReadSuccess") { |
||||
return readResult; |
||||
} |
||||
} |
||||
// Create the document
|
||||
const parentUri = getParentUri(resource.uri)!; |
||||
const headers: HeadersInit = { |
||||
"content-type": "text/turtle", |
||||
slug: getSlug(resource.uri), |
||||
}; |
||||
if (resource.type === "container") { |
||||
headers.link = '<http://www.w3.org/ns/ldp#Container>; rel="type"'; |
||||
} |
||||
const response = await fetch(parentUri, { |
||||
method: "post", |
||||
headers, |
||||
}); |
||||
|
||||
const httpError = HttpErrorResult.checkResponse(resource, response); |
||||
if (httpError) return httpError; |
||||
|
||||
if (options?.dataset) { |
||||
addResourceRdfToContainer(resource.uri, options.dataset); |
||||
} |
||||
return new CreateSuccess(resource, didOverwrite); |
||||
} catch (err) { |
||||
return UnexpectedResourceError.fromThrown(resource, err); |
||||
} |
||||
} |
@ -0,0 +1,109 @@ |
||||
import { namedNode } from "@rdfjs/data-model"; |
||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||
import { deleteResourceRdfFromContainer } from "../../util/rdfUtils"; |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { UnexpectedResourceError } from "@ldo/connected"; |
||||
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"; |
||||
import type { IBulkEditableDataset } from "@ldo/subscribable-dataset"; |
||||
import type { Quad } from "@rdfjs/types"; |
||||
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||
|
||||
/** |
||||
* All possible return values for deleteResource |
||||
*/ |
||||
export type DeleteResult<ResourceType extends Resource> = |
||||
| DeleteSuccess<ResourceType> |
||||
| DeleteResultError<ResourceType>; |
||||
|
||||
/** |
||||
* All possible errors that can be returned by deleteResource |
||||
*/ |
||||
export type DeleteResultError<ResourceType extends Resource> = |
||||
| HttpErrorResultType<ResourceType> |
||||
| UnexpectedResourceError<ResourceType>; |
||||
|
||||
/** |
||||
* Deletes a resource on a Pod at a given URL. |
||||
* |
||||
* @param uri - The URI for the resource that should be deleted |
||||
* @param options - Options to provide a fetch function and a local dataset to |
||||
* update. |
||||
* @returns a DeleteResult |
||||
* |
||||
* @example |
||||
* `deleteResource` will send a request to a Solid Pod using the provided fetch |
||||
* function. A local dataset can also be provided. It will be updated with any |
||||
* new information from the delete. |
||||
* |
||||
* ```typescript
|
||||
* import { deleteResource } from "@ldo/solid"; |
||||
* import { createDataset } from "@ldo/dataset" |
||||
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||
* |
||||
* const localDataset = createDataset(); |
||||
* const result = await deleteResource( |
||||
* "https://example.com/container/someResource.ttl", |
||||
* { fetch, dataset: localDataset }, |
||||
* ); |
||||
* if (!result.isError) { |
||||
* // Do something
|
||||
* } |
||||
* ``` |
||||
*/ |
||||
export async function deleteResource( |
||||
resource: SolidContainer, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<DeleteResult<SolidContainer>>; |
||||
export async function deleteResource( |
||||
resource: SolidLeaf, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<DeleteResult<SolidLeaf>>; |
||||
export async function deleteResource( |
||||
resource: SolidContainer | SolidLeaf, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<DeleteResult<SolidContainer | SolidLeaf>>; |
||||
export async function deleteResource( |
||||
resource: SolidContainer | SolidLeaf, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<DeleteResult<SolidContainer | SolidLeaf>> { |
||||
try { |
||||
const fetch = guaranteeFetch(options?.fetch); |
||||
const response = await fetch(resource.uri, { |
||||
method: "delete", |
||||
}); |
||||
const errorResult = HttpErrorResult.checkResponse(resource, response); |
||||
if (errorResult) return errorResult; |
||||
|
||||
// Specifically check for a 205. Annoyingly, the server will return 200 even
|
||||
// if it hasn't been deleted when you're unauthenticated. 404 happens when
|
||||
// the document never existed
|
||||
if (response.status === 205 || response.status === 404) { |
||||
if (options?.dataset) |
||||
updateDatasetOnSuccessfulDelete(resource.uri, options.dataset); |
||||
return new DeleteSuccess(resource, response.status === 205); |
||||
} |
||||
return new UnexpectedHttpError(resource, response); |
||||
} catch (err) { |
||||
return UnexpectedResourceError.fromThrown(resource, err); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Assuming a successful delete has just been performed, this function updates |
||||
* datastores to reflect that. |
||||
* |
||||
* @param uri - The uri of the resouce that was removed |
||||
* @param dataset - The dataset that should be updated |
||||
*/ |
||||
export function updateDatasetOnSuccessfulDelete( |
||||
uri: string, |
||||
dataset: IBulkEditableDataset<Quad>, |
||||
): void { |
||||
dataset.deleteMatches(undefined, undefined, undefined, namedNode(uri)); |
||||
deleteResourceRdfFromContainer(uri, dataset); |
||||
} |
@ -0,0 +1,169 @@ |
||||
import type { UnexpectedHttpError } from "../results/error/HttpErrorResult"; |
||||
import { |
||||
HttpErrorResult, |
||||
type HttpErrorResultType, |
||||
} from "../results/error/HttpErrorResult"; |
||||
import { |
||||
addRawTurtleToDataset, |
||||
addResourceRdfToContainer, |
||||
} from "../../util/rdfUtils"; |
||||
import type { DatasetRequestOptions } from "./requestOptions"; |
||||
import { |
||||
BinaryReadSuccess, |
||||
DataReadSuccess, |
||||
} from "../results/success/ReadSuccess"; |
||||
import { ContainerReadSuccess } from "../results/success/ReadSuccess"; |
||||
import { AbsentReadSuccess } from "../results/success/ReadSuccess"; |
||||
import { NoncompliantPodError } from "../results/error/NoncompliantPodError"; |
||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { UnexpectedResourceError } from "@ldo/connected"; |
||||
import { checkHeadersForRootContainer } from "./checkRootContainer"; |
||||
import { namedNode } from "@rdfjs/data-model"; |
||||
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||
|
||||
/** |
||||
* All possible return values for reading a leaf |
||||
*/ |
||||
export type ReadLeafResult = |
||||
| BinaryReadSuccess |
||||
| DataReadSuccess |
||||
| AbsentReadSuccess<SolidLeaf> |
||||
| ReadResultError<SolidContainer>; |
||||
|
||||
/** |
||||
* All possible return values for reading a container |
||||
*/ |
||||
export type ReadContainerResult = |
||||
| ContainerReadSuccess |
||||
| AbsentReadSuccess<SolidContainer> |
||||
| ReadResultError<SolidContainer>; |
||||
|
||||
/** |
||||
* All possible errors the readResource function can return |
||||
*/ |
||||
export type ReadResultError<ResourceType extends Resource> = |
||||
| HttpErrorResultType<ResourceType> |
||||
| NoncompliantPodError<ResourceType> |
||||
| UnexpectedHttpError<ResourceType> |
||||
| UnexpectedResourceError<ResourceType>; |
||||
|
||||
/** |
||||
* Reads resource at a provided URI and returns the result |
||||
* |
||||
* @param uri - The URI of the resource |
||||
* @param options - Options to provide a fetch function and a local dataset to |
||||
* update. |
||||
* @returns ReadResult |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* import { deleteResource } from "@ldo/solid"; |
||||
* import { createDataset } from "@ldo/dataset" |
||||
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||
* |
||||
* const dataset = createDataset(); |
||||
* const result = await readResource( |
||||
* "https://example.com/container/someResource.ttl", |
||||
* { fetch, dataset }, |
||||
* ); |
||||
* if (!result.isError) { |
||||
* if (result.type === "absentReadSuccess") { |
||||
* // There was no problem reading the resource, but it doesn't exist
|
||||
* } else if (result.type === "dataReadSuccess") { |
||||
* // The resource was read and it is an RDF resource. The dataset provided
|
||||
* // dataset will also be loaded with the data from the resource
|
||||
* } else if (result.type === "binaryReadSuccess") { |
||||
* // The resource is a binary
|
||||
* console.log(result.blob); |
||||
* console.log(result.mimeType); |
||||
* } |
||||
* } |
||||
* ``` |
||||
*/ |
||||
export async function readResource( |
||||
resource: SolidLeaf, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<ReadLeafResult>; |
||||
export async function readResource( |
||||
resource: SolidContainer, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<ReadContainerResult>; |
||||
export async function readResource( |
||||
resource: SolidLeaf | SolidContainer, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<ReadLeafResult | ReadContainerResult>; |
||||
export async function readResource( |
||||
resource: SolidLeaf | SolidContainer, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<ReadLeafResult | ReadContainerResult> { |
||||
try { |
||||
const fetch = guaranteeFetch(options?.fetch); |
||||
// Fetch options to determine the document type
|
||||
const response = await fetch(resource.uri, { |
||||
headers: { accept: "text/turtle, */*" }, |
||||
}); |
||||
if (response.status === 404) { |
||||
// Clear existing data if present
|
||||
if (options?.dataset) { |
||||
options.dataset.deleteMatches( |
||||
undefined, |
||||
undefined, |
||||
undefined, |
||||
namedNode(resource.uri), |
||||
); |
||||
} |
||||
|
||||
return new AbsentReadSuccess(resource, false); |
||||
} |
||||
const httpErrorResult = HttpErrorResult.checkResponse(resource, response); |
||||
if (httpErrorResult) return httpErrorResult; |
||||
|
||||
// Add this resource to the container
|
||||
if (options?.dataset) { |
||||
addResourceRdfToContainer(resource.uri, options.dataset); |
||||
} |
||||
|
||||
const contentType = response.headers.get("content-type"); |
||||
if (!contentType) { |
||||
return new NoncompliantPodError( |
||||
resource, |
||||
"Resource requests must return a content-type header.", |
||||
); |
||||
} |
||||
|
||||
if (contentType.startsWith("text/turtle")) { |
||||
// Parse Turtle
|
||||
const rawTurtle = await response.text(); |
||||
if (options?.dataset) { |
||||
const result = await addRawTurtleToDataset( |
||||
rawTurtle, |
||||
options.dataset, |
||||
resource.uri, |
||||
); |
||||
if (result) return result; |
||||
} |
||||
if (resource.type === "container") { |
||||
const result = checkHeadersForRootContainer(resource, response.headers); |
||||
return new ContainerReadSuccess( |
||||
resource, |
||||
false, |
||||
result.isRootContainer, |
||||
); |
||||
} |
||||
return new DataReadSuccess(resource as SolidLeaf, false); |
||||
} else { |
||||
// Load Blob
|
||||
const blob = await response.blob(); |
||||
return new BinaryReadSuccess( |
||||
resource as SolidLeaf, |
||||
false, |
||||
blob, |
||||
contentType, |
||||
); |
||||
} |
||||
} catch (err) { |
||||
return UnexpectedResourceError.fromThrown(resource, err); |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
import type { IBulkEditableDataset } from "@ldo/subscribable-dataset"; |
||||
import type { Quad } from "@rdfjs/types"; |
||||
|
||||
/** |
||||
* Request Options to be passed to request functions |
||||
*/ |
||||
export interface BasicRequestOptions { |
||||
/** |
||||
* A fetch function usually imported from @inrupt/solid-client-authn-js |
||||
*/ |
||||
fetch?: typeof fetch; |
||||
} |
||||
|
||||
/** |
||||
* Request options with a dataset component |
||||
*/ |
||||
export interface DatasetRequestOptions extends BasicRequestOptions { |
||||
/** |
||||
* A dataset to be modified with any new information obtained from a request |
||||
*/ |
||||
dataset?: IBulkEditableDataset<Quad>; |
||||
} |
@ -0,0 +1,118 @@ |
||||
import type { DatasetChanges } from "@ldo/rdf-utils"; |
||||
import { changesToSparqlUpdate } from "@ldo/rdf-utils"; |
||||
import type { Quad } from "@rdfjs/types"; |
||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { UnexpectedResourceError } from "@ldo/connected"; |
||||
import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; |
||||
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
||||
import { UpdateSuccess } from "../results/success/UpdateSuccess"; |
||||
import type { DatasetRequestOptions } from "./requestOptions"; |
||||
import type { SolidContainer } from "../../resources/SolidContainer"; |
||||
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||
|
||||
/** |
||||
* All return values for updateDataResource |
||||
*/ |
||||
export type UpdateResult<ResourceType extends Resource> = |
||||
| UpdateSuccess<ResourceType> |
||||
| UpdateResultError<ResourceType>; |
||||
|
||||
/** |
||||
* All errors updateDataResource can return |
||||
*/ |
||||
export type UpdateResultError<ResourceType extends Resource> = |
||||
| HttpErrorResultType<ResourceType> |
||||
| UnexpectedResourceError<ResourceType>; |
||||
|
||||
/** |
||||
* Updates a specific data resource with the provided dataset changes |
||||
* |
||||
* @param uri - the URI of the data resource |
||||
* @param datasetChanges - A set of triples added and removed from this dataset |
||||
* @param options - Options to provide a fetch function and a local dataset to |
||||
* update. |
||||
* @returns An UpdateResult |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* import { |
||||
* updateDataResource, |
||||
* transactionChanges, |
||||
* changeData, |
||||
* createSolidLdoDataset, |
||||
* } from "@ldo/solid"; |
||||
* import { fetch } from "@inrupt/solid-client-authn-browser"; |
||||
* |
||||
* // Initialize an LDO dataset
|
||||
* const solidLdoDataset = createSolidLdoDataset(); |
||||
* // Get a Linked Data Object
|
||||
* const profile = solidLdoDataset |
||||
* .usingType(ProfileShapeType) |
||||
* .fromSubject("https://example.com/profile#me"); |
||||
* // Create a transaction to change data
|
||||
* const cProfile = changeData( |
||||
* profile, |
||||
* solidLdoDataset.getResource("https://example.com/profile"), |
||||
* ); |
||||
* cProfile.name = "John Doe"; |
||||
* // Get data in "DatasetChanges" form
|
||||
* const datasetChanges = transactionChanges(someLinkedDataObject); |
||||
* // Use "updateDataResource" to apply the changes
|
||||
* const result = await updateDataResource( |
||||
* "https://example.com/profile", |
||||
* datasetChanges, |
||||
* { fetch, dataset: solidLdoDataset }, |
||||
* ); |
||||
* ``` |
||||
*/ |
||||
export async function updateDataResource( |
||||
resource: SolidLeaf, |
||||
datasetChanges: DatasetChanges<Quad>, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<UpdateResult<SolidLeaf>>; |
||||
export async function updateDataResource( |
||||
resource: SolidContainer, |
||||
datasetChanges: DatasetChanges<Quad>, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<UpdateResult<SolidContainer>>; |
||||
export async function updateDataResource( |
||||
resource: SolidLeaf | SolidContainer, |
||||
datasetChanges: DatasetChanges<Quad>, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<UpdateResult<SolidLeaf | SolidContainer>>; |
||||
export async function updateDataResource( |
||||
resource: SolidLeaf | SolidContainer, |
||||
datasetChanges: DatasetChanges<Quad>, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<UpdateResult<SolidLeaf | SolidContainer>> { |
||||
try { |
||||
// Optimistically add data
|
||||
options?.dataset?.bulk(datasetChanges); |
||||
const fetch = guaranteeFetch(options?.fetch); |
||||
|
||||
// Make request
|
||||
const sparqlUpdate = await changesToSparqlUpdate(datasetChanges); |
||||
const response = await fetch(resource.uri, { |
||||
method: "PATCH", |
||||
body: sparqlUpdate, |
||||
headers: { |
||||
"Content-Type": "application/sparql-update", |
||||
}, |
||||
}); |
||||
const httpError = HttpErrorResult.checkResponse(resource, response); |
||||
if (httpError) { |
||||
// Handle error rollback
|
||||
if (options?.dataset) { |
||||
options.dataset.bulk({ |
||||
added: datasetChanges.removed, |
||||
removed: datasetChanges.added, |
||||
}); |
||||
} |
||||
return httpError; |
||||
} |
||||
return new UpdateSuccess(resource); |
||||
} catch (err) { |
||||
return UnexpectedResourceError.fromThrown(resource, err); |
||||
} |
||||
} |
@ -0,0 +1,110 @@ |
||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||
import { |
||||
addResourceRdfToContainer, |
||||
getParentUri, |
||||
getSlug, |
||||
} from "../../util/rdfUtils"; |
||||
import { UnexpectedResourceError } from "@ldo/connected"; |
||||
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
||||
import type { |
||||
LeafCreateAndOverwriteResult, |
||||
LeafCreateIfAbsentResult, |
||||
} from "./createDataResource"; |
||||
import { deleteResource } from "./deleteResource"; |
||||
import { readResource } from "./readResource"; |
||||
import type { DatasetRequestOptions } from "./requestOptions"; |
||||
import type { SolidLeaf } from "../../resources/SolidLeaf"; |
||||
import { CreateSuccess } from "../results/success/CreateSuccess"; |
||||
|
||||
/** |
||||
* Uploads a binary resource at the provided URI |
||||
* |
||||
* @param uri - The URI of the resource |
||||
* @param overwrite - If true, the request will overwrite any previous resource |
||||
* at this URI. |
||||
* @param options - Options to provide a fetch function and a local dataset to |
||||
* update. |
||||
* @returns One of many create results depending on the input |
||||
* |
||||
* @example |
||||
* Any local RDFJS dataset passed to the `options` field will be updated with |
||||
* any new RDF data from the create process. |
||||
* |
||||
* ```typescript
|
||||
* import { createDataResource } from "@ldo/solid"; |
||||
* import { createDataset } from "@ldo/dataset" |
||||
* import { fetch } from "@inrupt/solid-client-autn-js"; |
||||
* |
||||
* const localDataset = createDataset(); |
||||
* const result = await uploadResource( |
||||
* "https://example.com/container/someResource.txt", |
||||
* new Blob("some text."), |
||||
* "text/txt", |
||||
* true, |
||||
* { fetch, dataset: localDataset }, |
||||
* ); |
||||
* if (!result.isError) { |
||||
* // Do something
|
||||
* } |
||||
* ``` |
||||
*/ |
||||
export function uploadResource( |
||||
resource: SolidLeaf, |
||||
blob: Blob, |
||||
mimeType: string, |
||||
overwrite: true, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<LeafCreateAndOverwriteResult>; |
||||
export function uploadResource( |
||||
resource: SolidLeaf, |
||||
blob: Blob, |
||||
mimeType: string, |
||||
overwrite?: false, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<LeafCreateIfAbsentResult>; |
||||
export async function uploadResource( |
||||
resource: SolidLeaf, |
||||
blob: Blob, |
||||
mimeType: string, |
||||
overwrite?: boolean, |
||||
options?: DatasetRequestOptions, |
||||
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult> { |
||||
try { |
||||
const fetch = guaranteeFetch(options?.fetch); |
||||
let didOverwrite = false; |
||||
if (overwrite) { |
||||
const deleteResult = await deleteResource(resource, options); |
||||
// Return if it wasn't deleted
|
||||
if (deleteResult.isError) return deleteResult; |
||||
didOverwrite = deleteResult.resourceExisted; |
||||
} else { |
||||
// Perform a read to check if it exists
|
||||
const readResult = await readResource(resource, options); |
||||
// If it does exist stop and return.
|
||||
if (readResult.type !== "absentReadSuccess") { |
||||
return readResult; |
||||
} |
||||
} |
||||
// Create the document
|
||||
const parentUri = getParentUri(resource.uri)!; |
||||
const response = await fetch(parentUri, { |
||||
method: "post", |
||||
headers: { |
||||
"content-type": mimeType, |
||||
slug: getSlug(resource.uri), |
||||
}, |
||||
body: blob, |
||||
}); |
||||
|
||||
const httpError = HttpErrorResult.checkResponse(resource, response); |
||||
if (httpError) return httpError; |
||||
|
||||
if (options?.dataset) { |
||||
addResourceRdfToContainer(resource.uri, options.dataset); |
||||
} |
||||
return new CreateSuccess(resource, didOverwrite); |
||||
} catch (err) { |
||||
const thing = UnexpectedResourceError.fromThrown(resource, err); |
||||
return thing; |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
/* istanbul ignore file */ |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { ResourceError } from "@ldo/connected"; |
||||
|
||||
/** |
||||
* An error: Could not fetch access rules |
||||
*/ |
||||
export class AccessRuleFetchError< |
||||
ResourceType extends Resource, |
||||
> extends ResourceError<ResourceType> { |
||||
readonly type = "accessRuleFetchError" as const; |
||||
|
||||
/** |
||||
* @param resource - The resource for which access rules couldn't be |
||||
* fetched |
||||
* @param message - A custom message for the error |
||||
*/ |
||||
constructor(resource: ResourceType, message?: string) { |
||||
super( |
||||
resource, |
||||
message ?? `${resource.uri} had trouble fetching access rules.`, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,176 @@ |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { ResourceError } from "@ldo/connected"; |
||||
|
||||
/** |
||||
* A set of standard errors that can be returned as a result of an HTTP request |
||||
*/ |
||||
export type HttpErrorResultType<ResourceType extends Resource> = |
||||
| ServerHttpError<ResourceType> |
||||
| UnexpectedHttpError<ResourceType> |
||||
| UnauthenticatedHttpError<ResourceType> |
||||
| UnauthorizedHttpError<ResourceType>; |
||||
|
||||
/** |
||||
* An error caused by an HTTP request |
||||
*/ |
||||
export abstract class HttpErrorResult< |
||||
ResourceType extends Resource, |
||||
> extends ResourceError<ResourceType> { |
||||
/** |
||||
* The status of the HTTP request |
||||
*/ |
||||
public readonly status: number; |
||||
|
||||
/** |
||||
* Headers returned by the HTTP request |
||||
*/ |
||||
public readonly headers: Headers; |
||||
|
||||
/** |
||||
* Response returned by the HTTP request |
||||
*/ |
||||
public readonly response: Response; |
||||
|
||||
/** |
||||
* @param resource - the resource |
||||
* @param response - The response returned by the HTTP requests |
||||
* @param message - A custom message for the error |
||||
*/ |
||||
constructor(resource: ResourceType, response: Response, message?: string) { |
||||
super( |
||||
resource, |
||||
message || |
||||
`Request for ${resource.uri} returned ${response.status} (${response.statusText}).`, |
||||
); |
||||
this.status = response.status; |
||||
this.headers = response.headers; |
||||
this.response = response; |
||||
} |
||||
|
||||
/** |
||||
* Checks to see if a given response does not constitute an HTTP Error |
||||
* @param response - The response of the request |
||||
* @returns true if the response does not constitute an HTTP Error |
||||
*/ |
||||
static isnt(response: Response) { |
||||
return ( |
||||
!(response.status >= 200 && response.status < 300) && |
||||
response.status !== 404 && |
||||
response.status !== 304 |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Checks a given response to see if it is a ServerHttpError, an |
||||
* UnauthenticatedHttpError or a some unexpected error. |
||||
* @param uri - The uri of the request |
||||
* @param response - The response of the request |
||||
* @returns An error if the response calls for it. Undefined if not. |
||||
*/ |
||||
static checkResponse<ResourceType extends Resource>( |
||||
resource: ResourceType, |
||||
response: Response, |
||||
) { |
||||
if (ServerHttpError.is(response)) { |
||||
return new ServerHttpError(resource, response); |
||||
} |
||||
if (UnauthenticatedHttpError.is(response)) { |
||||
return new UnauthenticatedHttpError(resource, response); |
||||
} |
||||
if (UnauthorizedHttpError.is(response)) { |
||||
return new UnauthorizedHttpError(resource, response); |
||||
} |
||||
if (HttpErrorResult.isnt(response)) { |
||||
return new UnexpectedHttpError(resource, response); |
||||
} |
||||
return undefined; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* An unexpected error as a result of an HTTP request. This is usually returned |
||||
* when the HTTP request returns a status code LDO does not recognize. |
||||
*/ |
||||
export class UnexpectedHttpError< |
||||
ResourceType extends Resource, |
||||
> extends HttpErrorResult<ResourceType> { |
||||
readonly type = "unexpectedHttpError" as const; |
||||
} |
||||
|
||||
/** |
||||
* An UnauthenticatedHttpError triggers when a Solid server returns a 401 status |
||||
* indicating that the request is not authenticated. |
||||
*/ |
||||
export class UnauthenticatedHttpError< |
||||
ResourceType extends Resource, |
||||
> extends HttpErrorResult<ResourceType> { |
||||
readonly type = "unauthenticatedError" as const; |
||||
|
||||
/** |
||||
* Indicates if a specific response constitutes an UnauthenticatedHttpError |
||||
* @param response - The request response |
||||
* @returns true if this response constitutes an UnauthenticatedHttpError |
||||
*/ |
||||
static is(response: Response) { |
||||
return response.status === 401; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* An UnauthenticatedHttpError triggers when a Solid server returns a 403 status |
||||
* indicating that the request is not authorized. |
||||
*/ |
||||
export class UnauthorizedHttpError< |
||||
ResourceType extends Resource, |
||||
> extends HttpErrorResult<ResourceType> { |
||||
readonly type = "unauthorizedError" as const; |
||||
|
||||
/** |
||||
* Indicates if a specific response constitutes an UnauthenticatedHttpError |
||||
* @param response - The request response |
||||
* @returns true if this response constitutes an UnauthenticatedHttpError |
||||
*/ |
||||
static is(response: Response) { |
||||
return response.status === 403; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* An NotFoundHttpError triggers when a Solid server returns a 404 status. This |
||||
* error is not returned in most cases as a "absent" resource is not considered |
||||
* an error, but it is thrown while trying for find a WAC rule for a resource |
||||
* that does not exist. |
||||
*/ |
||||
export class NotFoundHttpError< |
||||
ResourceType extends Resource, |
||||
> extends HttpErrorResult<ResourceType> { |
||||
readonly type = "notFoundError" as const; |
||||
|
||||
/** |
||||
* Indicates if a specific response constitutes an NotFoundHttpError |
||||
* @param response - The request response |
||||
* @returns true if this response constitutes an NotFoundHttpError |
||||
*/ |
||||
static is(response: Response) { |
||||
return response.status === 404; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A ServerHttpError triggers when a Solid server returns a 5XX status, |
||||
* indicating that an error happened on the server. |
||||
*/ |
||||
export class ServerHttpError< |
||||
ResourceType extends Resource, |
||||
> extends HttpErrorResult<ResourceType> { |
||||
readonly type = "serverError" as const; |
||||
|
||||
/** |
||||
* Indicates if a specific response constitutes a ServerHttpError |
||||
* @param response - The request response |
||||
* @returns true if this response constitutes a ServerHttpError |
||||
*/ |
||||
static is(response: Response) { |
||||
return response.status >= 500 && response.status < 600; |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { ResourceError } from "@ldo/connected"; |
||||
|
||||
/** |
||||
* An InvalidUriError is returned when a URI was provided that is not a valid |
||||
* URI. |
||||
*/ |
||||
export class InvalidUriError< |
||||
ResourceType extends Resource, |
||||
> extends ResourceError<ResourceType> { |
||||
readonly type = "invalidUriError" as const; |
||||
|
||||
constructor(resource: ResourceType, message?: string) { |
||||
super(resource, message || `${resource.uri} is an invalid uri.`); |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { ResourceError } from "@ldo/connected"; |
||||
|
||||
/** |
||||
* A NoncompliantPodError is returned when the server responded in a way that is |
||||
* not compliant with the Solid specification. |
||||
*/ |
||||
export class NoRootContainerError< |
||||
ResourceType extends Resource, |
||||
> extends ResourceError<ResourceType> { |
||||
readonly type = "noRootContainerError" as const; |
||||
|
||||
/** |
||||
* @param resource - the requested resource |
||||
* @param message - a custom message for the error |
||||
*/ |
||||
constructor(resource: ResourceType) { |
||||
super(resource, `${resource.uri} has not root container.`); |
||||
} |
||||
} |
@ -0,0 +1,23 @@ |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { ResourceError } from "@ldo/connected"; |
||||
|
||||
/** |
||||
* A NoncompliantPodError is returned when the server responded in a way that is |
||||
* not compliant with the Solid specification. |
||||
*/ |
||||
export class NoncompliantPodError< |
||||
ResourceType extends Resource, |
||||
> extends ResourceError<ResourceType> { |
||||
readonly type = "noncompliantPodError" as const; |
||||
|
||||
/** |
||||
* @param resource - the requested resource |
||||
* @param message - a custom message for the error |
||||
*/ |
||||
constructor(resource: ResourceType, message?: string) { |
||||
super( |
||||
resource, |
||||
`Response from ${resource.uri} is not compliant with the Solid Specification: ${message}`, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
import type { SolidContainer } from "../../../resources/SolidContainer"; |
||||
import { ResourceSuccess, SuccessResult } from "@ldo/connected"; |
||||
|
||||
/** |
||||
* Indicates that the request to check if a resource is the root container was |
||||
* a success. |
||||
*/ |
||||
export class CheckRootContainerSuccess extends ResourceSuccess<SolidContainer> { |
||||
type = "checkRootContainerSuccess" as const; |
||||
/** |
||||
* True if this resoure is the root container |
||||
*/ |
||||
isRootContainer: boolean; |
||||
|
||||
constructor(resource: SolidContainer, isRootContainer: boolean) { |
||||
super(resource); |
||||
this.isRootContainer = isRootContainer; |
||||
} |
||||
} |
||||
|
||||
export class GetStorageContainerFromWebIdSuccess extends SuccessResult { |
||||
type = "getStorageContainerFromWebIdSuccess" as const; |
||||
storageContainers: SolidContainer[]; |
||||
|
||||
constructor(storageContainers: SolidContainer[]) { |
||||
super(); |
||||
this.storageContainers = storageContainers; |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
import { ResourceSuccess } from "@ldo/connected"; |
||||
import type { Resource } from "@ldo/connected"; |
||||
|
||||
/** |
||||
* Indicates that the request to create the resource was a success. |
||||
*/ |
||||
export class CreateSuccess< |
||||
ResourceType extends Resource, |
||||
> extends ResourceSuccess<ResourceType> { |
||||
type = "createSuccess" as const; |
||||
/** |
||||
* True if there was a resource that existed before at the given URI that was |
||||
* overwritten |
||||
*/ |
||||
didOverwrite: boolean; |
||||
|
||||
constructor(resource: ResourceType, didOverwrite: boolean) { |
||||
super(resource); |
||||
this.didOverwrite = didOverwrite; |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { ResourceSuccess } from "@ldo/connected"; |
||||
|
||||
/** |
||||
* Indicates that the request to delete a resource was a success. |
||||
*/ |
||||
export class DeleteSuccess< |
||||
ResourceType extends Resource, |
||||
> extends ResourceSuccess<ResourceType> { |
||||
type = "deleteSuccess" as const; |
||||
|
||||
/** |
||||
* True if there was a resource at the provided URI that was deleted. False if |
||||
* a resource didn't exist. |
||||
*/ |
||||
resourceExisted: boolean; |
||||
|
||||
constructor(resource: ResourceType, resourceExisted: boolean) { |
||||
super(resource); |
||||
this.resourceExisted = resourceExisted; |
||||
} |
||||
} |
@ -0,0 +1,105 @@ |
||||
import { ResourceSuccess } from "@ldo/connected"; |
||||
import type { Resource, ResourceResult } from "@ldo/connected"; |
||||
import type { SolidLeaf } from "../../../resources/SolidLeaf"; |
||||
import type { SolidContainer } from "../../../resources/SolidContainer"; |
||||
|
||||
/** |
||||
* Indicates that the request to read a resource was a success |
||||
*/ |
||||
export abstract class ReadSuccess< |
||||
ResourceType extends Resource, |
||||
> extends ResourceSuccess<ResourceType> { |
||||
/** |
||||
* True if the resource was recalled from local memory rather than a recent |
||||
* request |
||||
*/ |
||||
recalledFromMemory: boolean; |
||||
|
||||
constructor(resource: ResourceType, recalledFromMemory: boolean) { |
||||
super(resource); |
||||
this.recalledFromMemory = recalledFromMemory; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Indicates that the read request was successful and that the resource |
||||
* retrieved was a binary resource. |
||||
*/ |
||||
export class BinaryReadSuccess extends ReadSuccess<SolidLeaf> { |
||||
type = "binaryReadSuccess" as const; |
||||
/** |
||||
* The raw data for the binary resource |
||||
*/ |
||||
blob: Blob; |
||||
/** |
||||
* The mime type of the binary resource |
||||
*/ |
||||
mimeType: string; |
||||
|
||||
constructor( |
||||
resource: SolidLeaf, |
||||
recalledFromMemory: boolean, |
||||
blob: Blob, |
||||
mimeType: string, |
||||
) { |
||||
super(resource, recalledFromMemory); |
||||
this.blob = blob; |
||||
this.mimeType = mimeType; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Indicates that the read request was successful and that the resource |
||||
* retrieved was a data (RDF) resource. |
||||
*/ |
||||
export class DataReadSuccess extends ReadSuccess<SolidLeaf> { |
||||
type = "dataReadSuccess" as const; |
||||
} |
||||
|
||||
/** |
||||
* Indicates that the read request was successful and that the resource |
||||
* retrieved was a container resource. |
||||
*/ |
||||
export class ContainerReadSuccess extends ReadSuccess<SolidContainer> { |
||||
type: "containerReadSuccess"; |
||||
/** |
||||
* True if this container is a root container |
||||
*/ |
||||
isRootContainer: boolean; |
||||
|
||||
constructor( |
||||
resource: SolidContainer, |
||||
recalledFromMemory: boolean, |
||||
isRootContainer: boolean, |
||||
) { |
||||
super(resource, recalledFromMemory); |
||||
this.isRootContainer = isRootContainer; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Indicates that the read request was successful, but no resource exists at |
||||
* the provided URI. |
||||
*/ |
||||
export class AbsentReadSuccess< |
||||
ResourceType extends Resource, |
||||
> extends ReadSuccess<ResourceType> { |
||||
type = "absentReadSuccess" as const; |
||||
} |
||||
|
||||
/** |
||||
* A helper function that checks to see if a result is a ReadSuccess result |
||||
* |
||||
* @param result - the result to check |
||||
* @returns true if the result is a ReadSuccessResult result |
||||
*/ |
||||
export function isReadSuccess<ResourceType extends Resource>( |
||||
result: ResourceResult<Resource>, |
||||
): result is ReadSuccess<ResourceType> { |
||||
return ( |
||||
result.type === "binaryReadSuccess" || |
||||
result.type === "dataReadSuccess" || |
||||
result.type === "absentReadSuccess" || |
||||
result.type === "containerReadSuccess" |
||||
); |
||||
} |
@ -0,0 +1,31 @@ |
||||
import { ResourceSuccess } from "@ldo/connected"; |
||||
import type { Resource } from "@ldo/connected"; |
||||
|
||||
/** |
||||
* Indicates that an update request to a resource was successful |
||||
*/ |
||||
export class UpdateSuccess< |
||||
ResourceType extends Resource, |
||||
> extends ResourceSuccess<ResourceType> { |
||||
type = "updateSuccess" as const; |
||||
} |
||||
|
||||
/** |
||||
* Indicates that an update request to the default graph was successful. This |
||||
* data was not written to a Pod. It was only written locally. |
||||
*/ |
||||
export class UpdateDefaultGraphSuccess< |
||||
ResourceType extends Resource, |
||||
> extends ResourceSuccess<ResourceType> { |
||||
type = "updateDefaultGraphSuccess" as const; |
||||
} |
||||
|
||||
/** |
||||
* Indicates that LDO ignored an invalid update (usually because a container |
||||
* attempted an update) |
||||
*/ |
||||
export class IgnoredInvalidUpdateSuccess< |
||||
ResourceType extends Resource, |
||||
> extends ResourceSuccess<ResourceType> { |
||||
type = "ignoredInvalidUpdateSuccess" as const; |
||||
} |
@ -0,0 +1,26 @@ |
||||
/* eslint-disable @typescript-eslint/no-explicit-any */ |
||||
import type { WaitingProcess } from "../../util/RequestBatcher"; |
||||
|
||||
/** |
||||
* @internal |
||||
* |
||||
* A helper function for a common way to modify the batch queue. This merges |
||||
* the incoming request with the currently executing request or the last request |
||||
* in the queue if its keys are the same. |
||||
* |
||||
* @param key - the key of the incoming request |
||||
* @returns a modifyQueue function |
||||
*/ |
||||
export function modifyQueueByMergingEventsWithTheSameKeys(key: string) { |
||||
return ( |
||||
queue: WaitingProcess<any[], any>[], |
||||
currentlyLoading: WaitingProcess<any[], any> | undefined, |
||||
) => { |
||||
if (queue.length === 0 && currentlyLoading?.name === key) { |
||||
return currentlyLoading; |
||||
} else if (queue[queue.length - 1]?.name === key) { |
||||
return queue[queue.length - 1]; |
||||
} |
||||
return undefined; |
||||
}; |
||||
} |
@ -1,3 +1,14 @@ |
||||
import type { Resource } from "@ldo/connected"; |
||||
import { SolidResource } from "./SolidResource"; |
||||
import type { SolidContainerUri, SolidLeafUri } from "../types"; |
||||
|
||||
export class SolidLeaf extends SolidResource {} |
||||
export class SolidLeaf |
||||
extends SolidResource |
||||
implements Resource<SolidLeafUri> { |
||||
public uri: SolidLeafUri; |
||||
|
||||
constructor() { |
||||
super() |
||||
} |
||||
|
||||
} |
||||
|
@ -1,126 +1,821 @@ |
||||
import type { |
||||
ConnectedContext, |
||||
ConnectedResult, |
||||
Resource, |
||||
ResourceEventEmitter, |
||||
ResourceResult, |
||||
SubscriptionCallbacks, |
||||
ResourceSuccess, |
||||
} from "@ldo/connected"; |
||||
import type { SolidContainerUri, SolidLeafUri } from "../types"; |
||||
import EventEmitter from "events"; |
||||
import type { SolidConnectedPlugin } from "../SolidConnectedPlugin"; |
||||
import type { BatchedRequester } from "../requester/BatchedRequester"; |
||||
import type { WacRule } from "../wac/WacRule"; |
||||
import type { SolidNotificationSubscription } from "../notifications/SolidNotificationSubscription"; |
||||
import { Websocket2023NotificationSubscription } from "../notifications/Websocket2023NotificationSubscription"; |
||||
import { getParentUri } from "../util/rdfUtils"; |
||||
import { |
||||
isReadSuccess, |
||||
type ReadSuccess, |
||||
} from "../requester/results/success/ReadSuccess"; |
||||
import type { |
||||
ReadContainerResult, |
||||
ReadLeafResult, |
||||
} from "../requester/requests/readResource"; |
||||
import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; |
||||
import type { DeleteResult } from "../requester/requests/deleteResource"; |
||||
import type { |
||||
ContainerCreateAndOverwriteResult, |
||||
ContainerCreateIfAbsentResult, |
||||
LeafCreateAndOverwriteResult, |
||||
LeafCreateIfAbsentResult, |
||||
} from "../requester/requests/createDataResource"; |
||||
import type { SolidContainer } from "./SolidContainer"; |
||||
import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; |
||||
import type { NoRootContainerError } from "../requester/results/error/NoRootContainerError"; |
||||
import type { SolidLeaf } from "./SolidLeaf"; |
||||
import type { GetWacUriError } from "../wac/getWacUri"; |
||||
import { getWacUri, type GetWacUriResult } from "../wac/getWacUri"; |
||||
import { getWacRuleWithAclUri, type GetWacRuleResult } from "../wac/getWacRule"; |
||||
import { setWacRuleForAclUri } from "../wac/setWacRule"; |
||||
import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError"; |
||||
|
||||
export class SolidResource |
||||
export abstract class SolidResource |
||||
extends (EventEmitter as new () => ResourceEventEmitter) |
||||
implements Resource<SolidLeafUri | SolidContainerUri> |
||||
{ |
||||
uri: SolidLeafUri | SolidContainerUri; |
||||
type: string; |
||||
status: ConnectedResult; |
||||
isLoading(): boolean { |
||||
throw new Error("Method not implemented."); |
||||
/** |
||||
* @internal |
||||
* The ConnectedContext from the Parent Dataset |
||||
*/ |
||||
protected readonly context: ConnectedContext<SolidConnectedPlugin[]>; |
||||
|
||||
/** |
||||
* The uri of the resource |
||||
*/ |
||||
abstract readonly uri: SolidLeafUri | SolidContainerUri; |
||||
|
||||
/** |
||||
* The type of resource (leaf or container) |
||||
*/ |
||||
abstract readonly type: "leaf" | "container"; |
||||
|
||||
/** |
||||
* The status of the last request made for this resource |
||||
*/ |
||||
abstract status: ConnectedResult; |
||||
|
||||
/** |
||||
* @internal |
||||
* Batched Requester for the Resource |
||||
*/ |
||||
protected abstract readonly requester: BatchedRequester<this>; |
||||
|
||||
/** |
||||
* @internal |
||||
* True if this resource has been fetched at least once |
||||
*/ |
||||
protected didInitialFetch: boolean = false; |
||||
|
||||
/** |
||||
* @internal |
||||
* True if this resource has been fetched but does not exist |
||||
*/ |
||||
protected absent: boolean | undefined; |
||||
|
||||
/** |
||||
* @internal |
||||
* If a wac uri is fetched, it is cached here |
||||
*/ |
||||
protected wacUri?: SolidLeafUri; |
||||
|
||||
/** |
||||
* @internal |
||||
* If a wac rule was fetched, it is cached here |
||||
*/ |
||||
protected wacRule?: WacRule; |
||||
|
||||
/** |
||||
* @internal |
||||
* Handles notification subscriptions |
||||
*/ |
||||
protected notificationSubscription: SolidNotificationSubscription; |
||||
|
||||
/** |
||||
* Indicates that resources are not errors |
||||
*/ |
||||
public readonly isError: false = false as const; |
||||
|
||||
/** |
||||
* @param context - SolidLdoDatasetContext for the parent dataset |
||||
*/ |
||||
constructor(context: ConnectedContext<SolidConnectedPlugin[]>) { |
||||
super(); |
||||
this.context = context; |
||||
this.notificationSubscription = new Websocket2023NotificationSubscription( |
||||
this, |
||||
this.onNotification.bind(this), |
||||
this.context, |
||||
); |
||||
} |
||||
isFetched(): boolean { |
||||
|
||||
readIfAbsent(): Promise<ResourceResult<this>> { |
||||
throw new Error("Method not implemented."); |
||||
} |
||||
isUnfetched(): boolean { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* GETTERS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* Checks to see if this resource is loading in any way |
||||
* @returns true if the resource is currently loading |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* resource.read().then(() => { |
||||
* // Logs "false"
|
||||
* console.log(resource.isLoading()) |
||||
* }); |
||||
* // Logs "true"
|
||||
* console.log(resource.isLoading()); |
||||
* ``` |
||||
*/ |
||||
isLoading(): boolean { |
||||
return this.requester.isLoading(); |
||||
} |
||||
isDoingInitialFetch(): boolean { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Checks to see if this resource is being created |
||||
* @returns true if the resource is currently being created |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* resource.read().then(() => { |
||||
* // Logs "false"
|
||||
* console.log(resource.isCreating()) |
||||
* }); |
||||
* // Logs "true"
|
||||
* console.log(resource.isCreating()); |
||||
* ``` |
||||
*/ |
||||
isCreating(): boolean { |
||||
return this.requester.isCreating(); |
||||
} |
||||
isPresent(): boolean { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Checks to see if this resource is being read |
||||
* @returns true if the resource is currently being read |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* resource.read().then(() => { |
||||
* // Logs "false"
|
||||
* console.log(resource.isReading()) |
||||
* }); |
||||
* // Logs "true"
|
||||
* console.log(resource.isReading()); |
||||
* ``` |
||||
*/ |
||||
isReading(): boolean { |
||||
return this.requester.isReading(); |
||||
} |
||||
isAbsent(): boolean { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Checks to see if this resource is being deleted |
||||
* @returns true if the resource is currently being deleted |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* resource.read().then(() => { |
||||
* // Logs "false"
|
||||
* console.log(resource.isDeleting()) |
||||
* }); |
||||
* // Logs "true"
|
||||
* console.log(resource.isDeleting()); |
||||
* ``` |
||||
*/ |
||||
isDeleting(): boolean { |
||||
return this.requester.isDeletinng(); |
||||
} |
||||
isSubscribedToNotifications(): boolean { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Checks to see if this resource is being read for the first time |
||||
* @returns true if the resource is currently being read for the first time |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* resource.read().then(() => { |
||||
* // Logs "false"
|
||||
* console.log(resource.isDoingInitialFetch()) |
||||
* }); |
||||
* // Logs "true"
|
||||
* console.log(resource.isDoingInitialFetch()); |
||||
* ``` |
||||
*/ |
||||
isDoingInitialFetch(): boolean { |
||||
return this.isReading() && !this.isFetched(); |
||||
} |
||||
read(): Promise<ResourceResult<this>> { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Checks to see if this resource is being read for a subsequent time |
||||
* @returns true if the resource is currently being read for a subsequent time |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* await resource.read(); |
||||
* resource.read().then(() => { |
||||
* // Logs "false"
|
||||
* console.log(resource.isCreating()) |
||||
* }); |
||||
* // Logs "true"
|
||||
* console.log(resource.isCreating()); |
||||
* ``` |
||||
*/ |
||||
isReloading(): boolean { |
||||
return this.isReading() && this.isFetched(); |
||||
} |
||||
readIfAbsent(): Promise<ResourceResult<this>> { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* CHECKERS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* Check to see if this resource has been fetched |
||||
* @returns true if this resource has been fetched before |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* // Logs "false"
|
||||
* console.log(resource.isFetched()); |
||||
* const result = await resource.read(); |
||||
* if (!result.isError) { |
||||
* // Logs "true"
|
||||
* console.log(resource.isFetched()); |
||||
* } |
||||
* ``` |
||||
*/ |
||||
isFetched(): boolean { |
||||
return this.didInitialFetch; |
||||
} |
||||
subscribeToNotifications(callbacks?: SubscriptionCallbacks): Promise<string> { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Check to see if this resource is currently unfetched |
||||
* @returns true if the resource is currently unfetched |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* // Logs "true"
|
||||
* console.log(resource.isUnetched()); |
||||
* const result = await resource.read(); |
||||
* if (!result.isError) { |
||||
* // Logs "false"
|
||||
* console.log(resource.isUnfetched()); |
||||
* } |
||||
* ``` |
||||
*/ |
||||
isUnfetched(): boolean { |
||||
return !this.didInitialFetch; |
||||
} |
||||
unsubscribeFromNotifications(subscriptionId: string): Promise<void> { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Is this resource currently absent (it does not exist) |
||||
* @returns true if the resource is absent, false if not, undefined if unknown |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* // Logs "undefined"
|
||||
* console.log(resource.isAbsent()); |
||||
* const result = await resource.read(); |
||||
* if (!result.isError) { |
||||
* // False if the resource exists, true if it does not
|
||||
* console.log(resource.isAbsent()); |
||||
* } |
||||
* ``` |
||||
*/ |
||||
isAbsent(): boolean | undefined { |
||||
return this.absent; |
||||
} |
||||
unsubscribeFromAllNotifications(): Promise<void> { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Is this resource currently present on the Pod |
||||
* @returns false if the resource is absent, true if not, undefined if unknown |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* // Logs "undefined"
|
||||
* console.log(resource.isPresent()); |
||||
* const result = await resource.read(); |
||||
* if (!result.isError) { |
||||
* // True if the resource exists, false if it does not
|
||||
* console.log(resource.isPresent()); |
||||
* } |
||||
* ``` |
||||
*/ |
||||
isPresent(): boolean | undefined { |
||||
return this.absent === undefined ? undefined : !this.absent; |
||||
} |
||||
addListener<E extends "update" | "notification">( |
||||
event: E, |
||||
listener: { update: () => void; notification: () => void }[E], |
||||
): this { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Is this resource currently listening to notifications from this document |
||||
* @returns true if the resource is subscribed to notifications, false if not |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* await resource.subscribeToNotifications(); |
||||
* // Logs "true"
|
||||
* console.log(resource.isSubscribedToNotifications()); |
||||
* ``` |
||||
*/ |
||||
isSubscribedToNotifications(): boolean { |
||||
return this.notificationSubscription.isSubscribedToNotifications(); |
||||
} |
||||
on<E extends "update" | "notification">( |
||||
event: E, |
||||
listener: { update: () => void; notification: () => void }[E], |
||||
): this { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* HELPER METHODS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* @internal |
||||
* Emits an update event for both this resource and the parent |
||||
*/ |
||||
protected emitThisAndParent() { |
||||
this.emit("update"); |
||||
const parentUri = getParentUri(this.uri); |
||||
if (parentUri) { |
||||
const parentContainer = this.context.dataset.getResource(parentUri); |
||||
parentContainer.emit("update"); |
||||
} |
||||
} |
||||
once<E extends "update" | "notification">( |
||||
event: E, |
||||
listener: { update: () => void; notification: () => void }[E], |
||||
): this { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* READ METHODS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* @internal |
||||
* A helper method updates this resource's internal state upon read success |
||||
* @param result - the result of the read success |
||||
*/ |
||||
protected updateWithReadSuccess(result: ReadSuccess<this>) { |
||||
this.absent = result.type === "absentReadSuccess"; |
||||
this.didInitialFetch = true; |
||||
} |
||||
prependListener<E extends "update" | "notification">( |
||||
event: E, |
||||
listener: { update: () => void; notification: () => void }[E], |
||||
): this { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* @internal |
||||
* A helper method that handles the core functions for reading |
||||
* @returns ReadResult |
||||
*/ |
||||
protected async handleRead(): Promise<ReadContainerResult | ReadLeafResult> { |
||||
const result = await this.requester.read(); |
||||
this.status = result; |
||||
if (result.isError) return result; |
||||
this.updateWithReadSuccess(result); |
||||
this.emitThisAndParent(); |
||||
return result; |
||||
} |
||||
prependOnceListener<E extends "update" | "notification">( |
||||
event: E, |
||||
listener: { update: () => void; notification: () => void }[E], |
||||
): this { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* @internal |
||||
* Converts the current state of this resource to a readResult |
||||
* @returns a ReadResult |
||||
*/ |
||||
protected abstract toReadResult(): ReadLeafResult | ReadContainerResult; |
||||
|
||||
/** |
||||
* Reads the resource |
||||
*/ |
||||
abstract read(): Promise<ReadLeafResult | ReadContainerResult>; |
||||
|
||||
/** |
||||
* Reads the resource if it isn't fetched yet |
||||
* @returns a ReadResult |
||||
*/ |
||||
async readIfUnfetched(): Promise<ReadLeafResult | ReadContainerResult> { |
||||
if (this.didInitialFetch) { |
||||
const readResult = this.toReadResult(); |
||||
this.status = readResult; |
||||
return readResult; |
||||
} |
||||
return this.read(); |
||||
} |
||||
off<E extends "update" | "notification">( |
||||
event: E, |
||||
listener: { update: () => void; notification: () => void }[E], |
||||
): this { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* DELETE METHODS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* @internal |
||||
* A helper method updates this resource's internal state upon delete success |
||||
* @param result - the result of the delete success |
||||
*/ |
||||
public updateWithDeleteSuccess(_result: DeleteSuccess<this>) { |
||||
this.absent = true; |
||||
this.didInitialFetch = true; |
||||
} |
||||
removeAllListeners<E extends "update" | "notification">( |
||||
event?: E | undefined, |
||||
): this { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* @internal |
||||
* Helper method that handles the core functions for deleting a resource |
||||
* @returns DeleteResult |
||||
*/ |
||||
protected async handleDelete(): Promise<DeleteResult<this>> { |
||||
const result = await this.requester.delete(); |
||||
this.status = result; |
||||
if (result.isError) return result; |
||||
this.updateWithDeleteSuccess(result); |
||||
this.emitThisAndParent(); |
||||
return result; |
||||
} |
||||
removeListener<E extends "update" | "notification">( |
||||
event: E, |
||||
listener: { update: () => void; notification: () => void }[E], |
||||
): this { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* CREATE METHODS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* A helper method updates this resource's internal state upon create success |
||||
* @param _result - the result of the create success |
||||
*/ |
||||
protected updateWithCreateSuccess(result: ResourceSuccess<this>) { |
||||
this.absent = false; |
||||
this.didInitialFetch = true; |
||||
if (isReadSuccess(result)) { |
||||
this.updateWithReadSuccess(result); |
||||
} |
||||
} |
||||
emit<E extends "update" | "notification">( |
||||
event: E, |
||||
...args: Parameters<{ update: () => void; notification: () => void }[E]> |
||||
): boolean { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Creates a resource at this URI and overwrites any that already exists |
||||
* @returns CreateAndOverwriteResult |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* const result = await resource.createAndOverwrite(); |
||||
* if (!result.isError) { |
||||
* // Do something
|
||||
* } |
||||
* ``` |
||||
*/ |
||||
abstract createAndOverwrite(): Promise< |
||||
ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult |
||||
>; |
||||
|
||||
/** |
||||
* @internal |
||||
* Helper method that handles the core functions for creating and overwriting |
||||
* a resource |
||||
* @returns DeleteResult |
||||
*/ |
||||
protected async handleCreateAndOverwrite(): 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; |
||||
} |
||||
eventNames(): (string | symbol)[] { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Creates a resource at this URI if the resource doesn't already exist |
||||
* @returns CreateIfAbsentResult |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* const result = await leaf.createIfAbsent(); |
||||
* if (!result.isError) { |
||||
* // Do something
|
||||
* } |
||||
* ``` |
||||
*/ |
||||
abstract createIfAbsent(): Promise< |
||||
ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult |
||||
>; |
||||
|
||||
/** |
||||
* @internal |
||||
* Helper method that handles the core functions for creating a resource if |
||||
* absent |
||||
* @returns DeleteResult |
||||
*/ |
||||
protected async handleCreateIfAbsent(): Promise< |
||||
ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult |
||||
> { |
||||
const result = await this.requester.createDataResource(); |
||||
this.status = result; |
||||
if (result.isError) return result; |
||||
this.updateWithCreateSuccess(result); |
||||
this.emitThisAndParent(); |
||||
return result; |
||||
} |
||||
rawListeners<E extends "update" | "notification">( |
||||
event: E, |
||||
): { update: () => void; notification: () => void }[E][] { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* PARENT CONTAINER METHODS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* Gets the root container for this resource. |
||||
* @returns The root container for this resource |
||||
* |
||||
* @example |
||||
* Suppose the root container is at `https://example.com/` |
||||
* |
||||
* ```typescript
|
||||
* const resource = ldoSolidDataset |
||||
* .getResource("https://example.com/container/resource.ttl"); |
||||
* const rootContainer = await resource.getRootContainer(); |
||||
* if (!rootContainer.isError) { |
||||
* // logs "https://example.com/"
|
||||
* console.log(rootContainer.uri); |
||||
* } |
||||
* ``` |
||||
*/ |
||||
abstract getRootContainer(): Promise< |
||||
| SolidContainer |
||||
| CheckRootResultError |
||||
| NoRootContainerError<SolidLeaf | SolidContainer> |
||||
>; |
||||
|
||||
abstract getParentContainer(): Promise< |
||||
SolidContainer | CheckRootResultError | undefined |
||||
>; |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* WEB ACCESS CONTROL METHODS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* Retrieves the URI for the web access control (WAC) rules for this resource |
||||
* @param options - set the "ignoreCache" field to true to ignore any cached |
||||
* information on WAC rules. |
||||
* @returns WAC Rules results |
||||
*/ |
||||
protected async getWacUri(options?: { |
||||
ignoreCache: boolean; |
||||
}): Promise<GetWacUriResult> { |
||||
// Get the wacUri if not already present
|
||||
if (!options?.ignoreCache && this.wacUri) { |
||||
return { |
||||
type: "getWacUriSuccess", |
||||
wacUri: this.wacUri, |
||||
isError: false, |
||||
uri: this.uri, |
||||
}; |
||||
} |
||||
|
||||
const wacUriResult = await getWacUri(this.uri, { |
||||
fetch: this.context.solid.fetch, |
||||
}); |
||||
if (wacUriResult.isError) { |
||||
return wacUriResult; |
||||
} |
||||
this.wacUri = wacUriResult.wacUri; |
||||
return wacUriResult; |
||||
} |
||||
listeners<E extends "update" | "notification">( |
||||
event: E, |
||||
): { update: () => void; notification: () => void }[E][] { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Retrieves web access control (WAC) rules for this resource |
||||
* @param options - set the "ignoreCache" field to true to ignore any cached |
||||
* information on WAC rules. |
||||
* @returns WAC Rules results |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* const resource = ldoSolidDataset |
||||
* .getResource("https://example.com/container/resource.ttl"); |
||||
* const wacRulesResult = await resource.getWac(); |
||||
* if (!wacRulesResult.isError) { |
||||
* const wacRules = wacRulesResult.wacRule; |
||||
* // True if the resource is publicly readable
|
||||
* console.log(wacRules.public.read); |
||||
* // True if authenticated agents can write to the resource
|
||||
* console.log(wacRules.authenticated.write); |
||||
* // True if the given WebId has append access
|
||||
* console.log( |
||||
* wacRules.agent[https://example.com/person1/profile/card#me].append
|
||||
* ); |
||||
* // True if the given WebId has control access
|
||||
* console.log( |
||||
* wacRules.agent[https://example.com/person1/profile/card#me].control
|
||||
* ); |
||||
* } |
||||
* ``` |
||||
*/ |
||||
async getWac(options?: { |
||||
ignoreCache: boolean; |
||||
}): Promise<GetWacUriError | GetWacRuleResult> { |
||||
// Return the wac rule if it's already cached
|
||||
if (!options?.ignoreCache && this.wacRule) { |
||||
return { |
||||
type: "getWacRuleSuccess", |
||||
uri: this.uri, |
||||
isError: false, |
||||
wacRule: this.wacRule, |
||||
}; |
||||
} |
||||
|
||||
// Get the wac uri
|
||||
const wacUriResult = await this.getWacUri(options); |
||||
if (wacUriResult.isError) return wacUriResult; |
||||
|
||||
// Get the wac rule
|
||||
const wacResult = await getWacRuleWithAclUri(wacUriResult.wacUri, { |
||||
fetch: this.context.solid.fetch, |
||||
}); |
||||
if (wacResult.isError) return wacResult; |
||||
// If the wac rules was successfully found
|
||||
if (wacResult.type === "getWacRuleSuccess") { |
||||
this.wacRule = wacResult.wacRule; |
||||
return wacResult; |
||||
} |
||||
|
||||
// If the WacRule is absent
|
||||
const parentResource = await this.getParentContainer(); |
||||
if (parentResource?.isError) return parentResource; |
||||
if (!parentResource) { |
||||
return new NoncompliantPodError( |
||||
this, |
||||
`Resource "${this.uri}" has no Effective ACL resource`, |
||||
); |
||||
} |
||||
return parentResource.getWac(); |
||||
} |
||||
listenerCount<E extends "update" | "notification">(event: E): number { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* Sets access rules for a specific resource |
||||
* @param wacRule - the access rules to set |
||||
* @returns SetWacRuleResult |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* const resource = ldoSolidDataset |
||||
* .getResource("https://example.com/container/resource.ttl"); |
||||
* const wacRulesResult = await resource.setWac({ |
||||
* public: { |
||||
* read: true, |
||||
* write: false, |
||||
* append: false, |
||||
* control: false |
||||
* }, |
||||
* authenticated: { |
||||
* read: true, |
||||
* write: false, |
||||
* append: true, |
||||
* control: false |
||||
* }, |
||||
* agent: { |
||||
* "https://example.com/person1/profile/card#me": { |
||||
* read: true, |
||||
* write: true, |
||||
* append: true, |
||||
* control: true |
||||
* } |
||||
* } |
||||
* }); |
||||
* ``` |
||||
*/ |
||||
async setWac(wacRule: WacRule): Promise<GetWacUriError | SetWacRuleResult> { |
||||
const wacUriResult = await this.getWacUri(); |
||||
if (wacUriResult.isError) return wacUriResult; |
||||
|
||||
const result = await setWacRuleForAclUri( |
||||
wacUriResult.wacUri, |
||||
wacRule, |
||||
this.uri, |
||||
{ |
||||
fetch: this.context.solid.fetch, |
||||
}, |
||||
); |
||||
if (result.isError) return result; |
||||
this.wacRule = result.wacRule; |
||||
return result; |
||||
} |
||||
getMaxListeners(): number { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* =========================================================================== |
||||
* SUBSCRIPTION METHODS |
||||
* =========================================================================== |
||||
*/ |
||||
|
||||
/** |
||||
* Activates Websocket subscriptions on this resource. Updates, deletions, |
||||
* and creations on this resource will be tracked and all changes will be |
||||
* relected in LDO's resources and graph. |
||||
* |
||||
* @param onNotificationError - A callback function if there is an error |
||||
* with notifications. |
||||
* @returns SubscriptionId: A string to use to unsubscribe |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* const resource = solidLdoDataset |
||||
* .getResource("https://example.com/spiderman"); |
||||
* // A listener for if anything about spiderman in the global dataset is
|
||||
* // changed. Note that this will also listen for any local changes as well
|
||||
* // as changes to remote resources to which you have notification
|
||||
* // subscriptions enabled.
|
||||
* solidLdoDataset.addListener( |
||||
* [namedNode("https://example.com/spiderman#spiderman"), null, null, null], |
||||
* () => { |
||||
* // Triggers when the file changes on the Pod or locally
|
||||
* console.log("Something changed about SpiderMan"); |
||||
* }, |
||||
* ); |
||||
* |
||||
* // Subscribe
|
||||
* const subscriptionId = await testContainer.subscribeToNotifications({ |
||||
* // These are optional callbacks. A subscription will automatically keep
|
||||
* // the dataset in sync. Use these callbacks for additional functionality.
|
||||
* onNotification: (message) => console.log(message), |
||||
* onNotificationError: (err) => console.log(err.message) |
||||
* }); |
||||
* // ... From there you can wait for a file to be changed on the Pod.
|
||||
*/ |
||||
async subscribeToNotifications( |
||||
callbacks?: SubscriptionCallbacks, |
||||
): Promise<string> { |
||||
return await this.notificationSubscription.subscribeToNotifications( |
||||
callbacks, |
||||
); |
||||
} |
||||
setMaxListeners(maxListeners: number): this { |
||||
throw new Error("Method not implemented."); |
||||
|
||||
/** |
||||
* @internal |
||||
* Function that triggers whenever a notification is recieved. |
||||
*/ |
||||
protected async onNotification(message: NotificationMessage): Promise<void> { |
||||
const objectResource = this.context.solidLdoDataset.getResource( |
||||
message.object, |
||||
); |
||||
switch (message.type) { |
||||
case "Update": |
||||
case "Add": |
||||
await objectResource.read(); |
||||
return; |
||||
case "Delete": |
||||
case "Remove": |
||||
// Delete the resource without have to make an additional read request
|
||||
updateDatasetOnSuccessfulDelete( |
||||
message.object, |
||||
this.context.solidLdoDataset, |
||||
); |
||||
objectResource.updateWithDeleteSuccess({ |
||||
type: "deleteSuccess", |
||||
isError: false, |
||||
uri: message.object, |
||||
resourceExisted: true, |
||||
}); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Unsubscribes from changes made to this resource on the Pod |
||||
* |
||||
* @returns UnsubscribeResult |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* const subscriptionId = await testContainer.subscribeToNotifications(); |
||||
* await testContainer.unsubscribeFromNotifications(subscriptionId); |
||||
* ``` |
||||
*/ |
||||
async unsubscribeFromNotifications(subscriptionId: string): Promise<void> { |
||||
return this.notificationSubscription.unsubscribeFromNotification( |
||||
subscriptionId, |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Unsubscribes from all notifications on this resource |
||||
* |
||||
* @returns UnsubscribeResult[] |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* const subscriptionResult = await testContainer.subscribeToNotifications(); |
||||
* await testContainer.unsubscribeFromAllNotifications(); |
||||
* ``` |
||||
*/ |
||||
async unsubscribeFromAllNotifications(): Promise<void> { |
||||
return this.notificationSubscription.unsubscribeFromAllNotifications(); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,187 @@ |
||||
/* eslint-disable @typescript-eslint/no-explicit-any */ |
||||
export interface WaitingProcess<Args extends any[], Return> { |
||||
name: string; |
||||
args: Args; |
||||
perform: (...args: Args) => Promise<Return>; |
||||
awaitingResolutions: ((returnValue: Return) => void)[]; |
||||
awaitingRejections: ((err: any) => void)[]; |
||||
after?: (result: Return) => void; |
||||
} |
||||
|
||||
export const ANY_KEY = "any"; |
||||
|
||||
/** |
||||
* Options for processes that are waiting to execute |
||||
*/ |
||||
export interface WaitingProcessOptions<Args extends any[], Return> { |
||||
/** |
||||
* The name of the process like "read" or "delete" |
||||
*/ |
||||
name: string; |
||||
/** |
||||
* The arguements supplied to the process |
||||
*/ |
||||
args: Args; |
||||
/** |
||||
* A function that will be triggered when it's time to execute this process |
||||
* @param args - arguments supplied to the process |
||||
* @returns a return type |
||||
*/ |
||||
perform: (...args: Args) => Promise<Return>; |
||||
/** |
||||
* A custom function to modify the queue based on the current state of the |
||||
* queue |
||||
* @param processQueue - The current process queue |
||||
* @param currentlyProcessing - The Process that is currently executing |
||||
* @param args - provided args |
||||
* @returns A WaitingProcess that this request should listen to, or undefined |
||||
* if it should create its own |
||||
*/ |
||||
modifyQueue: ( |
||||
processQueue: WaitingProcess<any[], any>[], |
||||
currentlyProcessing: WaitingProcess<any[], any> | undefined, |
||||
args: Args, |
||||
) => WaitingProcess<any[], any> | undefined; |
||||
after?: (result: Return) => void; |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* A utility for batching a request |
||||
*/ |
||||
export class RequestBatcher { |
||||
/** |
||||
* A mapping between a process key and the last time in UTC a process of that |
||||
* key was executed. |
||||
*/ |
||||
private lastRequestTimestampMap: Record<string, number> = {}; |
||||
|
||||
/** |
||||
* A pointer to the current process the batcher is working on |
||||
*/ |
||||
private currentlyProcessing: WaitingProcess<any[], any> | undefined = |
||||
undefined; |
||||
|
||||
/** |
||||
* A queue of upcoming processes |
||||
*/ |
||||
private processQueue: WaitingProcess<any[], any>[] = []; |
||||
|
||||
/** |
||||
* The amount of time (in milliseconds) between requests of the same key |
||||
*/ |
||||
public batchMillis: number; |
||||
|
||||
/** |
||||
* @param options - options, including the value for batchMillis |
||||
*/ |
||||
constructor( |
||||
options?: Partial<{ |
||||
batchMillis: number; |
||||
}>, |
||||
) { |
||||
this.batchMillis = options?.batchMillis || 1000; |
||||
} |
||||
|
||||
/** |
||||
* Check if the request batcher is currently working on a process |
||||
* @param key - the key of the process to check |
||||
* @returns true if the batcher is currently working on the provided process |
||||
*/ |
||||
public isLoading(key: string): boolean { |
||||
if (key === ANY_KEY) return !!this.currentlyProcessing; |
||||
return this.currentlyProcessing?.name === key; |
||||
} |
||||
|
||||
/** |
||||
* Triggers the next process in the queue or triggers a timeout to wait to |
||||
* execute the next process in the queue if not enough time has passed since |
||||
* the last process was triggered. |
||||
*/ |
||||
private triggerOrWaitProcess() { |
||||
if (!this.processQueue[0] || this.currentlyProcessing) { |
||||
return; |
||||
} |
||||
this.currentlyProcessing = this.processQueue.shift(); |
||||
const processName = this.currentlyProcessing!.name; |
||||
|
||||
// Set last request timestamp if not available
|
||||
if (!this.lastRequestTimestampMap[processName]) { |
||||
this.lastRequestTimestampMap[processName] = Date.UTC(0, 0, 0, 0, 0, 0, 0); |
||||
} |
||||
|
||||
const lastRequestTimestamp = this.lastRequestTimestampMap[processName]; |
||||
const timeSinceLastTrigger = Date.now() - lastRequestTimestamp; |
||||
|
||||
const triggerProcess = async () => { |
||||
this.lastRequestTimestampMap[processName] = Date.now(); |
||||
this.lastRequestTimestampMap[ANY_KEY] = Date.now(); |
||||
// Remove the process from the queue
|
||||
const processToTrigger = this.currentlyProcessing; |
||||
if (processToTrigger) { |
||||
this.currentlyProcessing = processToTrigger; |
||||
try { |
||||
const returnValue = await processToTrigger.perform( |
||||
...processToTrigger.args, |
||||
); |
||||
if (processToTrigger.after) { |
||||
processToTrigger.after(returnValue); |
||||
} |
||||
processToTrigger.awaitingResolutions.forEach((callback) => { |
||||
callback(returnValue); |
||||
}); |
||||
} catch (err) { |
||||
processToTrigger.awaitingRejections.forEach((callback) => { |
||||
callback(err); |
||||
}); |
||||
} |
||||
this.currentlyProcessing = undefined; |
||||
|
||||
this.triggerOrWaitProcess(); |
||||
} |
||||
}; |
||||
|
||||
if (timeSinceLastTrigger < this.batchMillis) { |
||||
setTimeout(triggerProcess, this.batchMillis - timeSinceLastTrigger); |
||||
} else { |
||||
triggerProcess(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Adds a process to the queue and waits for the process to be complete |
||||
* @param options - WaitingProcessOptions |
||||
* @returns A promise that resolves when the process resolves |
||||
*/ |
||||
public async queueProcess<Args extends any[], ReturnType>( |
||||
options: WaitingProcessOptions<Args, ReturnType>, |
||||
): Promise<ReturnType> { |
||||
return new Promise((resolve, reject) => { |
||||
const shouldAwait = options.modifyQueue( |
||||
this.processQueue, |
||||
this.currentlyProcessing, |
||||
options.args, |
||||
); |
||||
|
||||
if (shouldAwait) { |
||||
shouldAwait.awaitingResolutions.push(resolve); |
||||
shouldAwait.awaitingRejections.push(reject); |
||||
return; |
||||
} |
||||
|
||||
const waitingProcess: WaitingProcess<Args, ReturnType> = { |
||||
name: options.name, |
||||
args: options.args, |
||||
perform: options.perform, |
||||
awaitingResolutions: [resolve], |
||||
awaitingRejections: [reject], |
||||
after: options.after, |
||||
}; |
||||
// HACK: Ugly cast
|
||||
this.processQueue.push( |
||||
waitingProcess as unknown as WaitingProcess<any[], any>, |
||||
); |
||||
this.triggerOrWaitProcess(); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
import crossFetch from "cross-fetch"; |
||||
|
||||
/** |
||||
* @internal |
||||
* Guantees that some kind of fetch is available |
||||
* |
||||
* @param fetchInput - A potential fetch object |
||||
* @returns a proper fetch object. Cross-fetch is default |
||||
*/ |
||||
export function guaranteeFetch(fetchInput?: typeof fetch): typeof fetch { |
||||
return fetchInput || crossFetch; |
||||
} |
@ -0,0 +1,149 @@ |
||||
import type { LdoDataset } from "@ldo/ldo"; |
||||
import { parseRdf } from "@ldo/ldo"; |
||||
import { namedNode, quad as createQuad } from "@rdfjs/data-model"; |
||||
import type { Dataset } from "@rdfjs/types"; |
||||
import type { ContainerUri } from "./uriTypes"; |
||||
import { isContainerUri } from "./uriTypes"; |
||||
import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError"; |
||||
import { UnexpectedResourceError } from "../requester/results/error/ErrorResult"; |
||||
|
||||
export const ldpContains = namedNode("http://www.w3.org/ns/ldp#contains"); |
||||
export const rdfType = namedNode( |
||||
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
||||
); |
||||
export const ldpResource = namedNode("http://www.w3.org/ns/ldp#Resource"); |
||||
export const ldpContainer = namedNode("http://www.w3.org/ns/ldp#Container"); |
||||
export const ldpBasicContainer = namedNode( |
||||
"http://www.w3.org/ns/ldp#BasicContainer", |
||||
); |
||||
|
||||
/** |
||||
* @internal |
||||
* Gets the URI of a parent according the the Solid Spec |
||||
* |
||||
* @param uri - the child URI |
||||
* @returns A parent URI or undefined if not possible |
||||
*/ |
||||
export function getParentUri(uri: string): ContainerUri | undefined { |
||||
const urlObject = new URL(uri); |
||||
const pathItems = urlObject.pathname.split("/"); |
||||
if ( |
||||
pathItems.length < 2 || |
||||
(pathItems.length === 2 && pathItems[1].length === 0) |
||||
) { |
||||
return undefined; |
||||
} |
||||
if (pathItems[pathItems.length - 1] === "") { |
||||
pathItems.pop(); |
||||
} |
||||
pathItems.pop(); |
||||
urlObject.pathname = `${pathItems.join("/")}/`; |
||||
return urlObject.toString() as ContainerUri; |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* Gets the slug (last part of the path) for a given URI |
||||
* |
||||
* @param uri - the full URI |
||||
* @returns the slug of the URI |
||||
*/ |
||||
export function getSlug(uri: string): string { |
||||
const urlObject = new URL(uri); |
||||
const pathItems = urlObject.pathname.split("/"); |
||||
return pathItems[pathItems.length - 1] || pathItems[pathItems.length - 2]; |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* Deletes mention of a resource from the provided dataset |
||||
* |
||||
* @param resourceUri - the resource to delete |
||||
* @param dataset - dataset to modify |
||||
*/ |
||||
export function deleteResourceRdfFromContainer( |
||||
resourceUri: string, |
||||
dataset: Dataset, |
||||
) { |
||||
const parentUri = getParentUri(resourceUri); |
||||
if (parentUri) { |
||||
const parentNode = namedNode(parentUri); |
||||
const resourceNode = namedNode(resourceUri); |
||||
dataset.delete( |
||||
createQuad(parentNode, ldpContains, resourceNode, parentNode), |
||||
); |
||||
dataset.deleteMatches(resourceNode, undefined, undefined, parentNode); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* Adds a resource to a container in an RDF dataset |
||||
* |
||||
* @param resourceUri - the resource to add |
||||
* @param dataset - the dataset to modify |
||||
*/ |
||||
export function addResourceRdfToContainer( |
||||
resourceUri: string, |
||||
dataset: Dataset, |
||||
) { |
||||
const parentUri = getParentUri(resourceUri); |
||||
if (parentUri) { |
||||
const parentNode = namedNode(parentUri); |
||||
const resourceNode = namedNode(resourceUri); |
||||
dataset.add(createQuad(parentNode, ldpContains, resourceNode, parentNode)); |
||||
dataset.add(createQuad(resourceNode, rdfType, ldpResource, parentNode)); |
||||
if (isContainerUri(resourceUri)) { |
||||
dataset.add( |
||||
createQuad(resourceNode, rdfType, ldpBasicContainer, parentNode), |
||||
); |
||||
dataset.add(createQuad(resourceNode, rdfType, ldpContainer, parentNode)); |
||||
} |
||||
addResourceRdfToContainer(parentUri, dataset); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* Adds raw turtle to the provided dataset |
||||
* @param rawTurtle - String of raw turtle |
||||
* @param dataset - the dataset to modify |
||||
* @param baseUri - base URI to parsing turtle |
||||
* @returns Undefined if successful, noncompliantPodError if not |
||||
*/ |
||||
export async function addRawTurtleToDataset( |
||||
rawTurtle: string, |
||||
dataset: Dataset, |
||||
baseUri: string, |
||||
): Promise<undefined | NoncompliantPodError> { |
||||
const rawTurtleResult = await rawTurtleToDataset(rawTurtle, baseUri); |
||||
if (rawTurtleResult.isError) return rawTurtleResult; |
||||
const loadedDataset = rawTurtleResult.dataset; |
||||
const graphNode = namedNode(baseUri); |
||||
// Destroy all triples that were once a part of this resouce
|
||||
dataset.deleteMatches(undefined, undefined, undefined, graphNode); |
||||
// Add the triples from the fetched item
|
||||
dataset.addAll( |
||||
loadedDataset.map((quad) => |
||||
createQuad(quad.subject, quad.predicate, quad.object, graphNode), |
||||
), |
||||
); |
||||
} |
||||
|
||||
export async function rawTurtleToDataset( |
||||
rawTurtle: string, |
||||
baseUri: string, |
||||
): Promise<{ isError: false; dataset: LdoDataset } | NoncompliantPodError> { |
||||
try { |
||||
const loadedDataset = await parseRdf(rawTurtle, { |
||||
baseIRI: baseUri, |
||||
}); |
||||
return { isError: false, dataset: loadedDataset }; |
||||
} catch (err) { |
||||
const error = UnexpectedResourceError.fromThrown(baseUri, err); |
||||
return new NoncompliantPodError( |
||||
baseUri, |
||||
`Request returned noncompliant turtle: ${error.message}\n${rawTurtle}`, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
/** |
||||
* A list of modes that a certain agent has access to |
||||
*/ |
||||
export interface AccessModeList { |
||||
read: boolean; |
||||
append: boolean; |
||||
write: boolean; |
||||
control: boolean; |
||||
} |
||||
|
||||
/** |
||||
* A list of modes for each kind of agent |
||||
*/ |
||||
export interface WacRule { |
||||
public: AccessModeList; |
||||
authenticated: AccessModeList; |
||||
agent: Record<string, AccessModeList>; |
||||
} |
@ -0,0 +1,118 @@ |
||||
import type { GetWacRuleSuccess } from "./results/GetWacRuleSuccess"; |
||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||
import type { BasicRequestOptions } from "../../requester/requests/requestOptions"; |
||||
import type { HttpErrorResultType } from "../../requester/results/error/HttpErrorResult"; |
||||
import { HttpErrorResult } from "../../requester/results/error/HttpErrorResult"; |
||||
import type { NoncompliantPodError } from "../../requester/results/error/NoncompliantPodError"; |
||||
import type { UnexpectedResourceError } from "../../requester/results/error/ErrorResult"; |
||||
import { rawTurtleToDataset } from "../../util/rdfUtils"; |
||||
import { AuthorizationShapeType } from "../../.ldo/wac.shapeTypes"; |
||||
import type { AccessModeList, WacRule } from "./WacRule"; |
||||
import type { Authorization } from "../../.ldo/wac.typings"; |
||||
import type { WacRuleAbsent } from "./results/WacRuleAbsent"; |
||||
|
||||
export type GetWacRuleError = |
||||
| HttpErrorResultType |
||||
| NoncompliantPodError |
||||
| UnexpectedResourceError; |
||||
export type GetWacRuleResult = |
||||
| GetWacRuleSuccess |
||||
| GetWacRuleError |
||||
| WacRuleAbsent; |
||||
|
||||
/** |
||||
* Given the URI of an ACL document, return the Web Access Control (WAC) rules |
||||
* @param aclUri: The URI for the ACL document |
||||
* @param options: Options object to include an authenticated fetch function |
||||
* @returns GetWacRuleResult |
||||
*/ |
||||
export async function getWacRuleWithAclUri( |
||||
aclUri: string, |
||||
options?: BasicRequestOptions, |
||||
): Promise<GetWacRuleResult> { |
||||
const fetch = guaranteeFetch(options?.fetch); |
||||
const response = await fetch(aclUri); |
||||
const errorResult = HttpErrorResult.checkResponse(aclUri, response); |
||||
if (errorResult) return errorResult; |
||||
|
||||
if (response.status === 404) { |
||||
return { |
||||
type: "wacRuleAbsent", |
||||
uri: aclUri, |
||||
isError: false, |
||||
}; |
||||
} |
||||
|
||||
// Parse Turtle
|
||||
const rawTurtle = await response.text(); |
||||
const rawTurtleResult = await rawTurtleToDataset(rawTurtle, aclUri); |
||||
if (rawTurtleResult.isError) return rawTurtleResult; |
||||
const dataset = rawTurtleResult.dataset; |
||||
const authorizations = dataset |
||||
.usingType(AuthorizationShapeType) |
||||
.matchSubject( |
||||
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
||||
"http://www.w3.org/ns/auth/acl#Authorization", |
||||
); |
||||
|
||||
const wacRule: WacRule = { |
||||
public: { |
||||
read: false, |
||||
write: false, |
||||
append: false, |
||||
control: false, |
||||
}, |
||||
authenticated: { |
||||
read: false, |
||||
write: false, |
||||
append: false, |
||||
control: false, |
||||
}, |
||||
agent: {}, |
||||
}; |
||||
|
||||
function applyAccessModesToList( |
||||
accessModeList: AccessModeList, |
||||
authorization: Authorization, |
||||
): void { |
||||
authorization.mode?.forEach((mode) => { |
||||
accessModeList[mode["@id"].toLowerCase()] = true; |
||||
}); |
||||
} |
||||
|
||||
authorizations.forEach((authorization) => { |
||||
if ( |
||||
authorization.agentClass?.some( |
||||
(agentClass) => agentClass["@id"] === "Agent", |
||||
) |
||||
) { |
||||
applyAccessModesToList(wacRule.public, authorization); |
||||
applyAccessModesToList(wacRule.authenticated, authorization); |
||||
} |
||||
if ( |
||||
authorization.agentClass?.some( |
||||
(agentClass) => agentClass["@id"] === "AuthenticatedAgent", |
||||
) |
||||
) { |
||||
applyAccessModesToList(wacRule.authenticated, authorization); |
||||
} |
||||
authorization.agent?.forEach((agent) => { |
||||
if (!wacRule.agent[agent["@id"]]) { |
||||
wacRule.agent[agent["@id"]] = { |
||||
read: false, |
||||
write: false, |
||||
append: false, |
||||
control: false, |
||||
}; |
||||
} |
||||
applyAccessModesToList(wacRule.agent[agent["@id"]], authorization); |
||||
}); |
||||
}); |
||||
|
||||
return { |
||||
type: "getWacRuleSuccess", |
||||
uri: aclUri, |
||||
isError: false, |
||||
wacRule, |
||||
}; |
||||
} |
@ -0,0 +1,70 @@ |
||||
import type { GetWacUriSuccess } from "./results/GetWacUriSuccess"; |
||||
import type { HttpErrorResultType } from "../../requester/results/error/HttpErrorResult"; |
||||
import { |
||||
HttpErrorResult, |
||||
NotFoundHttpError, |
||||
} from "../../requester/results/error/HttpErrorResult"; |
||||
import { UnexpectedResourceError } from "../../requester/results/error/ErrorResult"; |
||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||
import type { BasicRequestOptions } from "../../requester/requests/requestOptions"; |
||||
import { NoncompliantPodError } from "../../requester/results/error/NoncompliantPodError"; |
||||
import { parse as parseLinkHeader } from "http-link-header"; |
||||
import type { LeafUri } from "../../util/uriTypes"; |
||||
|
||||
export type GetWacUriError = |
||||
| HttpErrorResultType |
||||
| NotFoundHttpError |
||||
| NoncompliantPodError |
||||
| UnexpectedResourceError; |
||||
export type GetWacUriResult = GetWacUriSuccess | GetWacUriError; |
||||
|
||||
/** |
||||
* Get the URI for the WAC rules of a specific resource |
||||
* @param resourceUri: the URI of the resource |
||||
* @param options: Options object to include an authenticated fetch function |
||||
* @returns GetWacUriResult |
||||
*/ |
||||
export async function getWacUri( |
||||
resourceUri: string, |
||||
options?: BasicRequestOptions, |
||||
): Promise<GetWacUriResult> { |
||||
try { |
||||
const fetch = guaranteeFetch(options?.fetch); |
||||
const response = await fetch(resourceUri, { |
||||
method: "head", |
||||
}); |
||||
const errorResult = HttpErrorResult.checkResponse(resourceUri, response); |
||||
if (errorResult) return errorResult; |
||||
if (NotFoundHttpError.is(response)) { |
||||
return new NotFoundHttpError( |
||||
resourceUri, |
||||
response, |
||||
"Could not get access control rules because the resource does not exist.", |
||||
); |
||||
} |
||||
// Get the URI from the link header
|
||||
const linkHeader = response.headers.get("link"); |
||||
if (!linkHeader) { |
||||
return new NoncompliantPodError( |
||||
resourceUri, |
||||
"No link header present in request.", |
||||
); |
||||
} |
||||
const parsedLinkHeader = parseLinkHeader(linkHeader); |
||||
const aclUris = parsedLinkHeader.get("rel", "acl"); |
||||
if (aclUris.length !== 1) { |
||||
return new NoncompliantPodError( |
||||
resourceUri, |
||||
`There must be one link with a rel="acl"`, |
||||
); |
||||
} |
||||
return { |
||||
type: "getWacUriSuccess", |
||||
isError: false, |
||||
uri: resourceUri, |
||||
wacUri: aclUris[0].uri as LeafUri, |
||||
}; |
||||
} catch (err: unknown) { |
||||
return UnexpectedResourceError.fromThrown(resourceUri, err); |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
||||
import type { WacRule } from "../WacRule"; |
||||
|
||||
/** |
||||
* Returned when a WAC rule is successfully retrieved |
||||
*/ |
||||
export interface GetWacRuleSuccess extends ResourceSuccess { |
||||
type: "getWacRuleSuccess"; |
||||
/** |
||||
* The rule that was retrieved |
||||
*/ |
||||
wacRule: WacRule; |
||||
} |
@ -0,0 +1,13 @@ |
||||
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
||||
import type { LeafUri } from "../../../util/uriTypes"; |
||||
|
||||
/** |
||||
* Returned when the URI for a resources ACL document was successfully retried |
||||
*/ |
||||
export interface GetWacUriSuccess extends ResourceSuccess { |
||||
type: "getWacUriSuccess"; |
||||
/** |
||||
* The URI of the ACL document |
||||
*/ |
||||
wacUri: LeafUri; |
||||
} |
@ -0,0 +1,13 @@ |
||||
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
||||
import type { WacRule } from "../WacRule"; |
||||
|
||||
/** |
||||
* Returned when rules were successfully written |
||||
*/ |
||||
export interface SetWacRuleSuccess extends ResourceSuccess { |
||||
type: "setWacRuleSuccess"; |
||||
/** |
||||
* The written rule |
||||
*/ |
||||
wacRule: WacRule; |
||||
} |
@ -0,0 +1,8 @@ |
||||
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; |
||||
|
||||
/** |
||||
* Returned if no WAC rule was returned from the server |
||||
*/ |
||||
export interface WacRuleAbsent extends ResourceSuccess { |
||||
type: "wacRuleAbsent"; |
||||
} |
@ -0,0 +1,108 @@ |
||||
import { createLdoDataset } from "@ldo/ldo"; |
||||
import type { BasicRequestOptions } from "../../requester/requests/requestOptions"; |
||||
import type { UnexpectedResourceError } from "../../requester/results/error/ErrorResult"; |
||||
import { |
||||
HttpErrorResult, |
||||
type HttpErrorResultType, |
||||
} from "../../requester/results/error/HttpErrorResult"; |
||||
import { isContainerUri, type LeafUri } from "../../util/uriTypes"; |
||||
import type { AccessModeList, WacRule } from "./WacRule"; |
||||
import type { SetWacRuleSuccess } from "./results/SetWacRuleSuccess"; |
||||
import type { Authorization } from "../../.ldo/wac.typings"; |
||||
import { AuthorizationShapeType } from "../../.ldo/wac.shapeTypes"; |
||||
import { v4 } from "uuid"; |
||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
||||
|
||||
export type SetWacRuleError = HttpErrorResultType | UnexpectedResourceError; |
||||
export type SetWacRuleResult = SetWacRuleSuccess | SetWacRuleError; |
||||
|
||||
/** |
||||
* Given the URI of an ACL document and some WAC rules, set the WAC rules of |
||||
* that document |
||||
* @param aclUri: The URI for the ACL document |
||||
* @param newRule: A new WAC rule to set. This will overwrite old rules |
||||
* @param accessTo: The document this rule refers to |
||||
* @param options: Options object to include an authenticated fetch function |
||||
* @returns SetWacRuleResult |
||||
*/ |
||||
export async function setWacRuleForAclUri( |
||||
aclUri: LeafUri, |
||||
newRule: WacRule, |
||||
accessTo: string, |
||||
options?: BasicRequestOptions, |
||||
): Promise<SetWacRuleResult> { |
||||
const fetch = guaranteeFetch(options?.fetch); |
||||
// The rule map keeps track of all the rules that are currently being used
|
||||
// so that similar rules can be grouped together
|
||||
const ruleMap: Record<string, Authorization> = {}; |
||||
// The dataset that will eventually be sent to the Pod
|
||||
const dataset = createLdoDataset(); |
||||
|
||||
// Helper function to add rules to the dataset by grouping them in the ruleMap
|
||||
function addRuleToDataset( |
||||
type: "public" | "authenticated" | "agent", |
||||
accessModeList: AccessModeList, |
||||
agentId?: string, |
||||
) { |
||||
const accessModeListHash = hashAccessModeList(accessModeList); |
||||
// No need to add if all access is false
|
||||
if (accessModeListHash === "") return; |
||||
if (!ruleMap[accessModeListHash]) { |
||||
const authorization = dataset |
||||
.usingType(AuthorizationShapeType) |
||||
.fromSubject(`${aclUri}#${v4()}`); |
||||
authorization.type = { "@id": "Authorization" }; |
||||
if (accessModeList.read) authorization.mode?.add({ "@id": "Read" }); |
||||
if (accessModeList.write) authorization.mode?.add({ "@id": "Write" }); |
||||
if (accessModeList.append) authorization.mode?.add({ "@id": "Append" }); |
||||
if (accessModeList.control) authorization.mode?.add({ "@id": "Control" }); |
||||
authorization.accessTo = { "@id": accessTo }; |
||||
if (isContainerUri(accessTo)) { |
||||
authorization.default = { "@id": accessTo }; |
||||
} |
||||
ruleMap[accessModeListHash] = authorization; |
||||
} |
||||
const authorization = ruleMap[accessModeListHash]; |
||||
// Add agents to the rule
|
||||
if (type === "public") { |
||||
authorization.agentClass?.add({ "@id": "Agent" }); |
||||
} else if (type === "authenticated") { |
||||
authorization.agentClass?.add({ "@id": "AuthenticatedAgent" }); |
||||
} else if (type === "agent" && agentId) { |
||||
authorization.agent?.add({ "@id": agentId }); |
||||
} |
||||
} |
||||
|
||||
// Add each rule to the dataset
|
||||
addRuleToDataset("public", newRule.public); |
||||
addRuleToDataset("authenticated", newRule.authenticated); |
||||
Object.entries(newRule.agent).forEach(([agentUri, accessModeList]) => { |
||||
addRuleToDataset("agent", accessModeList, agentUri); |
||||
}); |
||||
|
||||
// Save to Pod
|
||||
const response = await fetch(aclUri, { |
||||
method: "PUT", |
||||
headers: { |
||||
"content-type": "text/turtle", |
||||
}, |
||||
body: dataset.toString(), |
||||
}); |
||||
const errorResult = HttpErrorResult.checkResponse(aclUri, response); |
||||
if (errorResult) return errorResult; |
||||
|
||||
return { |
||||
type: "setWacRuleSuccess", |
||||
uri: aclUri, |
||||
isError: false, |
||||
wacRule: newRule, |
||||
}; |
||||
} |
||||
|
||||
// Hashes the access mode list for use in the rule map
|
||||
function hashAccessModeList(list: AccessModeList): string { |
||||
return Object.entries(list).reduce( |
||||
(agg, [key, isPresent]) => (isPresent ? agg + key : agg), |
||||
"", |
||||
); |
||||
} |
Loading…
Reference in new issue