parent
4e6e0355b7
commit
3b4efda881
@ -1,3 +0,0 @@ |
|||||||
{ |
|
||||||
"extends": ["../../.eslintrc"] |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
test/data |
|
@ -1,21 +0,0 @@ |
|||||||
MIT License |
|
||||||
|
|
||||||
Copyright (c) 2023 Jackson Morgan |
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
||||||
of this software and associated documentation files (the "Software"), to deal |
|
||||||
in the Software without restriction, including without limitation the rights |
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
||||||
copies of the Software, and to permit persons to whom the Software is |
|
||||||
furnished to do so, subject to the following conditions: |
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all |
|
||||||
copies or substantial portions of the Software. |
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
||||||
SOFTWARE. |
|
@ -1,238 +0,0 @@ |
|||||||
# @ldo/solid |
|
||||||
|
|
||||||
@ldo/solid is a client that implements the Solid specification with the use of Linked Data Objects. |
|
||||||
|
|
||||||
## Installation |
|
||||||
|
|
||||||
Navigate into your project's root folder and run the following command: |
|
||||||
``` |
|
||||||
cd my_project/ |
|
||||||
npx run @ldo/cli init |
|
||||||
``` |
|
||||||
|
|
||||||
Now install the @ldo/solid library |
|
||||||
|
|
||||||
``` |
|
||||||
npm i @ldo/solid |
|
||||||
``` |
|
||||||
|
|
||||||
<details> |
|
||||||
<summary> |
|
||||||
Manual Installation |
|
||||||
</summary> |
|
||||||
|
|
||||||
If you already have generated ShapeTypes, you may install the `@ldo/ldo` and `@ldo/solid` libraries independently. |
|
||||||
|
|
||||||
``` |
|
||||||
npm i @ldo/ldo @ldo/solid |
|
||||||
``` |
|
||||||
</details> |
|
||||||
|
|
||||||
## Simple Examples |
|
||||||
|
|
||||||
Below is a simple example of @ldo/solid. Assume that a ShapeType was previously generated and placed at `./.ldo/foafProfile.shapeTypes`. Also assume we have a shape type for social media at `./.ldo/socialMediaPost.shapeTypes` |
|
||||||
|
|
||||||
```typescript |
|
||||||
import { changeData, commitData, createSolidLdoDataset } from "@ldo/solid"; |
|
||||||
import { fetch, getDefaultSession } from "@inrupt/solid-client-authn-browser"; |
|
||||||
import { FoafProfileShapeType } from "./.ldo/foafProfile.shapeTypes"; |
|
||||||
import { SocialMediaPostShapeType } from "./.ldo/socialMediaPost.shapeTypes"; |
|
||||||
|
|
||||||
async function main() { |
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* READING DATA FROM A POD |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
// Before we begin using @ldo/solid. Let's get the WebId of the current user |
|
||||||
const webIdUri = getDefaultSession().info.webId; |
|
||||||
if (!webIdUri) throw new Error("User is not logged in"); |
|
||||||
|
|
||||||
// Now let's proceed with @ldo/solid. Our first step is setting up a |
|
||||||
// SolidLdoDataset. You can think of this dataset as a local store for all the |
|
||||||
// information in the Solidverse. Don't forget to pass the authenticated fetch |
|
||||||
// function to do your queries! |
|
||||||
const solidLdoDataset = createSolidLdoDataset({ fetch }); |
|
||||||
|
|
||||||
// We'll start with getting a representation of our WebId's resource |
|
||||||
const webIdResource = solidLdoDataset.getResource(webIdUri); |
|
||||||
|
|
||||||
// This resource is currently unfetched |
|
||||||
console.log(webIdResource.isUnfetched()); // Logs true |
|
||||||
|
|
||||||
// So let's fetch it! Running the `read` command will make a request to get |
|
||||||
// the WebId. |
|
||||||
const readResult = await webIdResource.read(); |
|
||||||
|
|
||||||
// @ldo/solid will never throw an error. Instead, it will return errors. This |
|
||||||
// design decision was made to force you to handle any errors. It may seem a |
|
||||||
// bit annoying at first, but it will result in more resiliant code. You can |
|
||||||
// easily follow intellisense tooltips to see what kinds of errors each action |
|
||||||
// can throw. |
|
||||||
if (readResult.isError) { |
|
||||||
switch (readResult.type) { |
|
||||||
case "serverError": |
|
||||||
console.error("The solid server had an error:", readResult.message); |
|
||||||
return; |
|
||||||
case "noncompliantPodError": |
|
||||||
console.error("The Pod responded in a way not compliant with the spec"); |
|
||||||
return; |
|
||||||
default: |
|
||||||
console.error("Some other error was detected:", readResult.message); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// When fetching a data resource, read triples will automatically be added to |
|
||||||
// the solidLdoDataset. You can access them using Linked Data Objects. In |
|
||||||
// the following example we're using a Profile Linked Data Object that was |
|
||||||
// generated with the init step. |
|
||||||
const profile = solidLdoDataset |
|
||||||
.usingType(FoafProfileShapeType) |
|
||||||
.fromSubject(webIdUri); |
|
||||||
|
|
||||||
// Now you can read "profile" like any JSON. |
|
||||||
console.log(profile.name); |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* MODIFYING DATA |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
// When we want to modify data the first step is to use the `changeData` |
|
||||||
// function. We pass in an object that we want to change (in this case, |
|
||||||
// "profile") as well an a list of any resources to which we want those |
|
||||||
// changes to be applied (in this case, just the webIdResource). This gives |
|
||||||
// us a new variable (conventionally named with a c for "changed") that we can |
|
||||||
// write changes to. |
|
||||||
const cProfile = changeData(profile, webIdResource); |
|
||||||
|
|
||||||
// We can make changes just like it's regular JSON |
|
||||||
cProfile.name = "Captain Cool Dude"; |
|
||||||
|
|
||||||
// Committing data is as easy as running the "commitData" function. |
|
||||||
const commitResult = await commitData(cProfile); |
|
||||||
|
|
||||||
// Remember to check for and handle errors! We'll keep it short this time. |
|
||||||
if (commitResult.isError) throw commitResult; |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* CREATING NEW RESOURCES |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
// Let's create some social media posts to be stored on the Solid Pod! |
|
||||||
// Our first step is going to be finding where to place these posts. In the |
|
||||||
// future, there will be advanced ways to determine the location of resources |
|
||||||
// but for now, let's throw it in the root folder. |
|
||||||
|
|
||||||
// But, first, let's find out where the root folder is. We can take our WebId |
|
||||||
// resource and call `getRootContainer`. Let's assume the root container has |
|
||||||
// a URI "https://example.com/" |
|
||||||
const rootContainer = await webIdResource.getRootContainer(); |
|
||||||
if (rootContainer.isError) throw rootContainer; |
|
||||||
|
|
||||||
// Now, let's create a container for our posts |
|
||||||
const createPostContainerResult = |
|
||||||
await rootContainer.createChildIfAbsent("social-posts/"); |
|
||||||
if (createPostContainerResult.isError) throw createPostContainerResult; |
|
||||||
|
|
||||||
// Most results store the affected resource in the "resource" field. This |
|
||||||
// container has the URI "https://example.com/social-posts/" |
|
||||||
const postContainer = createPostContainerResult.resource; |
|
||||||
|
|
||||||
// Now that we have our container, let's make a Post resource! This is a data |
|
||||||
// resource, which means we can put raw Solid Data (RDF) into it. |
|
||||||
const postResourceResult = |
|
||||||
await postContainer.createChildAndOverwrite("post1.ttl"); |
|
||||||
if (postResourceResult.isError) throw postResourceResult; |
|
||||||
const postResource = postResourceResult.resource; |
|
||||||
|
|
||||||
// We can also create binary resources with things like images |
|
||||||
const imageResourceResult = await postContainer.uploadChildAndOverwrite( |
|
||||||
// name of the binary |
|
||||||
"image1.svg", |
|
||||||
// A blob for the binary |
|
||||||
new Blob([`<svg><circle r="9" /></svg>`]), |
|
||||||
// mime type of the binary |
|
||||||
"image/svg+xml", |
|
||||||
); |
|
||||||
if (imageResourceResult.isError) throw imageResourceResult; |
|
||||||
const imageResource = imageResourceResult.resource; |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* CREATING NEW DATA |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
// We create data in a similar way to the way we modify data. We can use the |
|
||||||
// "createData" method. |
|
||||||
const cPost = solidLdoDataset.createData( |
|
||||||
// An LDO ShapeType saying that this is a social media psot |
|
||||||
SocialMediaPostShapeType, |
|
||||||
// The URI of the post (in this case we'll make it the same as the resource) |
|
||||||
postResource.uri, |
|
||||||
// The resource we should write it to |
|
||||||
postResource, |
|
||||||
); |
|
||||||
|
|
||||||
// We can add new data |
|
||||||
cPost.text = "Check out this bad svg:"; |
|
||||||
cPost.image = { "@id": imageResource.uri }; |
|
||||||
|
|
||||||
// And now we commit data |
|
||||||
const newDataResult = await commitData(cPost); |
|
||||||
if (newDataResult.isError) throw newDataResult; |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* DELETING RESOURCES |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
// Deleting resources can be done with a single method call. In this case, |
|
||||||
// the container will be deleted along with all its contained resources |
|
||||||
const deleteResult = await postContainer.delete(); |
|
||||||
if (deleteResult.isError) throw deleteResult; |
|
||||||
} |
|
||||||
main(); |
|
||||||
``` |
|
||||||
|
|
||||||
## API Details |
|
||||||
|
|
||||||
SolidLdoDataset |
|
||||||
|
|
||||||
- [createSolidLdoDataset](https://ldo.js.org/latest/api/solid/functions/createSolidLdoDataset/) |
|
||||||
- [SolidLdoDataset](https://ldo.js.org/latest/api/solid/classes/SolidLdoDataset/) |
|
||||||
|
|
||||||
Resources (Manage batching requests) |
|
||||||
|
|
||||||
- [LeafUri](https://ldo.js.org/latest/api/solid/types/LeafUri/) |
|
||||||
- [ContainerUri](https://ldo.js.org/latest/api/solid/types/ContainerUri/) |
|
||||||
- [Leaf](https://ldo.js.org/latest/api/solid/classes/Leaf/) |
|
||||||
- [Container](https://ldo.js.org/latest/api/solid/classes/Container/) |
|
||||||
|
|
||||||
Standalone Functions |
|
||||||
|
|
||||||
- [checkRootContainter](https://ldo.js.org/latest/api/solid/functions/checkRootContainer/) |
|
||||||
- [createDataResource](https://ldo.js.org/latest/api/solid/functions/createDataResource/) |
|
||||||
- [deleteResource](https://ldo.js.org/latest/api/solid/functions/deleteResource/) |
|
||||||
- [readResource](https://ldo.js.org/latest/api/solid/functions/readResource/) |
|
||||||
- [updateResource](https://ldo.js.org/latest/api/solid/functions/updateResource/) |
|
||||||
- [uploadResource](https://ldo.js.org/latest/api/solid/functions/uploadResource/) |
|
||||||
|
|
||||||
Data Functions |
|
||||||
- [changeData](https://ldo.js.org/latest/api/solid/functions/changeData/) |
|
||||||
- [commitData](https://ldo.js.org/latest/api/solid/functions/commitData/) |
|
||||||
|
|
||||||
## Sponsorship |
|
||||||
This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/). |
|
||||||
|
|
||||||
[<img src="https://nlnet.nl/logo/banner.png" alt="nlnet foundation logo" width="300" />](https://nlnet.nl/) |
|
||||||
[<img src="https://nlnet.nl/image/logos/NGI0Entrust_tag.svg" alt="NGI Zero Entrust Logo" width="300" />](https://nlnet.nl/) |
|
||||||
|
|
||||||
## Liscense |
|
||||||
MIT |
|
@ -1 +0,0 @@ |
|||||||
module.exports = { presets: ["@babel/preset-env"] }; |
|
@ -1,11 +0,0 @@ |
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const sharedConfig = require("../../jest.config.js"); |
|
||||||
module.exports = { |
|
||||||
...sharedConfig, |
|
||||||
rootDir: "./", |
|
||||||
setupFiles: ["<rootDir>/test/setup-tests.ts"], |
|
||||||
transform: { |
|
||||||
"^.+\\.(ts|tsx)?$": "ts-jest", |
|
||||||
"^.+\\.(js|jsx)$": "babel-jest", |
|
||||||
}, |
|
||||||
}; |
|
@ -1,61 +0,0 @@ |
|||||||
{ |
|
||||||
"name": "@ldo/solid", |
|
||||||
"version": "1.0.0-alpha.1", |
|
||||||
"description": "A library for LDO and Solid", |
|
||||||
"main": "dist/index.js", |
|
||||||
"scripts": { |
|
||||||
"example": "ts-node ./example/example.ts", |
|
||||||
"build": "tsc --project tsconfig.build.json", |
|
||||||
"watch": "tsc --watch", |
|
||||||
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage", |
|
||||||
"test:watch": "jest --watch", |
|
||||||
"prepublishOnly": "npm run test && npm run build", |
|
||||||
"build:ldo": "ldo build --input src/.shapes --output src/.ldo", |
|
||||||
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern", |
|
||||||
"docs": "typedoc --plugin typedoc-plugin-markdown" |
|
||||||
}, |
|
||||||
"repository": { |
|
||||||
"type": "git", |
|
||||||
"url": "git+https://github.com/o-development/ldobjects.git" |
|
||||||
}, |
|
||||||
"author": "Jackson Morgan", |
|
||||||
"license": "MIT", |
|
||||||
"bugs": { |
|
||||||
"url": "https://github.com/o-development/ldobjects/issues" |
|
||||||
}, |
|
||||||
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid#readme", |
|
||||||
"devDependencies": { |
|
||||||
"@inrupt/solid-client-authn-core": "^2.2.6", |
|
||||||
"@ldo/cli": "^1.0.0-alpha.1", |
|
||||||
"@rdfjs/data-model": "^1.2.0", |
|
||||||
"@rdfjs/types": "^1.0.1", |
|
||||||
"@solid-notifications/types": "^0.1.2", |
|
||||||
"@solid/community-server": "^7.1.3", |
|
||||||
"@types/jest": "^27.0.3", |
|
||||||
"cross-env": "^7.0.3", |
|
||||||
"dotenv": "^16.3.1", |
|
||||||
"jest-rdf": "^1.8.0", |
|
||||||
"ts-jest": "^27.1.2", |
|
||||||
"ts-node": "^10.9.1", |
|
||||||
"typed-emitter": "^2.1.0", |
|
||||||
"typedoc": "^0.25.4", |
|
||||||
"typedoc-plugin-markdown": "^3.17.1" |
|
||||||
}, |
|
||||||
"dependencies": { |
|
||||||
"@ldo/dataset": "^1.0.0-alpha.1", |
|
||||||
"@ldo/ldo": "^1.0.0-alpha.1", |
|
||||||
"@ldo/rdf-utils": "^1.0.0-alpha.1", |
|
||||||
"@solid-notifications/subscription": "^0.1.2", |
|
||||||
"cross-fetch": "^3.1.6", |
|
||||||
"http-link-header": "^1.1.1", |
|
||||||
"ws": "^8.18.0" |
|
||||||
}, |
|
||||||
"files": [ |
|
||||||
"dist", |
|
||||||
"src" |
|
||||||
], |
|
||||||
"publishConfig": { |
|
||||||
"access": "public" |
|
||||||
}, |
|
||||||
"gitHead": "0287cd6371f06630763568dec5e41653f7b8583e" |
|
||||||
} |
|
@ -1,108 +0,0 @@ |
|||||||
import { LdoJsonldContext } from "@ldo/ldo"; |
|
||||||
|
|
||||||
/** |
|
||||||
* ============================================================================= |
|
||||||
* solidContext: JSONLD Context for solid |
|
||||||
* ============================================================================= |
|
||||||
*/ |
|
||||||
export const solidContext: LdoJsonldContext = { |
|
||||||
type: { |
|
||||||
"@id": "@type", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
Container: { |
|
||||||
"@id": "http://www.w3.org/ns/ldp#Container", |
|
||||||
"@context": { |
|
||||||
type: { |
|
||||||
"@id": "@type", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
modified: { |
|
||||||
"@id": "http://purl.org/dc/terms/modified", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#string", |
|
||||||
}, |
|
||||||
contains: { |
|
||||||
"@id": "http://www.w3.org/ns/ldp#contains", |
|
||||||
"@type": "@id", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
mtime: { |
|
||||||
"@id": "http://www.w3.org/ns/posix/stat#mtime", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#decimal", |
|
||||||
}, |
|
||||||
size: { |
|
||||||
"@id": "http://www.w3.org/ns/posix/stat#size", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#integer", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
Resource: { |
|
||||||
"@id": "http://www.w3.org/ns/ldp#Resource", |
|
||||||
"@context": { |
|
||||||
type: { |
|
||||||
"@id": "@type", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
modified: { |
|
||||||
"@id": "http://purl.org/dc/terms/modified", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#string", |
|
||||||
}, |
|
||||||
contains: { |
|
||||||
"@id": "http://www.w3.org/ns/ldp#contains", |
|
||||||
"@type": "@id", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
mtime: { |
|
||||||
"@id": "http://www.w3.org/ns/posix/stat#mtime", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#decimal", |
|
||||||
}, |
|
||||||
size: { |
|
||||||
"@id": "http://www.w3.org/ns/posix/stat#size", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#integer", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
modified: { |
|
||||||
"@id": "http://purl.org/dc/terms/modified", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#string", |
|
||||||
}, |
|
||||||
contains: { |
|
||||||
"@id": "http://www.w3.org/ns/ldp#contains", |
|
||||||
"@type": "@id", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
Resource2: { |
|
||||||
"@id": "http://www.w3.org/ns/iana/media-types/text/turtle#Resource", |
|
||||||
"@context": { |
|
||||||
type: { |
|
||||||
"@id": "@type", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
modified: { |
|
||||||
"@id": "http://purl.org/dc/terms/modified", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#string", |
|
||||||
}, |
|
||||||
mtime: { |
|
||||||
"@id": "http://www.w3.org/ns/posix/stat#mtime", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#decimal", |
|
||||||
}, |
|
||||||
size: { |
|
||||||
"@id": "http://www.w3.org/ns/posix/stat#size", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#integer", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
mtime: { |
|
||||||
"@id": "http://www.w3.org/ns/posix/stat#mtime", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#decimal", |
|
||||||
}, |
|
||||||
size: { |
|
||||||
"@id": "http://www.w3.org/ns/posix/stat#size", |
|
||||||
"@type": "http://www.w3.org/2001/XMLSchema#integer", |
|
||||||
}, |
|
||||||
storage: { |
|
||||||
"@id": "http://www.w3.org/ns/pim/space#storage", |
|
||||||
"@type": "@id", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
}; |
|
@ -1,233 +0,0 @@ |
|||||||
import { Schema } from "shexj"; |
|
||||||
|
|
||||||
/** |
|
||||||
* ============================================================================= |
|
||||||
* solidSchema: ShexJ Schema for solid |
|
||||||
* ============================================================================= |
|
||||||
*/ |
|
||||||
export const solidSchema: Schema = { |
|
||||||
type: "Schema", |
|
||||||
shapes: [ |
|
||||||
{ |
|
||||||
id: "http://www.w3.org/ns/lddps#Container", |
|
||||||
type: "ShapeDecl", |
|
||||||
shapeExpr: { |
|
||||||
type: "Shape", |
|
||||||
expression: { |
|
||||||
id: "http://www.w3.org/ns/lddps#ContainerShape", |
|
||||||
type: "EachOf", |
|
||||||
expressions: [ |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
values: [ |
|
||||||
"http://www.w3.org/ns/ldp#Container", |
|
||||||
"http://www.w3.org/ns/ldp#Resource", |
|
||||||
], |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: -1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "A container on a Solid server", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://purl.org/dc/terms/modified", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
datatype: "http://www.w3.org/2001/XMLSchema#string", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: 1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "Date modified", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/ldp#contains", |
|
||||||
valueExpr: "http://www.w3.org/ns/lddps#Resource", |
|
||||||
min: 0, |
|
||||||
max: -1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "Defines a Solid Resource", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/posix/stat#mtime", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
datatype: "http://www.w3.org/2001/XMLSchema#decimal", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: 1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "?", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/posix/stat#size", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
datatype: "http://www.w3.org/2001/XMLSchema#integer", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: 1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "size of this container", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"], |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: "http://www.w3.org/ns/lddps#Resource", |
|
||||||
type: "ShapeDecl", |
|
||||||
shapeExpr: { |
|
||||||
type: "Shape", |
|
||||||
expression: { |
|
||||||
id: "http://www.w3.org/ns/lddps#ResourceShape", |
|
||||||
type: "EachOf", |
|
||||||
expressions: [ |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
values: [ |
|
||||||
"http://www.w3.org/ns/ldp#Resource", |
|
||||||
"http://www.w3.org/ns/iana/media-types/text/turtle#Resource", |
|
||||||
], |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: -1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "Any resource on a Solid server", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://purl.org/dc/terms/modified", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
datatype: "http://www.w3.org/2001/XMLSchema#string", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: 1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "Date modified", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/posix/stat#mtime", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
datatype: "http://www.w3.org/2001/XMLSchema#decimal", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: 1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "?", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/posix/stat#size", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
datatype: "http://www.w3.org/2001/XMLSchema#integer", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: 1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "size of this container", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"], |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: "http://www.w3.org/ns/lddps#ProfileWithStorage", |
|
||||||
type: "ShapeDecl", |
|
||||||
shapeExpr: { |
|
||||||
type: "Shape", |
|
||||||
expression: { |
|
||||||
id: "http://www.w3.org/ns/lddps#ProfileWithStorageShape", |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/pim/space#storage", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
nodeKind: "iri", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: -1, |
|
||||||
}, |
|
||||||
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"], |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}; |
|
@ -1,37 +0,0 @@ |
|||||||
import { ShapeType } from "@ldo/ldo"; |
|
||||||
import { solidSchema } from "./solid.schema"; |
|
||||||
import { solidContext } from "./solid.context"; |
|
||||||
import { Container, Resource, ProfileWithStorage } from "./solid.typings"; |
|
||||||
|
|
||||||
/** |
|
||||||
* ============================================================================= |
|
||||||
* LDO ShapeTypes solid |
|
||||||
* ============================================================================= |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Container ShapeType |
|
||||||
*/ |
|
||||||
export const ContainerShapeType: ShapeType<Container> = { |
|
||||||
schema: solidSchema, |
|
||||||
shape: "http://www.w3.org/ns/lddps#Container", |
|
||||||
context: solidContext, |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Resource ShapeType |
|
||||||
*/ |
|
||||||
export const ResourceShapeType: ShapeType<Resource> = { |
|
||||||
schema: solidSchema, |
|
||||||
shape: "http://www.w3.org/ns/lddps#Resource", |
|
||||||
context: solidContext, |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* ProfileWithStorage ShapeType |
|
||||||
*/ |
|
||||||
export const ProfileWithStorageShapeType: ShapeType<ProfileWithStorage> = { |
|
||||||
schema: solidSchema, |
|
||||||
shape: "http://www.w3.org/ns/lddps#ProfileWithStorage", |
|
||||||
context: solidContext, |
|
||||||
}; |
|
@ -1,84 +0,0 @@ |
|||||||
import { LdoJsonldContext, LdSet } from "@ldo/ldo"; |
|
||||||
|
|
||||||
/** |
|
||||||
* ============================================================================= |
|
||||||
* Typescript Typings for solid |
|
||||||
* ============================================================================= |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Container Type |
|
||||||
*/ |
|
||||||
export interface Container { |
|
||||||
"@id"?: string; |
|
||||||
"@context"?: LdoJsonldContext; |
|
||||||
/** |
|
||||||
* A container on a Solid server |
|
||||||
*/ |
|
||||||
type?: LdSet< |
|
||||||
| { |
|
||||||
"@id": "Container"; |
|
||||||
} |
|
||||||
| { |
|
||||||
"@id": "Resource"; |
|
||||||
} |
|
||||||
>; |
|
||||||
/** |
|
||||||
* Date modified |
|
||||||
*/ |
|
||||||
modified?: string; |
|
||||||
/** |
|
||||||
* Defines a Solid Resource |
|
||||||
*/ |
|
||||||
contains?: LdSet<Resource>; |
|
||||||
/** |
|
||||||
* ? |
|
||||||
*/ |
|
||||||
mtime?: number; |
|
||||||
/** |
|
||||||
* size of this container |
|
||||||
*/ |
|
||||||
size?: number; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Resource Type |
|
||||||
*/ |
|
||||||
export interface Resource { |
|
||||||
"@id"?: string; |
|
||||||
"@context"?: LdoJsonldContext; |
|
||||||
/** |
|
||||||
* Any resource on a Solid server |
|
||||||
*/ |
|
||||||
type?: LdSet< |
|
||||||
| { |
|
||||||
"@id": "Resource"; |
|
||||||
} |
|
||||||
| { |
|
||||||
"@id": "Resource2"; |
|
||||||
} |
|
||||||
>; |
|
||||||
/** |
|
||||||
* Date modified |
|
||||||
*/ |
|
||||||
modified?: string; |
|
||||||
/** |
|
||||||
* ? |
|
||||||
*/ |
|
||||||
mtime?: number; |
|
||||||
/** |
|
||||||
* size of this container |
|
||||||
*/ |
|
||||||
size?: number; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* ProfileWithStorage Type |
|
||||||
*/ |
|
||||||
export interface ProfileWithStorage { |
|
||||||
"@id"?: string; |
|
||||||
"@context"?: LdoJsonldContext; |
|
||||||
storage?: LdSet<{ |
|
||||||
"@id": string; |
|
||||||
}>; |
|
||||||
} |
|
@ -1,78 +0,0 @@ |
|||||||
import { LdoJsonldContext } from "@ldo/ldo"; |
|
||||||
|
|
||||||
/** |
|
||||||
* ============================================================================= |
|
||||||
* wacContext: JSONLD Context for wac |
|
||||||
* ============================================================================= |
|
||||||
*/ |
|
||||||
export const wacContext: LdoJsonldContext = { |
|
||||||
type: { |
|
||||||
"@id": "@type", |
|
||||||
}, |
|
||||||
Authorization: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#Authorization", |
|
||||||
"@context": { |
|
||||||
type: { |
|
||||||
"@id": "@type", |
|
||||||
}, |
|
||||||
accessTo: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#accessTo", |
|
||||||
"@type": "@id", |
|
||||||
}, |
|
||||||
default: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#default", |
|
||||||
"@type": "@id", |
|
||||||
}, |
|
||||||
agent: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#agent", |
|
||||||
"@type": "@id", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
agentGroup: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#agentGroup", |
|
||||||
"@type": "@id", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
agentClass: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#agentClass", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
mode: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#mode", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
accessTo: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#accessTo", |
|
||||||
"@type": "@id", |
|
||||||
}, |
|
||||||
default: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#default", |
|
||||||
"@type": "@id", |
|
||||||
}, |
|
||||||
agent: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#agent", |
|
||||||
"@type": "@id", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
agentGroup: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#agentGroup", |
|
||||||
"@type": "@id", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
agentClass: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#agentClass", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
AuthenticatedAgent: "http://www.w3.org/ns/auth/acl#AuthenticatedAgent", |
|
||||||
Agent: "http://xmlns.com/foaf/0.1/Agent", |
|
||||||
mode: { |
|
||||||
"@id": "http://www.w3.org/ns/auth/acl#mode", |
|
||||||
"@isCollection": true, |
|
||||||
}, |
|
||||||
Read: "http://www.w3.org/ns/auth/acl#Read", |
|
||||||
Write: "http://www.w3.org/ns/auth/acl#Write", |
|
||||||
Append: "http://www.w3.org/ns/auth/acl#Append", |
|
||||||
Control: "http://www.w3.org/ns/auth/acl#Control", |
|
||||||
}; |
|
@ -1,169 +0,0 @@ |
|||||||
import { Schema } from "shexj"; |
|
||||||
|
|
||||||
/** |
|
||||||
* ============================================================================= |
|
||||||
* wacSchema: ShexJ Schema for wac |
|
||||||
* ============================================================================= |
|
||||||
*/ |
|
||||||
export const wacSchema: Schema = { |
|
||||||
type: "Schema", |
|
||||||
shapes: [ |
|
||||||
{ |
|
||||||
id: "http://www.w3.org/ns/auth/acls#Authorization", |
|
||||||
type: "ShapeDecl", |
|
||||||
shapeExpr: { |
|
||||||
type: "Shape", |
|
||||||
expression: { |
|
||||||
id: "http://www.w3.org/ns/auth/acls#AuthorizationShape", |
|
||||||
type: "EachOf", |
|
||||||
expressions: [ |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
values: ["http://www.w3.org/ns/auth/acl#Authorization"], |
|
||||||
}, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "Denotes this as an acl:Authorization", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/auth/acl#accessTo", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
nodeKind: "iri", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: 1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "The subject of this authorization", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/auth/acl#default", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
nodeKind: "iri", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: 1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: "The container subject of this authorization", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/auth/acl#agent", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
nodeKind: "iri", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: -1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: |
|
||||||
"An agent is a person, social entity or software identified by a URI, e.g., a WebID denotes an agent", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/auth/acl#agentGroup", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
nodeKind: "iri", |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: -1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: |
|
||||||
"Denotes a group of agents being given the access permission", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/auth/acl#agentClass", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
values: [ |
|
||||||
"http://www.w3.org/ns/auth/acl#AuthenticatedAgent", |
|
||||||
"http://xmlns.com/foaf/0.1/Agent", |
|
||||||
], |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: -1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: |
|
||||||
"An agent class is a class of persons or entities identified by a URI.", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: "TripleConstraint", |
|
||||||
predicate: "http://www.w3.org/ns/auth/acl#mode", |
|
||||||
valueExpr: { |
|
||||||
type: "NodeConstraint", |
|
||||||
values: [ |
|
||||||
"http://www.w3.org/ns/auth/acl#Read", |
|
||||||
"http://www.w3.org/ns/auth/acl#Write", |
|
||||||
"http://www.w3.org/ns/auth/acl#Append", |
|
||||||
"http://www.w3.org/ns/auth/acl#Control", |
|
||||||
], |
|
||||||
}, |
|
||||||
min: 0, |
|
||||||
max: -1, |
|
||||||
annotations: [ |
|
||||||
{ |
|
||||||
type: "Annotation", |
|
||||||
predicate: "http://www.w3.org/2000/01/rdf-schema#comment", |
|
||||||
object: { |
|
||||||
value: |
|
||||||
"Denotes a class of operations that the agents can perform on a resource.", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"], |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}; |
|
@ -1,19 +0,0 @@ |
|||||||
import { ShapeType } from "@ldo/ldo"; |
|
||||||
import { wacSchema } from "./wac.schema"; |
|
||||||
import { wacContext } from "./wac.context"; |
|
||||||
import { Authorization } from "./wac.typings"; |
|
||||||
|
|
||||||
/** |
|
||||||
* ============================================================================= |
|
||||||
* LDO ShapeTypes wac |
|
||||||
* ============================================================================= |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Authorization ShapeType |
|
||||||
*/ |
|
||||||
export const AuthorizationShapeType: ShapeType<Authorization> = { |
|
||||||
schema: wacSchema, |
|
||||||
shape: "http://www.w3.org/ns/auth/acls#Authorization", |
|
||||||
context: wacContext, |
|
||||||
}; |
|
@ -1,73 +0,0 @@ |
|||||||
import { LdoJsonldContext, LdSet } from "@ldo/ldo"; |
|
||||||
|
|
||||||
/** |
|
||||||
* ============================================================================= |
|
||||||
* Typescript Typings for wac |
|
||||||
* ============================================================================= |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Authorization Type |
|
||||||
*/ |
|
||||||
export interface Authorization { |
|
||||||
"@id"?: string; |
|
||||||
"@context"?: LdoJsonldContext; |
|
||||||
/** |
|
||||||
* Denotes this as an acl:Authorization |
|
||||||
*/ |
|
||||||
type: { |
|
||||||
"@id": "Authorization"; |
|
||||||
}; |
|
||||||
/** |
|
||||||
* The subject of this authorization |
|
||||||
*/ |
|
||||||
accessTo?: { |
|
||||||
"@id": string; |
|
||||||
}; |
|
||||||
/** |
|
||||||
* The container subject of this authorization |
|
||||||
*/ |
|
||||||
default?: { |
|
||||||
"@id": string; |
|
||||||
}; |
|
||||||
/** |
|
||||||
* An agent is a person, social entity or software identified by a URI, e.g., a WebID denotes an agent |
|
||||||
*/ |
|
||||||
agent?: LdSet<{ |
|
||||||
"@id": string; |
|
||||||
}>; |
|
||||||
/** |
|
||||||
* Denotes a group of agents being given the access permission |
|
||||||
*/ |
|
||||||
agentGroup?: LdSet<{ |
|
||||||
"@id": string; |
|
||||||
}>; |
|
||||||
/** |
|
||||||
* An agent class is a class of persons or entities identified by a URI. |
|
||||||
*/ |
|
||||||
agentClass?: LdSet< |
|
||||||
| { |
|
||||||
"@id": "AuthenticatedAgent"; |
|
||||||
} |
|
||||||
| { |
|
||||||
"@id": "Agent"; |
|
||||||
} |
|
||||||
>; |
|
||||||
/** |
|
||||||
* Denotes a class of operations that the agents can perform on a resource. |
|
||||||
*/ |
|
||||||
mode?: LdSet< |
|
||||||
| { |
|
||||||
"@id": "Read"; |
|
||||||
} |
|
||||||
| { |
|
||||||
"@id": "Write"; |
|
||||||
} |
|
||||||
| { |
|
||||||
"@id": "Append"; |
|
||||||
} |
|
||||||
| { |
|
||||||
"@id": "Control"; |
|
||||||
} |
|
||||||
>; |
|
||||||
} |
|
@ -1,43 +0,0 @@ |
|||||||
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> |
|
||||||
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> |
|
||||||
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> |
|
||||||
PREFIX ldp: <http://www.w3.org/ns/ldp#> |
|
||||||
PREFIX ldps: <http://www.w3.org/ns/lddps#> |
|
||||||
PREFIX dct: <http://purl.org/dc/terms/> |
|
||||||
PREFIX stat: <http://www.w3.org/ns/posix/stat#> |
|
||||||
PREFIX tur: <http://www.w3.org/ns/iana/media-types/text/turtle#> |
|
||||||
PREFIX pim: <http://www.w3.org/ns/pim/space#> |
|
||||||
|
|
||||||
ldps:Container EXTRA a { |
|
||||||
$ldps:ContainerShape ( |
|
||||||
a [ ldp:Container ldp:Resource ]* |
|
||||||
// rdfs:comment "A container on a Solid server"; |
|
||||||
dct:modified xsd:string? |
|
||||||
// rdfs:comment "Date modified"; |
|
||||||
ldp:contains @ldps:Resource* |
|
||||||
// rdfs:comment "Defines a Solid Resource"; |
|
||||||
stat:mtime xsd:decimal? |
|
||||||
// rdfs:comment "?"; |
|
||||||
stat:size xsd:integer? |
|
||||||
// rdfs:comment "size of this container"; |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
ldps:Resource EXTRA a { |
|
||||||
$ldps:ResourceShape ( |
|
||||||
a [ ldp:Resource tur:Resource ]* |
|
||||||
// rdfs:comment "Any resource on a Solid server"; |
|
||||||
dct:modified xsd:string? |
|
||||||
// rdfs:comment "Date modified"; |
|
||||||
stat:mtime xsd:decimal? |
|
||||||
// rdfs:comment "?"; |
|
||||||
stat:size xsd:integer? |
|
||||||
// rdfs:comment "size of this container"; |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
ldps:ProfileWithStorage EXTRA a { |
|
||||||
$ldps:ProfileWithStorageShape ( |
|
||||||
pim:storage IRI *; |
|
||||||
) |
|
||||||
} |
|
@ -1,23 +0,0 @@ |
|||||||
PREFIX acl: <http://www.w3.org/ns/auth/acl#> |
|
||||||
PREFIX acls: <http://www.w3.org/ns/auth/acls#> |
|
||||||
PREFIX foaf: <http://xmlns.com/foaf/0.1/> |
|
||||||
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> |
|
||||||
|
|
||||||
acls:Authorization EXTRA a { |
|
||||||
$acls:AuthorizationShape ( |
|
||||||
a [ acl:Authorization ] |
|
||||||
// rdfs:comment "Denotes this as an acl:Authorization"; |
|
||||||
acl:accessTo IRI? |
|
||||||
// rdfs:comment "The subject of this authorization"; |
|
||||||
acl:default IRI? |
|
||||||
// rdfs:comment "The container subject of this authorization"; |
|
||||||
acl:agent IRI* |
|
||||||
// rdfs:comment "An agent is a person, social entity or software identified by a URI, e.g., a WebID denotes an agent"; |
|
||||||
acl:agentGroup IRI* |
|
||||||
// rdfs:comment "Denotes a group of agents being given the access permission"; |
|
||||||
acl:agentClass [ acl:AuthenticatedAgent foaf:Agent ]* |
|
||||||
// rdfs:comment "An agent class is a class of persons or entities identified by a URI."; |
|
||||||
acl:mode [ acl:Read acl:Write acl:Append acl:Control ]* |
|
||||||
// rdfs:comment "Denotes a class of operations that the agents can perform on a resource."; |
|
||||||
) |
|
||||||
} |
|
@ -1,79 +0,0 @@ |
|||||||
import { Container } from "./resource/Container"; |
|
||||||
import { Leaf } from "./resource/Leaf"; |
|
||||||
import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; |
|
||||||
import type { ContainerUri, LeafUri } from "./util/uriTypes"; |
|
||||||
import { isContainerUri } from "./util/uriTypes"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Options for getting a resource |
|
||||||
*/ |
|
||||||
export interface ResourceGetterOptions { |
|
||||||
/** |
|
||||||
* If autoLoad is set to true and the resource is unfetched, `read` will be called. |
|
||||||
* |
|
||||||
* @default false |
|
||||||
*/ |
|
||||||
autoLoad?: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* A store of Solid resources |
|
||||||
*/ |
|
||||||
export class ResourceStore { |
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* |
|
||||||
* A mapping between a resource URI and a Solid resource |
|
||||||
*/ |
|
||||||
protected resourceMap: Map<string, Leaf | Container>; |
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* |
|
||||||
* Context about the SolidLdoDataset |
|
||||||
*/ |
|
||||||
protected context: SolidLdoDatasetContext; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param context - A SolidLdoDatasetContext of the parent SolidLdoDataset |
|
||||||
*/ |
|
||||||
constructor(context: SolidLdoDatasetContext) { |
|
||||||
this.resourceMap = new Map(); |
|
||||||
this.context = context; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Gets a resource representation |
|
||||||
* |
|
||||||
* @param uri - The URI of the resource |
|
||||||
* @param options - ResourceGetterOptions |
|
||||||
* |
|
||||||
* @returns The resource representation |
|
||||||
*/ |
|
||||||
get(uri: ContainerUri, options?: ResourceGetterOptions): Container; |
|
||||||
get(uri: LeafUri, options?: ResourceGetterOptions): Leaf; |
|
||||||
get(uri: string, options?: ResourceGetterOptions): Leaf | Container; |
|
||||||
get(uri: string, options?: ResourceGetterOptions): Leaf | Container { |
|
||||||
// Normalize URI by removing hash
|
|
||||||
const url = new URL(uri); |
|
||||||
url.hash = ""; |
|
||||||
const normalizedUri = url.toString(); |
|
||||||
|
|
||||||
// Get the document and return if exists
|
|
||||||
let resource = this.resourceMap.get(normalizedUri); |
|
||||||
if (!resource) { |
|
||||||
if (isContainerUri(normalizedUri)) { |
|
||||||
resource = new Container(normalizedUri, this.context); |
|
||||||
} else { |
|
||||||
resource = new Leaf(normalizedUri as LeafUri, this.context); |
|
||||||
} |
|
||||||
this.resourceMap.set(normalizedUri, resource); |
|
||||||
} |
|
||||||
|
|
||||||
if (options?.autoLoad) { |
|
||||||
resource.read(); |
|
||||||
} |
|
||||||
|
|
||||||
return resource; |
|
||||||
} |
|
||||||
} |
|
@ -1,169 +0,0 @@ |
|||||||
import type { LdoBase, ShapeType } from "@ldo/ldo"; |
|
||||||
import { LdoDataset, startTransaction } from "@ldo/ldo"; |
|
||||||
import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types"; |
|
||||||
import type { Container } from "./resource/Container"; |
|
||||||
import type { Leaf } from "./resource/Leaf"; |
|
||||||
import type { ResourceGetterOptions } from "./ResourceStore"; |
|
||||||
import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; |
|
||||||
import type { ContainerUri, LeafUri } from "./util/uriTypes"; |
|
||||||
import { SolidLdoTransactionDataset } from "./SolidLdoTransactionDataset"; |
|
||||||
import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset"; |
|
||||||
import type { SubjectNode } from "@ldo/rdf-utils"; |
|
||||||
import type { Resource } from "./resource/Resource"; |
|
||||||
import type { CheckRootResultError } from "./requester/requests/checkRootContainer"; |
|
||||||
import type { NoRootContainerError } from "./requester/results/error/NoRootContainerError"; |
|
||||||
import type { ReadResultError } from "./requester/requests/readResource"; |
|
||||||
import { ProfileWithStorageShapeType } from "./.ldo/solid.shapeTypes"; |
|
||||||
import type { GetStorageContainerFromWebIdSuccess } from "./requester/results/success/CheckRootContainerSuccess"; |
|
||||||
import type { ISolidLdoDataset } from "./types"; |
|
||||||
|
|
||||||
/** |
|
||||||
* A SolidLdoDataset has all the functionality of an LdoDataset with the added |
|
||||||
* functionality of keeping track of fetched Solid Resources. |
|
||||||
* |
|
||||||
* It is recommended to use the { @link createSolidLdoDataset } to initialize |
|
||||||
* this class |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* import { createSolidLdoDataset } from "@ldo/solid"; |
|
||||||
* import { ProfileShapeType } from "./.ldo/profile.shapeTypes.ts" |
|
||||||
* |
|
||||||
* // ...
|
|
||||||
* |
|
||||||
* const solidLdoDataset = createSolidLdoDataset(); |
|
||||||
* |
|
||||||
* const profileDocument = solidLdoDataset |
|
||||||
* .getResource("https://example.com/profile"); |
|
||||||
* await profileDocument.read(); |
|
||||||
* |
|
||||||
* const profile = solidLdoDataset |
|
||||||
* .using(ProfileShapeType) |
|
||||||
* .fromSubject("https://example.com/profile#me"); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
export class SolidLdoDataset extends LdoDataset implements ISolidLdoDataset { |
|
||||||
/** |
|
||||||
* @internal |
|
||||||
*/ |
|
||||||
public context: SolidLdoDatasetContext; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param context - SolidLdoDatasetContext |
|
||||||
* @param datasetFactory - An optional dataset factory |
|
||||||
* @param transactionDatasetFactory - A factory for creating transaction datasets |
|
||||||
* @param initialDataset - A set of triples to initialize this dataset |
|
||||||
*/ |
|
||||||
constructor( |
|
||||||
context: SolidLdoDatasetContext, |
|
||||||
datasetFactory: DatasetFactory, |
|
||||||
transactionDatasetFactory: ITransactionDatasetFactory<Quad>, |
|
||||||
initialDataset?: Dataset, |
|
||||||
) { |
|
||||||
super(datasetFactory, transactionDatasetFactory, initialDataset); |
|
||||||
this.context = context; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Retireves a representation (either a LeafResource or a ContainerResource) |
|
||||||
* of a Solid Resource at the given URI. This resource represents the |
|
||||||
* current state of the resource: whether it is currently fetched or in the |
|
||||||
* process of fetching as well as some information about it. |
|
||||||
* |
|
||||||
* @param uri - the URI of the resource |
|
||||||
* @param options - Special options for getting the resource |
|
||||||
* |
|
||||||
* @returns a Leaf or Container Resource |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const profileDocument = solidLdoDataset |
|
||||||
* .getResource("https://example.com/profile"); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
getResource(uri: ContainerUri, options?: ResourceGetterOptions): Container; |
|
||||||
getResource(uri: LeafUri, options?: ResourceGetterOptions): Leaf; |
|
||||||
getResource(uri: string, options?: ResourceGetterOptions): Leaf | Container { |
|
||||||
return this.context.resourceStore.get(uri, options); |
|
||||||
} |
|
||||||
|
|
||||||
public startTransaction(): SolidLdoTransactionDataset { |
|
||||||
return new SolidLdoTransactionDataset( |
|
||||||
this, |
|
||||||
this.context, |
|
||||||
this.datasetFactory, |
|
||||||
this.transactionDatasetFactory, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Shorthand for solidLdoDataset |
|
||||||
* .usingType(shapeType) |
|
||||||
* .write(...resources.map((r) => r.uri)) |
|
||||||
* .fromSubject(subject); |
|
||||||
* @param shapeType - The shapetype to represent the data |
|
||||||
* @param subject - A subject URI |
|
||||||
* @param resources - The resources changes to should written to |
|
||||||
*/ |
|
||||||
createData<Type extends LdoBase>( |
|
||||||
shapeType: ShapeType<Type>, |
|
||||||
subject: string | SubjectNode, |
|
||||||
resource: Resource, |
|
||||||
...additionalResources: Resource[] |
|
||||||
): Type { |
|
||||||
const resources = [resource, ...additionalResources]; |
|
||||||
const linkedDataObject = this.usingType(shapeType) |
|
||||||
.write(...resources.map((r) => r.uri)) |
|
||||||
.fromSubject(subject); |
|
||||||
startTransaction(linkedDataObject); |
|
||||||
return linkedDataObject; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Gets a list of root storage containers for a user given their WebId |
|
||||||
* @param webId: The webId for the user |
|
||||||
* @returns A list of storages if successful, an error if not |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await solidLdoDataset |
|
||||||
* .getStorageFromWebId("https://example.com/profile/card#me"); |
|
||||||
* if (result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* console.log(result.storageContainer[0].uri); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async getStorageFromWebId( |
|
||||||
webId: LeafUri, |
|
||||||
): Promise< |
|
||||||
| GetStorageContainerFromWebIdSuccess |
|
||||||
| CheckRootResultError |
|
||||||
| ReadResultError |
|
||||||
| NoRootContainerError |
|
||||||
> { |
|
||||||
const webIdResource = this.getResource(webId); |
|
||||||
const readResult = await webIdResource.readIfUnfetched(); |
|
||||||
if (readResult.isError) return readResult; |
|
||||||
const profile = this.usingType(ProfileWithStorageShapeType).fromSubject( |
|
||||||
webId, |
|
||||||
); |
|
||||||
if (profile.storage && profile.storage.size > 0) { |
|
||||||
const containers = profile.storage.map((storageNode) => |
|
||||||
this.getResource(storageNode["@id"] as ContainerUri), |
|
||||||
); |
|
||||||
return { |
|
||||||
type: "getStorageContainerFromWebIdSuccess", |
|
||||||
isError: false, |
|
||||||
storageContainers: containers, |
|
||||||
}; |
|
||||||
} |
|
||||||
const getContainerResult = await webIdResource.getRootContainer(); |
|
||||||
if (getContainerResult.type === "container") |
|
||||||
return { |
|
||||||
type: "getStorageContainerFromWebIdSuccess", |
|
||||||
isError: false, |
|
||||||
storageContainers: [getContainerResult], |
|
||||||
}; |
|
||||||
return getContainerResult; |
|
||||||
} |
|
||||||
} |
|
@ -1,20 +0,0 @@ |
|||||||
import type { ResourceStore } from "./ResourceStore"; |
|
||||||
import type { SolidLdoDataset } from "./SolidLdoDataset"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Context to be shared between aspects of a SolidLdoDataset |
|
||||||
*/ |
|
||||||
export interface SolidLdoDatasetContext { |
|
||||||
/** |
|
||||||
* A pointer to the parent SolidLdoDataset |
|
||||||
*/ |
|
||||||
solidLdoDataset: SolidLdoDataset; |
|
||||||
/** |
|
||||||
* The resource store of the SolidLdoDataset |
|
||||||
*/ |
|
||||||
resourceStore: ResourceStore; |
|
||||||
/** |
|
||||||
* Http fetch function |
|
||||||
*/ |
|
||||||
fetch: typeof fetch; |
|
||||||
} |
|
@ -1,186 +0,0 @@ |
|||||||
import { LdoTransactionDataset } from "@ldo/ldo"; |
|
||||||
import type { ISolidLdoDataset } from "./types"; |
|
||||||
import type { ResourceGetterOptions } from "./ResourceStore"; |
|
||||||
import type { Container } from "./resource/Container"; |
|
||||||
import type { Leaf } from "./resource/Leaf"; |
|
||||||
import { |
|
||||||
isContainerUri, |
|
||||||
type ContainerUri, |
|
||||||
type LeafUri, |
|
||||||
} from "./util/uriTypes"; |
|
||||||
import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; |
|
||||||
import type { DatasetFactory, Quad } from "@rdfjs/types"; |
|
||||||
import { |
|
||||||
updateDatasetInBulk, |
|
||||||
type ITransactionDatasetFactory, |
|
||||||
} from "@ldo/subscribable-dataset"; |
|
||||||
import type { AggregateSuccess } from "./requester/results/success/SuccessResult"; |
|
||||||
import type { ResourceResult } from "./resource/resourceResult/ResourceResult"; |
|
||||||
import type { |
|
||||||
IgnoredInvalidUpdateSuccess, |
|
||||||
UpdateDefaultGraphSuccess, |
|
||||||
UpdateSuccess, |
|
||||||
} from "./requester/results/success/UpdateSuccess"; |
|
||||||
import { AggregateError } from "./requester/results/error/ErrorResult"; |
|
||||||
import type { |
|
||||||
UpdateResult, |
|
||||||
UpdateResultError, |
|
||||||
} from "./requester/requests/updateDataResource"; |
|
||||||
import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils"; |
|
||||||
import { splitChangesByGraph } from "./util/splitChangesByGraph"; |
|
||||||
|
|
||||||
/** |
|
||||||
* A SolidLdoTransactionDataset has all the functionality of a SolidLdoDataset |
|
||||||
* and represents a transaction to the parent SolidLdoDataset. |
|
||||||
* |
|
||||||
* It is recommended to use the `startTransaction` method on a SolidLdoDataset |
|
||||||
* to initialize this class |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* import { createSolidLdoDataset } from "@ldo/solid"; |
|
||||||
* import { ProfileShapeType } from "./.ldo/profile.shapeTypes.ts" |
|
||||||
* |
|
||||||
* // ...
|
|
||||||
* |
|
||||||
* const solidLdoDataset = createSolidLdoDataset(); |
|
||||||
* |
|
||||||
* const profileDocument = solidLdoDataset |
|
||||||
* .getResource("https://example.com/profile"); |
|
||||||
* await profileDocument.read(); |
|
||||||
* |
|
||||||
* const transaction = solidLdoDataset.startTransaction(); |
|
||||||
* |
|
||||||
* const profile = transaction |
|
||||||
* .using(ProfileShapeType) |
|
||||||
* .fromSubject("https://example.com/profile#me"); |
|
||||||
* profile.name = "Some Name"; |
|
||||||
* await transaction.commitToPod(); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
export class SolidLdoTransactionDataset |
|
||||||
extends LdoTransactionDataset |
|
||||||
implements ISolidLdoDataset |
|
||||||
{ |
|
||||||
/** |
|
||||||
* @internal |
|
||||||
*/ |
|
||||||
public context: SolidLdoDatasetContext; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param context - SolidLdoDatasetContext |
|
||||||
* @param datasetFactory - An optional dataset factory |
|
||||||
* @param transactionDatasetFactory - A factory for creating transaction datasets |
|
||||||
* @param initialDataset - A set of triples to initialize this dataset |
|
||||||
*/ |
|
||||||
constructor( |
|
||||||
parentDataset: ISolidLdoDataset, |
|
||||||
context: SolidLdoDatasetContext, |
|
||||||
datasetFactory: DatasetFactory, |
|
||||||
transactionDatasetFactory: ITransactionDatasetFactory<Quad>, |
|
||||||
) { |
|
||||||
super(parentDataset, datasetFactory, transactionDatasetFactory); |
|
||||||
this.context = context; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Retireves a representation (either a LeafResource or a ContainerResource) |
|
||||||
* of a Solid Resource at the given URI. This resource represents the |
|
||||||
* current state of the resource: whether it is currently fetched or in the |
|
||||||
* process of fetching as well as some information about it. |
|
||||||
* |
|
||||||
* @param uri - the URI of the resource |
|
||||||
* @param options - Special options for getting the resource |
|
||||||
* |
|
||||||
* @returns a Leaf or Container Resource |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const profileDocument = solidLdoDataset |
|
||||||
* .getResource("https://example.com/profile"); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
getResource(uri: ContainerUri, options?: ResourceGetterOptions): Container; |
|
||||||
getResource(uri: LeafUri, options?: ResourceGetterOptions): Leaf; |
|
||||||
getResource(uri: string, options?: ResourceGetterOptions): Leaf | Container; |
|
||||||
getResource(uri: string, options?: ResourceGetterOptions): Leaf | Container { |
|
||||||
return this.context.resourceStore.get(uri, options); |
|
||||||
} |
|
||||||
|
|
||||||
public startTransaction(): SolidLdoTransactionDataset { |
|
||||||
return new SolidLdoTransactionDataset( |
|
||||||
this, |
|
||||||
this.context, |
|
||||||
this.datasetFactory, |
|
||||||
this.transactionDatasetFactory, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
async commitToPod(): Promise< |
|
||||||
| AggregateSuccess< |
|
||||||
ResourceResult<UpdateSuccess | UpdateDefaultGraphSuccess, Leaf> |
|
||||||
> |
|
||||||
| AggregateError<UpdateResultError> |
|
||||||
> { |
|
||||||
const changes = this.getChanges(); |
|
||||||
const changesByGraph = splitChangesByGraph(changes); |
|
||||||
|
|
||||||
// Iterate through all changes by graph in
|
|
||||||
const results: [ |
|
||||||
GraphNode, |
|
||||||
DatasetChanges<Quad>, |
|
||||||
UpdateResult | IgnoredInvalidUpdateSuccess | UpdateDefaultGraphSuccess, |
|
||||||
][] = await Promise.all( |
|
||||||
Array.from(changesByGraph.entries()).map( |
|
||||||
async ([graph, datasetChanges]) => { |
|
||||||
if (graph.termType === "DefaultGraph") { |
|
||||||
// Undefined means that this is the default graph
|
|
||||||
updateDatasetInBulk(this.parentDataset, datasetChanges); |
|
||||||
return [ |
|
||||||
graph, |
|
||||||
datasetChanges, |
|
||||||
{ |
|
||||||
type: "updateDefaultGraphSuccess", |
|
||||||
isError: false, |
|
||||||
} as UpdateDefaultGraphSuccess, |
|
||||||
]; |
|
||||||
} |
|
||||||
if (isContainerUri(graph.value)) { |
|
||||||
return [ |
|
||||||
graph, |
|
||||||
datasetChanges, |
|
||||||
{ |
|
||||||
type: "ignoredInvalidUpdateSuccess", |
|
||||||
isError: false, |
|
||||||
} as IgnoredInvalidUpdateSuccess, |
|
||||||
]; |
|
||||||
} |
|
||||||
const resource = this.getResource(graph.value as LeafUri); |
|
||||||
const updateResult = await resource.update(datasetChanges); |
|
||||||
return [graph, datasetChanges, updateResult]; |
|
||||||
}, |
|
||||||
), |
|
||||||
); |
|
||||||
|
|
||||||
// If one has errored, return error
|
|
||||||
const errors = results.filter((result) => result[2].isError); |
|
||||||
|
|
||||||
if (errors.length > 0) { |
|
||||||
return new AggregateError( |
|
||||||
errors.map((result) => result[2] as UpdateResultError), |
|
||||||
); |
|
||||||
} |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "aggregateSuccess", |
|
||||||
results: results |
|
||||||
.map((result) => result[2]) |
|
||||||
.filter( |
|
||||||
(result): result is ResourceResult<UpdateSuccess, Leaf> => |
|
||||||
result.type === "updateSuccess" || |
|
||||||
result.type === "updateDefaultGraphSuccess" || |
|
||||||
result.type === "ignoredInvalidUpdateSuccess", |
|
||||||
), |
|
||||||
}; |
|
||||||
} |
|
||||||
} |
|
@ -1,68 +0,0 @@ |
|||||||
import type { Dataset, DatasetFactory } from "@rdfjs/types"; |
|
||||||
import { SolidLdoDataset } from "./SolidLdoDataset"; |
|
||||||
|
|
||||||
import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; |
|
||||||
import { createDataset, createDatasetFactory } from "@ldo/dataset"; |
|
||||||
import { ResourceStore } from "./ResourceStore"; |
|
||||||
import { guaranteeFetch } from "./util/guaranteeFetch"; |
|
||||||
import { createTransactionDatasetFactory } from "@ldo/subscribable-dataset"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Options for createSolidDataset |
|
||||||
*/ |
|
||||||
export interface CreateSolidLdoDatasetOptions { |
|
||||||
/** |
|
||||||
* A fetch function. Most often, this is the fetch function from @inrupt/solid-clieht-authn-js |
|
||||||
*/ |
|
||||||
fetch?: typeof fetch; |
|
||||||
/** |
|
||||||
* An initial dataset |
|
||||||
* @default A blank dataset |
|
||||||
*/ |
|
||||||
dataset?: Dataset; |
|
||||||
/** |
|
||||||
* An RDFJS DatasetFactory |
|
||||||
* @default An extended RDFJS DatasetFactory |
|
||||||
*/ |
|
||||||
datasetFactory?: DatasetFactory; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a SolidLdoDataset |
|
||||||
* |
|
||||||
* @param options - CreateSolidLdoDatasetOptions |
|
||||||
* @returns A SolidLdoDataset |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* import { createSolidLdoDataset } from "@ldo/solid"; |
|
||||||
* import { fetch } from "@inrupt/solid-client-authn-browswer"; |
|
||||||
* |
|
||||||
* const solidLdoDataset = createSolidLdoDataset({ fetch }); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
export function createSolidLdoDataset( |
|
||||||
options?: CreateSolidLdoDatasetOptions, |
|
||||||
): SolidLdoDataset { |
|
||||||
const finalFetch = guaranteeFetch(options?.fetch); |
|
||||||
const finalDatasetFactory = options?.datasetFactory || createDatasetFactory(); |
|
||||||
const finalDataset = options?.dataset || createDataset(); |
|
||||||
|
|
||||||
// Ignoring because of circular dependency
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
const context: SolidLdoDatasetContext = { |
|
||||||
fetch: finalFetch, |
|
||||||
}; |
|
||||||
const solidLdoDataset = new SolidLdoDataset( |
|
||||||
context, |
|
||||||
finalDatasetFactory, |
|
||||||
createTransactionDatasetFactory(), |
|
||||||
finalDataset, |
|
||||||
); |
|
||||||
const resourceStore = new ResourceStore(context); |
|
||||||
context.solidLdoDataset = solidLdoDataset; |
|
||||||
context.resourceStore = resourceStore; |
|
||||||
|
|
||||||
return solidLdoDataset; |
|
||||||
} |
|
@ -1,31 +0,0 @@ |
|||||||
export * from "./createSolidLdoDataset"; |
|
||||||
export * from "./SolidLdoDataset"; |
|
||||||
export * from "./SolidLdoDatasetContext"; |
|
||||||
export * from "./SolidLdoTransactionDataset"; |
|
||||||
|
|
||||||
export * from "./resource/Resource"; |
|
||||||
export * from "./resource/Container"; |
|
||||||
export * from "./resource/Leaf"; |
|
||||||
|
|
||||||
export * from "./util/uriTypes"; |
|
||||||
|
|
||||||
export * from "./methods"; |
|
||||||
|
|
||||||
export * from "./requester/requests/checkRootContainer"; |
|
||||||
export * from "./requester/requests/createDataResource"; |
|
||||||
export * from "./requester/requests/deleteResource"; |
|
||||||
export * from "./requester/requests/readResource"; |
|
||||||
export * from "./requester/requests/requestOptions"; |
|
||||||
export * from "./requester/requests/updateDataResource"; |
|
||||||
export * from "./requester/requests/uploadResource"; |
|
||||||
|
|
||||||
export * from "./resource/wac/WacRule"; |
|
||||||
export * from "./resource/wac/getWacRule"; |
|
||||||
export * from "./resource/wac/getWacUri"; |
|
||||||
export * from "./resource/wac/setWacRule"; |
|
||||||
export * from "./resource/wac/results/GetWacRuleSuccess"; |
|
||||||
export * from "./resource/wac/results/GetWacUriSuccess"; |
|
||||||
export * from "./resource/wac/results/SetWacRuleSuccess"; |
|
||||||
export * from "./resource/wac/results/WacRuleAbsent"; |
|
||||||
|
|
||||||
export * from "./types"; |
|
@ -1,84 +0,0 @@ |
|||||||
import { startTransaction, type LdoBase, write, getDataset } from "@ldo/ldo"; |
|
||||||
import type { Resource } from "./resource/Resource"; |
|
||||||
import type { Quad } from "@rdfjs/types"; |
|
||||||
import { _proxyContext, getProxyFromObject } from "@ldo/jsonld-dataset-proxy"; |
|
||||||
import type { SubscribableDataset } from "@ldo/subscribable-dataset"; |
|
||||||
import type { SolidLdoTransactionDataset } from "./SolidLdoTransactionDataset"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Begins tracking changes to eventually commit. |
|
||||||
* |
|
||||||
* @param input - A linked data object to track changes on |
|
||||||
* @param resource - A resource that all additions will eventually be committed to |
|
||||||
* @param additionalResources - Any additional resources that changes will eventually be committed to |
|
||||||
* |
|
||||||
* @returns A transactable Linked Data Object |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* import { changeData } from "@ldo/solid"; |
|
||||||
* |
|
||||||
* // ...
|
|
||||||
* |
|
||||||
* const profile = solidLdoDataset |
|
||||||
* .using(ProfileShapeType) |
|
||||||
* .fromSubject("https://example.com/profile#me"); |
|
||||||
* const resource = solidLdoDataset.getResource("https://example.com/profile"); |
|
||||||
* |
|
||||||
* const cProfile = changeData(profile, resource); |
|
||||||
* cProfile.name = "My New Name"; |
|
||||||
* const result = await commitData(cProfile); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
export function changeData<Type extends LdoBase>( |
|
||||||
input: Type, |
|
||||||
resource: Resource, |
|
||||||
...additionalResources: Resource[] |
|
||||||
): Type { |
|
||||||
const resources = [resource, ...additionalResources]; |
|
||||||
// Clone the input and set a graph
|
|
||||||
const [transactionLdo] = write(...resources.map((r) => r.uri)).usingCopy( |
|
||||||
input, |
|
||||||
); |
|
||||||
// Start a transaction with the input
|
|
||||||
startTransaction(transactionLdo); |
|
||||||
// Return
|
|
||||||
return transactionLdo; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Commits the transaction to the global dataset, syncing all subscribing |
|
||||||
* components and Solid Pods |
|
||||||
* |
|
||||||
* @param input - A transactable linked data object |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* import { changeData } from "@ldo/solid"; |
|
||||||
* |
|
||||||
* // ...
|
|
||||||
* |
|
||||||
* const profile = solidLdoDataset |
|
||||||
* .using(ProfileShapeType) |
|
||||||
* .fromSubject("https://example.com/profile#me"); |
|
||||||
* const resource = solidLdoDataset.getResource("https://example.com/profile"); |
|
||||||
* |
|
||||||
* const cProfile = changeData(profile, resource); |
|
||||||
* cProfile.name = "My New Name"; |
|
||||||
* const result = await commitData(cProfile); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
export async function commitData( |
|
||||||
input: LdoBase, |
|
||||||
): ReturnType<SolidLdoTransactionDataset["commitToPod"]> { |
|
||||||
const transactionDataset = getDataset(input) as SolidLdoTransactionDataset; |
|
||||||
const result = await transactionDataset.commitToPod(); |
|
||||||
if (result.isError) return result; |
|
||||||
// Take the LdoProxy out of commit mode. This uses hidden methods of JSONLD-DATASET-PROXY
|
|
||||||
const proxy = getProxyFromObject(input); |
|
||||||
proxy[_proxyContext] = proxy[_proxyContext].duplicate({ |
|
||||||
dataset: proxy[_proxyContext].state |
|
||||||
.parentDataset as SubscribableDataset<Quad>, |
|
||||||
}); |
|
||||||
return result; |
|
||||||
} |
|
@ -1,190 +0,0 @@ |
|||||||
import { ANY_KEY, RequestBatcher } from "../util/RequestBatcher"; |
|
||||||
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; |
|
||||||
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"; |
|
||||||
|
|
||||||
const READ_KEY = "read"; |
|
||||||
const CREATE_KEY = "createDataResource"; |
|
||||||
const DELETE_KEY = "delete"; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* |
|
||||||
* A singleton for handling batched requests |
|
||||||
*/ |
|
||||||
export abstract class BatchedRequester { |
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* A request batcher to maintain state for ongoing requests |
|
||||||
*/ |
|
||||||
protected readonly requestBatcher = new RequestBatcher(); |
|
||||||
|
|
||||||
/** |
|
||||||
* The uri of the resource |
|
||||||
*/ |
|
||||||
abstract readonly uri: string; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* SolidLdoDatasetContext for the parent SolidLdoDataset |
|
||||||
*/ |
|
||||||
protected context: SolidLdoDatasetContext; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param context - SolidLdoDatasetContext for the parent SolidLdoDataset |
|
||||||
*/ |
|
||||||
constructor(context: SolidLdoDatasetContext) { |
|
||||||
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.solidLdoDataset.startTransaction(); |
|
||||||
const result = await this.requestBatcher.queueProcess({ |
|
||||||
name: READ_KEY, |
|
||||||
args: [this.uri, { dataset: transaction, fetch: this.context.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> { |
|
||||||
const transaction = this.context.solidLdoDataset.startTransaction(); |
|
||||||
const result = await this.requestBatcher.queueProcess({ |
|
||||||
name: DELETE_KEY, |
|
||||||
args: [this.uri, { dataset: transaction, fetch: this.context.fetch }], |
|
||||||
perform: deleteResource, |
|
||||||
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys(DELETE_KEY), |
|
||||||
after: (result) => { |
|
||||||
if (!result.isError) { |
|
||||||
transaction.commit(); |
|
||||||
} |
|
||||||
}, |
|
||||||
}); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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.solidLdoDataset.startTransaction(); |
|
||||||
const result = await this.requestBatcher.queueProcess({ |
|
||||||
name: CREATE_KEY, |
|
||||||
args: [ |
|
||||||
this.uri, |
|
||||||
overwrite, |
|
||||||
{ dataset: transaction, fetch: this.context.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; |
|
||||||
} |
|
||||||
} |
|
@ -1,79 +0,0 @@ |
|||||||
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; |
|
||||||
import type { ContainerUri } from "../util/uriTypes"; |
|
||||||
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"; |
|
||||||
|
|
||||||
export const IS_ROOT_CONTAINER_KEY = "isRootContainer"; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* |
|
||||||
* A singleton to handle batched requests for containers |
|
||||||
*/ |
|
||||||
export class ContainerBatchedRequester extends BatchedRequester { |
|
||||||
/** |
|
||||||
* The URI of the container |
|
||||||
*/ |
|
||||||
readonly uri: ContainerUri; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param uri - The URI of the container |
|
||||||
* @param context - SolidLdoDatasetContext of the parent dataset |
|
||||||
*/ |
|
||||||
constructor(uri: ContainerUri, context: SolidLdoDatasetContext) { |
|
||||||
super(context); |
|
||||||
this.uri = uri; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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.uri as ContainerUri, { fetch: this.context.fetch }], |
|
||||||
perform: checkRootContainer, |
|
||||||
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys( |
|
||||||
IS_ROOT_CONTAINER_KEY, |
|
||||||
), |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
@ -1,172 +0,0 @@ |
|||||||
import type { DatasetChanges } from "@ldo/rdf-utils"; |
|
||||||
import { mergeDatasetChanges } from "@ldo/subscribable-dataset"; |
|
||||||
import type { Quad } from "@rdfjs/types"; |
|
||||||
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; |
|
||||||
import type { LeafUri } from "../util/uriTypes"; |
|
||||||
import { 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"; |
|
||||||
|
|
||||||
export const UPDATE_KEY = "update"; |
|
||||||
export const UPLOAD_KEY = "upload"; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* |
|
||||||
* A singleton to handle batched requests for leafs |
|
||||||
*/ |
|
||||||
export class LeafBatchedRequester extends BatchedRequester { |
|
||||||
/** |
|
||||||
* The URI of the leaf |
|
||||||
*/ |
|
||||||
readonly uri: LeafUri; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param uri - the URI of the leaf |
|
||||||
* @param context - SolidLdoDatasetContext of the parent dataset |
|
||||||
*/ |
|
||||||
constructor(uri: LeafUri, context: SolidLdoDatasetContext) { |
|
||||||
super(context); |
|
||||||
this.uri = uri; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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> { |
|
||||||
const result = await this.requestBatcher.queueProcess({ |
|
||||||
name: UPDATE_KEY, |
|
||||||
args: [ |
|
||||||
this.uri, |
|
||||||
changes, |
|
||||||
{ fetch: this.context.fetch, dataset: this.context.solidLdoDataset }, |
|
||||||
], |
|
||||||
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; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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.solidLdoDataset.startTransaction(); |
|
||||||
const result = await this.requestBatcher.queueProcess({ |
|
||||||
name: UPLOAD_KEY, |
|
||||||
args: [ |
|
||||||
this.uri, |
|
||||||
blob, |
|
||||||
mimeType, |
|
||||||
overwrite, |
|
||||||
{ dataset: transaction, fetch: this.context.fetch }, |
|
||||||
], |
|
||||||
perform: uploadResource, |
|
||||||
modifyQueue: (queue, 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; |
|
||||||
} |
|
||||||
} |
|
@ -1,99 +0,0 @@ |
|||||||
import type { BasicRequestOptions } from "./requestOptions"; |
|
||||||
import { parse as parseLinkHeader } from "http-link-header"; |
|
||||||
import type { CheckRootContainerSuccess } from "../results/success/CheckRootContainerSuccess"; |
|
||||||
import type { |
|
||||||
HttpErrorResultType, |
|
||||||
UnexpectedHttpError, |
|
||||||
} from "../results/error/HttpErrorResult"; |
|
||||||
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
|
||||||
import { UnexpectedResourceError } from "../results/error/ErrorResult"; |
|
||||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
|
||||||
import type { ContainerUri } from "../../util/uriTypes"; |
|
||||||
|
|
||||||
/** |
|
||||||
* checkRootContainer result |
|
||||||
*/ |
|
||||||
export type CheckRootResult = CheckRootContainerSuccess | CheckRootResultError; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible errors checkRootResult can return |
|
||||||
*/ |
|
||||||
export type CheckRootResultError = |
|
||||||
| HttpErrorResultType |
|
||||||
| UnexpectedHttpError |
|
||||||
| UnexpectedResourceError; |
|
||||||
|
|
||||||
/** |
|
||||||
* @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( |
|
||||||
uri: ContainerUri, |
|
||||||
headers: Headers, |
|
||||||
): CheckRootContainerSuccess { |
|
||||||
const linkHeader = headers.get("link"); |
|
||||||
if (!linkHeader) { |
|
||||||
return { |
|
||||||
uri, |
|
||||||
isRootContainer: false, |
|
||||||
type: "checkRootContainerSuccess", |
|
||||||
isError: 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 { |
|
||||||
uri, |
|
||||||
isRootContainer, |
|
||||||
type: "checkRootContainerSuccess", |
|
||||||
isError: false, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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( |
|
||||||
uri: ContainerUri, |
|
||||||
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(uri, { method: "HEAD", cache: "no-store" }); |
|
||||||
const httpErrorResult = HttpErrorResult.checkResponse(uri, response); |
|
||||||
if (httpErrorResult) return httpErrorResult; |
|
||||||
|
|
||||||
return checkHeadersForRootContainer(uri, response.headers); |
|
||||||
} catch (err) { |
|
||||||
return UnexpectedResourceError.fromThrown(uri, err); |
|
||||||
} |
|
||||||
} |
|
@ -1,241 +0,0 @@ |
|||||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
|
||||||
import { |
|
||||||
addResourceRdfToContainer, |
|
||||||
getParentUri, |
|
||||||
getSlug, |
|
||||||
} from "../../util/rdfUtils"; |
|
||||||
import type { ContainerUri, LeafUri } from "../../util/uriTypes"; |
|
||||||
import { isContainerUri } from "../../util/uriTypes"; |
|
||||||
import { UnexpectedResourceError } from "../results/error/ErrorResult"; |
|
||||||
import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; |
|
||||||
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
|
||||||
import type { 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"; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible return values when creating and overwriting a container |
|
||||||
*/ |
|
||||||
export type ContainerCreateAndOverwriteResult = |
|
||||||
| CreateSuccess |
|
||||||
| CreateAndOverwriteResultErrors; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible return values when creating and overwriting a leaf |
|
||||||
*/ |
|
||||||
export type LeafCreateAndOverwriteResult = |
|
||||||
| CreateSuccess |
|
||||||
| CreateAndOverwriteResultErrors; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible return values when creating a container if absent |
|
||||||
*/ |
|
||||||
export type ContainerCreateIfAbsentResult = |
|
||||||
| CreateSuccess |
|
||||||
| Exclude<ReadContainerResult, AbsentReadSuccess> |
|
||||||
| CreateIfAbsentResultErrors; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible return values when creating a leaf if absent |
|
||||||
*/ |
|
||||||
export type LeafCreateIfAbsentResult = |
|
||||||
| CreateSuccess |
|
||||||
| Exclude<ReadLeafResult, AbsentReadSuccess> |
|
||||||
| CreateIfAbsentResultErrors; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible errors returned by creating and overwriting a resource |
|
||||||
*/ |
|
||||||
export type CreateAndOverwriteResultErrors = DeleteResultError | CreateErrors; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible errors returned by creating a resource if absent |
|
||||||
*/ |
|
||||||
export type CreateIfAbsentResultErrors = ReadResultError | CreateErrors; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible errors returned by creating a resource |
|
||||||
*/ |
|
||||||
export type CreateErrors = HttpErrorResultType | UnexpectedResourceError; |
|
||||||
|
|
||||||
/** |
|
||||||
* 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( |
|
||||||
uri: ContainerUri, |
|
||||||
overwrite: true, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<ContainerCreateAndOverwriteResult>; |
|
||||||
export function createDataResource( |
|
||||||
uri: LeafUri, |
|
||||||
overwrite: true, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<LeafCreateAndOverwriteResult>; |
|
||||||
export function createDataResource( |
|
||||||
uri: ContainerUri, |
|
||||||
overwrite?: false, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<ContainerCreateIfAbsentResult>; |
|
||||||
export function createDataResource( |
|
||||||
uri: LeafUri, |
|
||||||
overwrite?: false, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<LeafCreateIfAbsentResult>; |
|
||||||
export function createDataResource( |
|
||||||
uri: ContainerUri, |
|
||||||
overwrite?: boolean, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<ContainerCreateIfAbsentResult | ContainerCreateAndOverwriteResult>; |
|
||||||
export function createDataResource( |
|
||||||
uri: LeafUri, |
|
||||||
overwrite?: boolean, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult>; |
|
||||||
export function createDataResource( |
|
||||||
uri: string, |
|
||||||
overwrite: true, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult>; |
|
||||||
export function createDataResource( |
|
||||||
uri: string, |
|
||||||
overwrite?: false, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<LeafCreateIfAbsentResult | LeafCreateIfAbsentResult>; |
|
||||||
export function createDataResource( |
|
||||||
uri: string, |
|
||||||
overwrite?: boolean, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise< |
|
||||||
| ContainerCreateAndOverwriteResult |
|
||||||
| LeafCreateAndOverwriteResult |
|
||||||
| ContainerCreateIfAbsentResult |
|
||||||
| LeafCreateIfAbsentResult |
|
||||||
>; |
|
||||||
export async function createDataResource( |
|
||||||
uri: string, |
|
||||||
overwrite?: boolean, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise< |
|
||||||
| ContainerCreateAndOverwriteResult |
|
||||||
| LeafCreateAndOverwriteResult |
|
||||||
| ContainerCreateIfAbsentResult |
|
||||||
| LeafCreateIfAbsentResult |
|
||||||
> { |
|
||||||
try { |
|
||||||
const fetch = guaranteeFetch(options?.fetch); |
|
||||||
let didOverwrite = false; |
|
||||||
if (overwrite) { |
|
||||||
const deleteResult = await deleteResource(uri, 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(uri, options); |
|
||||||
|
|
||||||
// If it does exist stop and return.
|
|
||||||
if (readResult.type !== "absentReadSuccess") { |
|
||||||
return readResult; |
|
||||||
} |
|
||||||
} |
|
||||||
// Create the document
|
|
||||||
const parentUri = getParentUri(uri)!; |
|
||||||
const headers: HeadersInit = { |
|
||||||
"content-type": "text/turtle", |
|
||||||
slug: getSlug(uri), |
|
||||||
}; |
|
||||||
if (isContainerUri(uri)) { |
|
||||||
headers.link = '<http://www.w3.org/ns/ldp#Container>; rel="type"'; |
|
||||||
} |
|
||||||
const response = await fetch(parentUri, { |
|
||||||
method: "post", |
|
||||||
headers, |
|
||||||
}); |
|
||||||
|
|
||||||
const httpError = HttpErrorResult.checkResponse(uri, response); |
|
||||||
if (httpError) return httpError; |
|
||||||
|
|
||||||
if (options?.dataset) { |
|
||||||
addResourceRdfToContainer(uri, options.dataset); |
|
||||||
} |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "createSuccess", |
|
||||||
uri, |
|
||||||
didOverwrite, |
|
||||||
}; |
|
||||||
} catch (err) { |
|
||||||
return UnexpectedResourceError.fromThrown(uri, err); |
|
||||||
} |
|
||||||
} |
|
@ -1,95 +0,0 @@ |
|||||||
import { namedNode } from "@rdfjs/data-model"; |
|
||||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
|
||||||
import { deleteResourceRdfFromContainer } from "../../util/rdfUtils"; |
|
||||||
import { UnexpectedResourceError } from "../results/error/ErrorResult"; |
|
||||||
import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; |
|
||||||
import { UnexpectedHttpError } from "../results/error/HttpErrorResult"; |
|
||||||
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
|
||||||
import type { DeleteSuccess } from "../results/success/DeleteSuccess"; |
|
||||||
import type { DatasetRequestOptions } from "./requestOptions"; |
|
||||||
import type { IBulkEditableDataset } from "@ldo/subscribable-dataset"; |
|
||||||
import type { Quad } from "@rdfjs/types"; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible return values for deleteResource |
|
||||||
*/ |
|
||||||
export type DeleteResult = DeleteSuccess | DeleteResultError; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible errors that can be returned by deleteResource |
|
||||||
*/ |
|
||||||
export type DeleteResultError = HttpErrorResultType | UnexpectedResourceError; |
|
||||||
|
|
||||||
/** |
|
||||||
* 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( |
|
||||||
uri: string, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<DeleteResult> { |
|
||||||
try { |
|
||||||
const fetch = guaranteeFetch(options?.fetch); |
|
||||||
const response = await fetch(uri, { |
|
||||||
method: "delete", |
|
||||||
}); |
|
||||||
const errorResult = HttpErrorResult.checkResponse(uri, response); |
|
||||||
if (errorResult) return errorResult; |
|
||||||
|
|
||||||
// Specifically check for a 205. Annoyingly, the server will return 200 even
|
|
||||||
// if it hasn't been deleted when you're unauthenticated. 404 happens when
|
|
||||||
// the document never existed
|
|
||||||
if (response.status === 205 || response.status === 404) { |
|
||||||
if (options?.dataset) |
|
||||||
updateDatasetOnSuccessfulDelete(uri, options.dataset); |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "deleteSuccess", |
|
||||||
uri, |
|
||||||
resourceExisted: response.status === 205, |
|
||||||
}; |
|
||||||
} |
|
||||||
return new UnexpectedHttpError(uri, response); |
|
||||||
} catch (err) { |
|
||||||
return UnexpectedResourceError.fromThrown(uri, 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); |
|
||||||
} |
|
@ -1,182 +0,0 @@ |
|||||||
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 type { ContainerUri, LeafUri } from "../../util/uriTypes"; |
|
||||||
import { isContainerUri } from "../../util/uriTypes"; |
|
||||||
import type { BinaryReadSuccess } from "../results/success/ReadSuccess"; |
|
||||||
import type { |
|
||||||
ContainerReadSuccess, |
|
||||||
DataReadSuccess, |
|
||||||
} from "../results/success/ReadSuccess"; |
|
||||||
import type { AbsentReadSuccess } from "../results/success/ReadSuccess"; |
|
||||||
import { NoncompliantPodError } from "../results/error/NoncompliantPodError"; |
|
||||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
|
||||||
import { UnexpectedResourceError } from "../results/error/ErrorResult"; |
|
||||||
import { checkHeadersForRootContainer } from "./checkRootContainer"; |
|
||||||
import { namedNode } from "@rdfjs/data-model"; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible return values for reading a leaf |
|
||||||
*/ |
|
||||||
export type ReadLeafResult = |
|
||||||
| BinaryReadSuccess |
|
||||||
| DataReadSuccess |
|
||||||
| AbsentReadSuccess |
|
||||||
| ReadResultError; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible return values for reading a container |
|
||||||
*/ |
|
||||||
export type ReadContainerResult = |
|
||||||
| ContainerReadSuccess |
|
||||||
| AbsentReadSuccess |
|
||||||
| ReadResultError; |
|
||||||
|
|
||||||
/** |
|
||||||
* All possible errors the readResource function can return |
|
||||||
*/ |
|
||||||
export type ReadResultError = |
|
||||||
| HttpErrorResultType |
|
||||||
| NoncompliantPodError |
|
||||||
| UnexpectedHttpError |
|
||||||
| UnexpectedResourceError; |
|
||||||
|
|
||||||
/** |
|
||||||
* 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( |
|
||||||
uri: LeafUri, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<ReadLeafResult>; |
|
||||||
export async function readResource( |
|
||||||
uri: ContainerUri, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<ReadContainerResult>; |
|
||||||
export async function readResource( |
|
||||||
uri: string, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<ReadLeafResult | ReadContainerResult>; |
|
||||||
export async function readResource( |
|
||||||
uri: string, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<ReadLeafResult | ReadContainerResult> { |
|
||||||
try { |
|
||||||
const fetch = guaranteeFetch(options?.fetch); |
|
||||||
// Fetch options to determine the document type
|
|
||||||
const response = await fetch(uri, { |
|
||||||
headers: { accept: "text/turtle, */*" }, |
|
||||||
}); |
|
||||||
if (response.status === 404) { |
|
||||||
// Clear existing data if present
|
|
||||||
if (options?.dataset) { |
|
||||||
options.dataset.deleteMatches( |
|
||||||
undefined, |
|
||||||
undefined, |
|
||||||
undefined, |
|
||||||
namedNode(uri), |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "absentReadSuccess", |
|
||||||
uri, |
|
||||||
recalledFromMemory: false, |
|
||||||
}; |
|
||||||
} |
|
||||||
const httpErrorResult = HttpErrorResult.checkResponse(uri, response); |
|
||||||
if (httpErrorResult) return httpErrorResult; |
|
||||||
|
|
||||||
// Add this resource to the container
|
|
||||||
if (options?.dataset) { |
|
||||||
addResourceRdfToContainer(uri, options.dataset); |
|
||||||
} |
|
||||||
|
|
||||||
const contentType = response.headers.get("content-type"); |
|
||||||
if (!contentType) { |
|
||||||
return new NoncompliantPodError( |
|
||||||
uri, |
|
||||||
"Resource requests must return a content-type header.", |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
if (contentType.startsWith("text/turtle")) { |
|
||||||
// Parse Turtle
|
|
||||||
const rawTurtle = await response.text(); |
|
||||||
if (options?.dataset) { |
|
||||||
const result = await addRawTurtleToDataset( |
|
||||||
rawTurtle, |
|
||||||
options.dataset, |
|
||||||
uri, |
|
||||||
); |
|
||||||
if (result) return result; |
|
||||||
} |
|
||||||
if (isContainerUri(uri)) { |
|
||||||
const result = checkHeadersForRootContainer(uri, response.headers); |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "containerReadSuccess", |
|
||||||
uri, |
|
||||||
recalledFromMemory: false, |
|
||||||
isRootContainer: result.isRootContainer, |
|
||||||
}; |
|
||||||
} |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "dataReadSuccess", |
|
||||||
uri, |
|
||||||
recalledFromMemory: false, |
|
||||||
}; |
|
||||||
} else { |
|
||||||
// Load Blob
|
|
||||||
const blob = await response.blob(); |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "binaryReadSuccess", |
|
||||||
uri, |
|
||||||
recalledFromMemory: false, |
|
||||||
blob, |
|
||||||
mimeType: contentType, |
|
||||||
}; |
|
||||||
} |
|
||||||
} catch (err) { |
|
||||||
return UnexpectedResourceError.fromThrown(uri, err); |
|
||||||
} |
|
||||||
} |
|
@ -1,22 +0,0 @@ |
|||||||
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>; |
|
||||||
} |
|
@ -1,101 +0,0 @@ |
|||||||
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 { LeafUri } from "../../util/uriTypes"; |
|
||||||
import { UnexpectedResourceError } from "../results/error/ErrorResult"; |
|
||||||
import type { HttpErrorResultType } from "../results/error/HttpErrorResult"; |
|
||||||
import { HttpErrorResult } from "../results/error/HttpErrorResult"; |
|
||||||
import type { UpdateSuccess } from "../results/success/UpdateSuccess"; |
|
||||||
import type { DatasetRequestOptions } from "./requestOptions"; |
|
||||||
|
|
||||||
/** |
|
||||||
* All return values for updateDataResource |
|
||||||
*/ |
|
||||||
export type UpdateResult = UpdateSuccess | UpdateResultError; |
|
||||||
|
|
||||||
/** |
|
||||||
* All errors updateDataResource can return |
|
||||||
*/ |
|
||||||
export type UpdateResultError = HttpErrorResultType | UnexpectedResourceError; |
|
||||||
|
|
||||||
/** |
|
||||||
* 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( |
|
||||||
uri: LeafUri, |
|
||||||
datasetChanges: DatasetChanges<Quad>, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<UpdateResult> { |
|
||||||
try { |
|
||||||
// Optimistically add data
|
|
||||||
options?.dataset?.bulk(datasetChanges); |
|
||||||
const fetch = guaranteeFetch(options?.fetch); |
|
||||||
|
|
||||||
// Make request
|
|
||||||
const sparqlUpdate = await changesToSparqlUpdate(datasetChanges); |
|
||||||
const response = await fetch(uri, { |
|
||||||
method: "PATCH", |
|
||||||
body: sparqlUpdate, |
|
||||||
headers: { |
|
||||||
"Content-Type": "application/sparql-update", |
|
||||||
}, |
|
||||||
}); |
|
||||||
const httpError = HttpErrorResult.checkResponse(uri, response); |
|
||||||
if (httpError) { |
|
||||||
// Handle error rollback
|
|
||||||
if (options?.dataset) { |
|
||||||
options.dataset.bulk({ |
|
||||||
added: datasetChanges.removed, |
|
||||||
removed: datasetChanges.added, |
|
||||||
}); |
|
||||||
} |
|
||||||
return httpError; |
|
||||||
} |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "updateSuccess", |
|
||||||
uri, |
|
||||||
}; |
|
||||||
} catch (err) { |
|
||||||
return UnexpectedResourceError.fromThrown(uri, err); |
|
||||||
} |
|
||||||
} |
|
@ -1,121 +0,0 @@ |
|||||||
import { guaranteeFetch } from "../../util/guaranteeFetch"; |
|
||||||
import { |
|
||||||
addResourceRdfToContainer, |
|
||||||
getParentUri, |
|
||||||
getSlug, |
|
||||||
} from "../../util/rdfUtils"; |
|
||||||
import type { LeafUri } from "../../util/uriTypes"; |
|
||||||
import { UnexpectedResourceError } from "../results/error/ErrorResult"; |
|
||||||
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"; |
|
||||||
|
|
||||||
/** |
|
||||||
* 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( |
|
||||||
uri: LeafUri, |
|
||||||
blob: Blob, |
|
||||||
mimeType: string, |
|
||||||
overwrite: true, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<LeafCreateAndOverwriteResult>; |
|
||||||
export function uploadResource( |
|
||||||
uri: LeafUri, |
|
||||||
blob: Blob, |
|
||||||
mimeType: string, |
|
||||||
overwrite?: false, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<LeafCreateIfAbsentResult>; |
|
||||||
export function uploadResource( |
|
||||||
uri: LeafUri, |
|
||||||
blob: Blob, |
|
||||||
mimeType: string, |
|
||||||
overwrite?: boolean, |
|
||||||
options?: DatasetRequestOptions, |
|
||||||
): Promise<LeafCreateIfAbsentResult | LeafCreateAndOverwriteResult>; |
|
||||||
export async function uploadResource( |
|
||||||
uri: LeafUri, |
|
||||||
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(uri, 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(uri, options); |
|
||||||
// If it does exist stop and return.
|
|
||||||
if (readResult.type !== "absentReadSuccess") { |
|
||||||
return readResult; |
|
||||||
} |
|
||||||
} |
|
||||||
// Create the document
|
|
||||||
const parentUri = getParentUri(uri)!; |
|
||||||
const response = await fetch(parentUri, { |
|
||||||
method: "post", |
|
||||||
headers: { |
|
||||||
"content-type": mimeType, |
|
||||||
slug: getSlug(uri), |
|
||||||
}, |
|
||||||
body: blob, |
|
||||||
}); |
|
||||||
|
|
||||||
const httpError = HttpErrorResult.checkResponse(uri, response); |
|
||||||
if (httpError) return httpError; |
|
||||||
|
|
||||||
if (options?.dataset) { |
|
||||||
addResourceRdfToContainer(uri, options.dataset); |
|
||||||
} |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "createSuccess", |
|
||||||
uri, |
|
||||||
didOverwrite, |
|
||||||
}; |
|
||||||
} catch (err) { |
|
||||||
const thing = UnexpectedResourceError.fromThrown(uri, err); |
|
||||||
return thing; |
|
||||||
} |
|
||||||
} |
|
@ -1,7 +0,0 @@ |
|||||||
/** |
|
||||||
* A type returned by all request functions |
|
||||||
*/ |
|
||||||
export interface RequesterResult { |
|
||||||
type: string; |
|
||||||
isError: boolean; |
|
||||||
} |
|
@ -1,18 +0,0 @@ |
|||||||
/* istanbul ignore file */ |
|
||||||
import { ResourceError } from "./ErrorResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* An error: Could not fetch access rules |
|
||||||
*/ |
|
||||||
export class AccessRuleFetchError extends ResourceError { |
|
||||||
readonly type = "accessRuleFetchError" as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param uri - The uri of the resource for which access rules couldn't be |
|
||||||
* fetched |
|
||||||
* @param message - A custom message for the error |
|
||||||
*/ |
|
||||||
constructor(uri: string, message?: string) { |
|
||||||
super(uri, message || `${uri} had trouble fetching access rules.`); |
|
||||||
} |
|
||||||
} |
|
@ -1,125 +0,0 @@ |
|||||||
import type { RequesterResult } from "../RequesterResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* A result indicating that the request failed in some kind of way |
|
||||||
*/ |
|
||||||
export abstract class ErrorResult extends Error implements RequesterResult { |
|
||||||
/** |
|
||||||
* Indicates the specific type of error |
|
||||||
*/ |
|
||||||
abstract type: string; |
|
||||||
|
|
||||||
/** |
|
||||||
* Always true |
|
||||||
*/ |
|
||||||
readonly isError = true as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param message - a custom message for the error |
|
||||||
*/ |
|
||||||
constructor(message?: string) { |
|
||||||
super(message || "An unkown error was encountered."); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* An error for a specific resource |
|
||||||
*/ |
|
||||||
export abstract class ResourceError extends ErrorResult { |
|
||||||
/** |
|
||||||
* The URI of the resource |
|
||||||
*/ |
|
||||||
readonly uri: string; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param uri - The URI of the resource |
|
||||||
* @param message - A custom message for the error |
|
||||||
*/ |
|
||||||
constructor(uri: string, message?: string) { |
|
||||||
super(message || `An unkown error for ${uri}`); |
|
||||||
this.uri = uri; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* An error that aggregates many errors |
|
||||||
*/ |
|
||||||
export class AggregateError<ErrorType extends ErrorResult> extends ErrorResult { |
|
||||||
readonly type = "aggregateError" as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* A list of all errors returned |
|
||||||
*/ |
|
||||||
readonly errors: ErrorType[]; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param errors - List of all errors returned |
|
||||||
* @param message - A custom message for the error |
|
||||||
*/ |
|
||||||
constructor( |
|
||||||
errors: (ErrorType | AggregateError<ErrorType>)[], |
|
||||||
message?: string, |
|
||||||
) { |
|
||||||
const allErrors: ErrorType[] = []; |
|
||||||
errors.forEach((error) => { |
|
||||||
if (error instanceof AggregateError) { |
|
||||||
error.errors.forEach((subError) => { |
|
||||||
allErrors.push(subError); |
|
||||||
}); |
|
||||||
} else { |
|
||||||
allErrors.push(error); |
|
||||||
} |
|
||||||
}); |
|
||||||
super( |
|
||||||
message || |
|
||||||
`Encountered multiple errors:${allErrors.reduce( |
|
||||||
(agg, cur) => `${agg}\n${cur}`, |
|
||||||
"", |
|
||||||
)}`,
|
|
||||||
); |
|
||||||
this.errors = allErrors; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Represents some error that isn't handled under other errors. This is usually |
|
||||||
* returned when something threw an error that LDO did not expect. |
|
||||||
*/ |
|
||||||
export class UnexpectedResourceError extends ResourceError { |
|
||||||
readonly type = "unexpectedResourceError" as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* The error that was thrown |
|
||||||
*/ |
|
||||||
error: Error; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param uri - URI of the resource |
|
||||||
* @param error - The error that was thrown |
|
||||||
*/ |
|
||||||
constructor(uri: string, error: Error) { |
|
||||||
super(uri, error.message); |
|
||||||
this.error = error; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* |
|
||||||
* Creates an UnexpectedResourceError from a thrown error |
|
||||||
* @param uri - The URI of the resource |
|
||||||
* @param err - The thrown error |
|
||||||
* @returns an UnexpectedResourceError |
|
||||||
*/ |
|
||||||
static fromThrown(uri: string, err: unknown) { |
|
||||||
if (err instanceof Error) { |
|
||||||
return new UnexpectedResourceError(uri, err); |
|
||||||
} else if (typeof err === "string") { |
|
||||||
return new UnexpectedResourceError(uri, new Error(err)); |
|
||||||
} else { |
|
||||||
return new UnexpectedResourceError( |
|
||||||
uri, |
|
||||||
new Error(`Error of type ${typeof err} thrown: ${err}`), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,160 +0,0 @@ |
|||||||
import { ResourceError } from "./ErrorResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* A set of standard errors that can be returned as a result of an HTTP request |
|
||||||
*/ |
|
||||||
export type HttpErrorResultType = |
|
||||||
| ServerHttpError |
|
||||||
| UnexpectedHttpError |
|
||||||
| UnauthenticatedHttpError |
|
||||||
| UnauthorizedHttpError; |
|
||||||
|
|
||||||
/** |
|
||||||
* An error caused by an HTTP request |
|
||||||
*/ |
|
||||||
export abstract class HttpErrorResult extends ResourceError { |
|
||||||
/** |
|
||||||
* 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 uri - URI of the resource |
|
||||||
* @param response - The response returned by the HTTP requests |
|
||||||
* @param message - A custom message for the error |
|
||||||
*/ |
|
||||||
constructor(uri: string, response: Response, message?: string) { |
|
||||||
super( |
|
||||||
uri, |
|
||||||
message || |
|
||||||
`Request for ${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(uri: string, response: Response) { |
|
||||||
if (ServerHttpError.is(response)) { |
|
||||||
return new ServerHttpError(uri, response); |
|
||||||
} |
|
||||||
if (UnauthenticatedHttpError.is(response)) { |
|
||||||
return new UnauthenticatedHttpError(uri, response); |
|
||||||
} |
|
||||||
if (UnauthorizedHttpError.is(response)) { |
|
||||||
return new UnauthorizedHttpError(uri, response); |
|
||||||
} |
|
||||||
if (HttpErrorResult.isnt(response)) { |
|
||||||
return new UnexpectedHttpError(uri, 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 extends HttpErrorResult { |
|
||||||
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 extends HttpErrorResult { |
|
||||||
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 extends HttpErrorResult { |
|
||||||
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 extends HttpErrorResult { |
|
||||||
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 extends HttpErrorResult { |
|
||||||
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; |
|
||||||
} |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
import { ResourceError } from "./ErrorResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* An InvalidUriError is returned when a URI was provided that is not a valid |
|
||||||
* URI. |
|
||||||
*/ |
|
||||||
export class InvalidUriError extends ResourceError { |
|
||||||
readonly type = "invalidUriError" as const; |
|
||||||
|
|
||||||
constructor(uri: string, message?: string) { |
|
||||||
super(uri, message || `${uri} is an invalid uri.`); |
|
||||||
} |
|
||||||
} |
|
@ -1,17 +0,0 @@ |
|||||||
import { ResourceError } from "./ErrorResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* A NoncompliantPodError is returned when the server responded in a way that is |
|
||||||
* not compliant with the Solid specification. |
|
||||||
*/ |
|
||||||
export class NoRootContainerError extends ResourceError { |
|
||||||
readonly type = "noRootContainerError" as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param uri - the URI of the requested resource |
|
||||||
* @param message - a custom message for the error |
|
||||||
*/ |
|
||||||
constructor(uri: string) { |
|
||||||
super(uri, `${uri} has not root container.`); |
|
||||||
} |
|
||||||
} |
|
@ -1,20 +0,0 @@ |
|||||||
import { ResourceError } from "./ErrorResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* A NoncompliantPodError is returned when the server responded in a way that is |
|
||||||
* not compliant with the Solid specification. |
|
||||||
*/ |
|
||||||
export class NoncompliantPodError extends ResourceError { |
|
||||||
readonly type = "noncompliantPodError" as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param uri - the URI of the requested resource |
|
||||||
* @param message - a custom message for the error |
|
||||||
*/ |
|
||||||
constructor(uri: string, message?: string) { |
|
||||||
super( |
|
||||||
uri, |
|
||||||
`Response from ${uri} is not compliant with the Solid Specification: ${message}`, |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
@ -1,19 +0,0 @@ |
|||||||
import type { Container } from "../../../resource/Container"; |
|
||||||
import type { ResourceSuccess, SuccessResult } from "./SuccessResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that the request to check if a resource is the root container was |
|
||||||
* a success. |
|
||||||
*/ |
|
||||||
export interface CheckRootContainerSuccess extends ResourceSuccess { |
|
||||||
type: "checkRootContainerSuccess"; |
|
||||||
/** |
|
||||||
* True if this resoure is the root container |
|
||||||
*/ |
|
||||||
isRootContainer: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
export interface GetStorageContainerFromWebIdSuccess extends SuccessResult { |
|
||||||
type: "getStorageContainerFromWebIdSuccess"; |
|
||||||
storageContainers: Container[]; |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
import type { ResourceSuccess } from "./SuccessResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that the request to create the resource was a success. |
|
||||||
*/ |
|
||||||
export interface CreateSuccess extends ResourceSuccess { |
|
||||||
type: "createSuccess"; |
|
||||||
/** |
|
||||||
* True if there was a resource that existed before at the given URI that was |
|
||||||
* overwritten |
|
||||||
*/ |
|
||||||
didOverwrite: boolean; |
|
||||||
} |
|
@ -1,14 +0,0 @@ |
|||||||
import type { ResourceSuccess } from "./SuccessResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that the request to delete a resource was a success. |
|
||||||
*/ |
|
||||||
export interface DeleteSuccess extends ResourceSuccess { |
|
||||||
type: "deleteSuccess"; |
|
||||||
|
|
||||||
/** |
|
||||||
* True if there was a resource at the provided URI that was deleted. False if |
|
||||||
* a resource didn't exist. |
|
||||||
*/ |
|
||||||
resourceExisted: boolean; |
|
||||||
} |
|
@ -1,71 +0,0 @@ |
|||||||
import type { ResourceSuccess, SuccessResult } from "./SuccessResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that the request to read a resource was a success |
|
||||||
*/ |
|
||||||
export interface ReadSuccess extends ResourceSuccess { |
|
||||||
/** |
|
||||||
* True if the resource was recalled from local memory rather than a recent |
|
||||||
* request |
|
||||||
*/ |
|
||||||
recalledFromMemory: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that the read request was successful and that the resource |
|
||||||
* retrieved was a binary resource. |
|
||||||
*/ |
|
||||||
export interface BinaryReadSuccess extends ReadSuccess { |
|
||||||
type: "binaryReadSuccess"; |
|
||||||
/** |
|
||||||
* The raw data for the binary resource |
|
||||||
*/ |
|
||||||
blob: Blob; |
|
||||||
/** |
|
||||||
* The mime type of the binary resource |
|
||||||
*/ |
|
||||||
mimeType: string; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that the read request was successful and that the resource |
|
||||||
* retrieved was a data (RDF) resource. |
|
||||||
*/ |
|
||||||
export interface DataReadSuccess extends ReadSuccess { |
|
||||||
type: "dataReadSuccess"; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that the read request was successful and that the resource |
|
||||||
* retrieved was a container resource. |
|
||||||
*/ |
|
||||||
export interface ContainerReadSuccess extends ReadSuccess { |
|
||||||
type: "containerReadSuccess"; |
|
||||||
/** |
|
||||||
* True if this container is a root container |
|
||||||
*/ |
|
||||||
isRootContainer: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that the read request was successful, but no resource exists at |
|
||||||
* the provided URI. |
|
||||||
*/ |
|
||||||
export interface AbsentReadSuccess extends ReadSuccess { |
|
||||||
type: "absentReadSuccess"; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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(result: SuccessResult): result is ReadSuccess { |
|
||||||
return ( |
|
||||||
result.type === "binaryReadSuccess" || |
|
||||||
result.type === "dataReadSuccess" || |
|
||||||
result.type === "absentReadSuccess" || |
|
||||||
result.type === "containerReadSuccess" |
|
||||||
); |
|
||||||
} |
|
@ -1,31 +0,0 @@ |
|||||||
import type { RequesterResult } from "../RequesterResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that some action taken by LDO was a success |
|
||||||
*/ |
|
||||||
export interface SuccessResult extends RequesterResult { |
|
||||||
isError: false; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that a request to a resource was aa success |
|
||||||
*/ |
|
||||||
export interface ResourceSuccess extends SuccessResult { |
|
||||||
/** |
|
||||||
* The URI of the resource |
|
||||||
*/ |
|
||||||
uri: string; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* A grouping of multiple successes as a result of an action |
|
||||||
*/ |
|
||||||
export interface AggregateSuccess<SuccessType extends SuccessResult> |
|
||||||
extends SuccessResult { |
|
||||||
type: "aggregateSuccess"; |
|
||||||
|
|
||||||
/** |
|
||||||
* An array of all successesses |
|
||||||
*/ |
|
||||||
results: SuccessType[]; |
|
||||||
} |
|
@ -1,8 +0,0 @@ |
|||||||
import type { ResourceSuccess } from "./SuccessResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that a specific resource is unfetched |
|
||||||
*/ |
|
||||||
export interface Unfetched extends ResourceSuccess { |
|
||||||
type: "unfetched"; |
|
||||||
} |
|
@ -1,24 +0,0 @@ |
|||||||
import type { ResourceSuccess } from "./SuccessResult"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that an update request to a resource was successful |
|
||||||
*/ |
|
||||||
export interface UpdateSuccess extends ResourceSuccess { |
|
||||||
type: "updateSuccess"; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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 interface UpdateDefaultGraphSuccess extends ResourceSuccess { |
|
||||||
type: "updateDefaultGraphSuccess"; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that LDO ignored an invalid update (usually because a container |
|
||||||
* attempted an update) |
|
||||||
*/ |
|
||||||
export interface IgnoredInvalidUpdateSuccess extends ResourceSuccess { |
|
||||||
type: "ignoredInvalidUpdateSuccess"; |
|
||||||
} |
|
@ -1,26 +0,0 @@ |
|||||||
/* 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,599 +0,0 @@ |
|||||||
import { namedNode } from "@rdfjs/data-model"; |
|
||||||
import { ContainerBatchedRequester } from "../requester/ContainerBatchedRequester"; |
|
||||||
import type { |
|
||||||
CheckRootResult, |
|
||||||
CheckRootResultError, |
|
||||||
} from "../requester/requests/checkRootContainer"; |
|
||||||
import type { |
|
||||||
ContainerCreateAndOverwriteResult, |
|
||||||
ContainerCreateIfAbsentResult, |
|
||||||
LeafCreateAndOverwriteResult, |
|
||||||
LeafCreateIfAbsentResult, |
|
||||||
} from "../requester/requests/createDataResource"; |
|
||||||
import type { |
|
||||||
DeleteResult, |
|
||||||
DeleteResultError, |
|
||||||
} from "../requester/requests/deleteResource"; |
|
||||||
import type { |
|
||||||
ReadContainerResult, |
|
||||||
ReadResultError, |
|
||||||
} from "../requester/requests/readResource"; |
|
||||||
import { AggregateError } from "../requester/results/error/ErrorResult"; |
|
||||||
import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; |
|
||||||
import type { AbsentReadSuccess } from "../requester/results/success/ReadSuccess"; |
|
||||||
import type { ContainerReadSuccess } from "../requester/results/success/ReadSuccess"; |
|
||||||
import type { AggregateSuccess } from "../requester/results/success/SuccessResult"; |
|
||||||
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; |
|
||||||
import { getParentUri, ldpContains } from "../util/rdfUtils"; |
|
||||||
import type { ContainerUri, LeafUri } from "../util/uriTypes"; |
|
||||||
import type { Leaf } from "./Leaf"; |
|
||||||
import type { SharedStatuses } from "./Resource"; |
|
||||||
import { Resource } from "./Resource"; |
|
||||||
import type { ResourceResult } from "./resourceResult/ResourceResult"; |
|
||||||
import { NoRootContainerError } from "../requester/results/error/NoRootContainerError"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Represents the current status of a specific container on a Pod as known by |
|
||||||
* LDO. |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const container = solidLdoDataset |
|
||||||
* .getResource("https://example.com/container/"); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
export class Container extends Resource { |
|
||||||
/** |
|
||||||
* The URI of the container |
|
||||||
*/ |
|
||||||
readonly uri: ContainerUri; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Batched Requester for the Container |
|
||||||
*/ |
|
||||||
protected requester: ContainerBatchedRequester; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* True if this is the root container, false if not, undefined if unknown |
|
||||||
*/ |
|
||||||
protected rootContainer: boolean | undefined; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that this resource is a container resource |
|
||||||
*/ |
|
||||||
readonly type = "container" as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that this resource is not an error |
|
||||||
*/ |
|
||||||
readonly isError = false as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* The status of the last request made for this container |
|
||||||
*/ |
|
||||||
status: |
|
||||||
| SharedStatuses |
|
||||||
| ReadContainerResult |
|
||||||
| ContainerCreateAndOverwriteResult |
|
||||||
| ContainerCreateIfAbsentResult |
|
||||||
| CheckRootResult; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param uri - The uri of the container |
|
||||||
* @param context - SolidLdoDatasetContext for the parent dataset |
|
||||||
*/ |
|
||||||
constructor(uri: ContainerUri, context: SolidLdoDatasetContext) { |
|
||||||
super(context); |
|
||||||
this.uri = uri; |
|
||||||
this.requester = new ContainerBatchedRequester(uri, context); |
|
||||||
this.status = { isError: false, type: "unfetched", uri }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Checks if this container is a root container |
|
||||||
* @returns true if this container is a root container, false if not, and |
|
||||||
* undefined if this is unknown at the moment. |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* // Returns "undefined" when the container is unfetched
|
|
||||||
* console.log(container.isRootContainer()); |
|
||||||
* const result = await container.read(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Returns true or false
|
|
||||||
* console.log(container.isRootContainer()); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
isRootContainer(): boolean | undefined { |
|
||||||
return this.rootContainer; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* READ METHODS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* A helper method updates this container's internal state upon read success |
|
||||||
* @param result - the result of the read success |
|
||||||
*/ |
|
||||||
protected updateWithReadSuccess( |
|
||||||
result: ContainerReadSuccess | AbsentReadSuccess, |
|
||||||
): void { |
|
||||||
super.updateWithReadSuccess(result); |
|
||||||
if (result.type === "containerReadSuccess") { |
|
||||||
this.rootContainer = result.isRootContainer; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Reads the container |
|
||||||
* @returns A read result |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await container.read(); |
|
||||||
* if (result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async read(): Promise<ResourceResult<ReadContainerResult, Container>> { |
|
||||||
const result = (await this.handleRead()) as ReadContainerResult; |
|
||||||
if (result.isError) return result; |
|
||||||
return { ...result, resource: this }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Converts the current state of this container to a readResult |
|
||||||
* @returns a ReadContainerResult |
|
||||||
*/ |
|
||||||
protected toReadResult(): ResourceResult<ReadContainerResult, Container> { |
|
||||||
if (this.isAbsent()) { |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "absentReadSuccess", |
|
||||||
uri: this.uri, |
|
||||||
recalledFromMemory: true, |
|
||||||
resource: this, |
|
||||||
}; |
|
||||||
} else { |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "containerReadSuccess", |
|
||||||
uri: this.uri, |
|
||||||
recalledFromMemory: true, |
|
||||||
isRootContainer: this.isRootContainer()!, |
|
||||||
resource: this, |
|
||||||
}; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Makes a request to read this container if it hasn't been fetched yet. If it |
|
||||||
* has, return the cached informtation |
|
||||||
* @returns a ReadContainerResult |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await container.read(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Will execute without making a request
|
|
||||||
* const result2 = await container.readIfUnfetched(); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async readIfUnfetched(): Promise< |
|
||||||
ResourceResult<ReadContainerResult, Container> |
|
||||||
> { |
|
||||||
return super.readIfUnfetched() as Promise< |
|
||||||
ResourceResult<ReadContainerResult, Container> |
|
||||||
>; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* PARENT CONTAINER METHODS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Checks if this container is a root container by making a request |
|
||||||
* @returns CheckRootResult |
|
||||||
*/ |
|
||||||
private async checkIfIsRootContainer(): Promise< |
|
||||||
ResourceResult<CheckRootResult, Container> |
|
||||||
> { |
|
||||||
const rootContainerResult = await this.requester.isRootContainer(); |
|
||||||
this.status = rootContainerResult; |
|
||||||
if (rootContainerResult.isError) return rootContainerResult; |
|
||||||
this.rootContainer = rootContainerResult.isRootContainer; |
|
||||||
this.emit("update"); |
|
||||||
return { ...rootContainerResult, resource: this }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Gets the root container of this container. If this container is the root |
|
||||||
* container, this function returns itself. |
|
||||||
* @returns The root container for this container or undefined if there is no |
|
||||||
* root container. |
|
||||||
* |
|
||||||
* @example |
|
||||||
* Suppose the root container is at `https://example.com/` |
|
||||||
* |
|
||||||
* ```typescript
|
|
||||||
* const container = ldoSolidDataset |
|
||||||
* .getResource("https://example.com/container/"); |
|
||||||
* const rootContainer = await container.getRootContainer(); |
|
||||||
* if (!rootContainer.isError) { |
|
||||||
* // logs "https://example.com/"
|
|
||||||
* console.log(rootContainer.uri); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async getRootContainer(): Promise< |
|
||||||
Container | CheckRootResultError | NoRootContainerError |
|
||||||
> { |
|
||||||
const parentContainerResult = await this.getParentContainer(); |
|
||||||
if (parentContainerResult?.isError) return parentContainerResult; |
|
||||||
if (!parentContainerResult) { |
|
||||||
return this.isRootContainer() ? this : new NoRootContainerError(this.uri); |
|
||||||
} |
|
||||||
return parentContainerResult.getRootContainer(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Gets the parent container for this container by making a request |
|
||||||
* @returns The parent container or undefined if there is no parent container |
|
||||||
* because this container is the root container |
|
||||||
* |
|
||||||
* @example |
|
||||||
* Suppose the root container is at `https://example.com/` |
|
||||||
* |
|
||||||
* ```typescript
|
|
||||||
* const root = solidLdoDataset.getResource("https://example.com/"); |
|
||||||
* const container = solidLdoDataset |
|
||||||
* .getResource("https://example.com/container"); |
|
||||||
* const rootParent = await root.getParentContainer(); |
|
||||||
* console.log(rootParent); // Logs "undefined"
|
|
||||||
* const containerParent = await container.getParentContainer(); |
|
||||||
* if (!containerParent.isError) { |
|
||||||
* // Logs "https://example.com/"
|
|
||||||
* console.log(containerParent.uri); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async getParentContainer(): Promise< |
|
||||||
Container | CheckRootResultError | undefined |
|
||||||
> { |
|
||||||
if (this.rootContainer === undefined) { |
|
||||||
const checkResult = await this.checkIfIsRootContainer(); |
|
||||||
if (checkResult.isError) return checkResult; |
|
||||||
} |
|
||||||
if (this.rootContainer) return undefined; |
|
||||||
const parentUri = getParentUri(this.uri); |
|
||||||
if (!parentUri) { |
|
||||||
return undefined; |
|
||||||
} |
|
||||||
return this.context.resourceStore.get(parentUri); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Lists the currently cached children of this container (no request is made) |
|
||||||
* @returns An array of children |
|
||||||
* |
|
||||||
* ```typescript
|
|
||||||
* const result = await container.read(); |
|
||||||
* if (!result.isError) { |
|
||||||
* const children = container.children(); |
|
||||||
* children.forEach((child) => { |
|
||||||
* console.log(child.uri); |
|
||||||
* }); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
children(): (Leaf | Container)[] { |
|
||||||
const childQuads = this.context.solidLdoDataset.match( |
|
||||||
namedNode(this.uri), |
|
||||||
ldpContains, |
|
||||||
null, |
|
||||||
namedNode(this.uri), |
|
||||||
); |
|
||||||
return childQuads.toArray().map((childQuad) => { |
|
||||||
return this.context.resourceStore.get(childQuad.object.value); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns a child resource with a given name (slug) |
|
||||||
* @param slug - the given name for that child resource |
|
||||||
* @returns the child resource (either a Leaf or Container depending on the |
|
||||||
* name) |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const root = solidLdoDataset.getResource("https://example.com/"); |
|
||||||
* const container = solidLdoDataset.child("container/"); |
|
||||||
* // Logs "https://example.com/container/"
|
|
||||||
* console.log(container.uri); |
|
||||||
* const resource = container.child("resource.ttl"); |
|
||||||
* // Logs "https://example.com/container/resource.ttl"
|
|
||||||
* console.log(resource.uri); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
child(slug: ContainerUri): Container; |
|
||||||
child(slug: LeafUri): Leaf; |
|
||||||
child(slug: string): Leaf | Container; |
|
||||||
child(slug: string): Leaf | Container { |
|
||||||
return this.context.resourceStore.get(`${this.uri}${slug}`); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* CHILD CREATORS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a resource and overwrites any existing resource that existed at the |
|
||||||
* URI |
|
||||||
* |
|
||||||
* @param slug - the name of the resource |
|
||||||
* @return the result of creating that resource |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const container = solidLdoDataset |
|
||||||
* .getResource("https://example.com/container/"); |
|
||||||
* cosnt result = await container.createChildAndOverwrite("resource.ttl"); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
createChildAndOverwrite( |
|
||||||
slug: ContainerUri, |
|
||||||
): Promise<ResourceResult<ContainerCreateAndOverwriteResult, Container>>; |
|
||||||
createChildAndOverwrite( |
|
||||||
slug: LeafUri, |
|
||||||
): Promise<ResourceResult<LeafCreateAndOverwriteResult, Leaf>>; |
|
||||||
createChildAndOverwrite( |
|
||||||
slug: string, |
|
||||||
): Promise< |
|
||||||
ResourceResult< |
|
||||||
ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult, |
|
||||||
Leaf | Container |
|
||||||
> |
|
||||||
>; |
|
||||||
createChildAndOverwrite( |
|
||||||
slug: string, |
|
||||||
): Promise< |
|
||||||
ResourceResult< |
|
||||||
ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult, |
|
||||||
Leaf | Container |
|
||||||
> |
|
||||||
> { |
|
||||||
return this.child(slug).createAndOverwrite(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a resource only if that resource doesn't already exist on the Solid |
|
||||||
* Pod |
|
||||||
* |
|
||||||
* @param slug - the name of the resource |
|
||||||
* @return the result of creating that resource |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const container = solidLdoDataset |
|
||||||
* .getResource("https://example.com/container/"); |
|
||||||
* cosnt result = await container.createChildIfAbsent("resource.ttl"); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
createChildIfAbsent( |
|
||||||
slug: ContainerUri, |
|
||||||
): Promise<ResourceResult<ContainerCreateIfAbsentResult, Container>>; |
|
||||||
createChildIfAbsent( |
|
||||||
slug: LeafUri, |
|
||||||
): Promise<ResourceResult<LeafCreateIfAbsentResult, Leaf>>; |
|
||||||
createChildIfAbsent( |
|
||||||
slug: string, |
|
||||||
): Promise< |
|
||||||
ResourceResult< |
|
||||||
ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult, |
|
||||||
Leaf | Container |
|
||||||
> |
|
||||||
>; |
|
||||||
createChildIfAbsent( |
|
||||||
slug: string, |
|
||||||
): Promise< |
|
||||||
ResourceResult< |
|
||||||
ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult, |
|
||||||
Leaf | Container |
|
||||||
> |
|
||||||
> { |
|
||||||
return this.child(slug).createIfAbsent(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a new binary resource and overwrites any existing resource that |
|
||||||
* existed at the URI |
|
||||||
* |
|
||||||
* @param slug - the name of the resource |
|
||||||
* @return the result of creating that resource |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const container = solidLdoDataset |
|
||||||
* .getResource("https://example.com/container/"); |
|
||||||
* cosnt result = await container.uploadChildAndOverwrite( |
|
||||||
* "resource.txt", |
|
||||||
* new Blob("some text."), |
|
||||||
* "text/txt", |
|
||||||
* ); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async uploadChildAndOverwrite( |
|
||||||
slug: LeafUri, |
|
||||||
blob: Blob, |
|
||||||
mimeType: string, |
|
||||||
): Promise<ResourceResult<LeafCreateAndOverwriteResult, Leaf>> { |
|
||||||
return this.child(slug).uploadAndOverwrite(blob, mimeType); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a new binary resource and overwrites any existing resource that |
|
||||||
* existed at the URI |
|
||||||
* |
|
||||||
* @param slug - the name of the resource |
|
||||||
* @return the result of creating that resource |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const container = solidLdoDataset |
|
||||||
* .getResource("https://example.com/container/"); |
|
||||||
* cosnt result = await container.uploadChildIfAbsent( |
|
||||||
* "resource.txt", |
|
||||||
* new Blob("some text."), |
|
||||||
* "text/txt", |
|
||||||
* ); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async uploadChildIfAbsent( |
|
||||||
slug: LeafUri, |
|
||||||
blob: Blob, |
|
||||||
mimeType: string, |
|
||||||
): Promise<ResourceResult<LeafCreateIfAbsentResult, Leaf>> { |
|
||||||
return this.child(slug).uploadIfAbsent(blob, mimeType); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Deletes all contents in this container |
|
||||||
* @returns An AggregateSuccess or Aggregate error corresponding with all the |
|
||||||
* deleted resources |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = container.clear(); |
|
||||||
* if (!result.isError) { |
|
||||||
* console.log("All deleted resources:"); |
|
||||||
* result.results.forEach((result) => console.log(result.uri)); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async clear(): Promise< |
|
||||||
ResourceResult< |
|
||||||
| AggregateSuccess<ResourceResult<DeleteSuccess, Container | Leaf>> |
|
||||||
| AggregateError<DeleteResultError | ReadResultError>, |
|
||||||
Container |
|
||||||
> |
|
||||||
> { |
|
||||||
const readResult = await this.read(); |
|
||||||
if (readResult.isError) return new AggregateError([readResult]); |
|
||||||
const results = ( |
|
||||||
await Promise.all( |
|
||||||
this.children().map(async (child) => { |
|
||||||
return child.delete(); |
|
||||||
}), |
|
||||||
) |
|
||||||
).flat(); |
|
||||||
const errors = results.filter( |
|
||||||
( |
|
||||||
value, |
|
||||||
): value is |
|
||||||
| DeleteResultError |
|
||||||
| AggregateError<DeleteResultError | ReadResultError> => value.isError, |
|
||||||
); |
|
||||||
if (errors.length > 0) { |
|
||||||
return new AggregateError(errors); |
|
||||||
} |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "aggregateSuccess", |
|
||||||
results: results as ResourceResult<DeleteSuccess, Container | Leaf>[], |
|
||||||
resource: this, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Deletes this container and all its contents |
|
||||||
* @returns A Delete result for this container |
|
||||||
* |
|
||||||
* ```typescript
|
|
||||||
* const result = await container.delete(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async delete(): Promise< |
|
||||||
ResourceResult< |
|
||||||
DeleteResult | AggregateError<DeleteResultError | ReadResultError>, |
|
||||||
Container |
|
||||||
> |
|
||||||
> { |
|
||||||
const clearResult = await this.clear(); |
|
||||||
if (clearResult.isError) return clearResult; |
|
||||||
const deleteResult = await this.handleDelete(); |
|
||||||
if (deleteResult.isError) return deleteResult; |
|
||||||
return { ...deleteResult, resource: this }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a container at this URI and overwrites any that already exists |
|
||||||
* @returns ContainerCreateAndOverwriteResult |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await container.createAndOverwrite(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async createAndOverwrite(): Promise< |
|
||||||
ResourceResult<ContainerCreateAndOverwriteResult, Container> |
|
||||||
> { |
|
||||||
const createResult = |
|
||||||
(await this.handleCreateAndOverwrite()) as ContainerCreateAndOverwriteResult; |
|
||||||
if (createResult.isError) return createResult; |
|
||||||
return { ...createResult, resource: this }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a container at this URI if the container doesn't already exist |
|
||||||
* @returns ContainerCreateIfAbsentResult |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await container.createIfAbsent(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async createIfAbsent(): Promise< |
|
||||||
ResourceResult<ContainerCreateIfAbsentResult, Container> |
|
||||||
> { |
|
||||||
const createResult = |
|
||||||
(await this.handleCreateIfAbsent()) as ContainerCreateIfAbsentResult; |
|
||||||
if (createResult.isError) return createResult; |
|
||||||
return { ...createResult, resource: this }; |
|
||||||
} |
|
||||||
} |
|
@ -1,559 +0,0 @@ |
|||||||
import type { DatasetChanges } from "@ldo/rdf-utils"; |
|
||||||
import type { Quad } from "@rdfjs/types"; |
|
||||||
import { LeafBatchedRequester } from "../requester/LeafBatchedRequester"; |
|
||||||
import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; |
|
||||||
import type { |
|
||||||
LeafCreateAndOverwriteResult, |
|
||||||
LeafCreateIfAbsentResult, |
|
||||||
} from "../requester/requests/createDataResource"; |
|
||||||
import type { DeleteResult } from "../requester/requests/deleteResource"; |
|
||||||
import type { ReadLeafResult } from "../requester/requests/readResource"; |
|
||||||
import type { UpdateResult } from "../requester/requests/updateDataResource"; |
|
||||||
import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; |
|
||||||
import type { AbsentReadSuccess } from "../requester/results/success/ReadSuccess"; |
|
||||||
import type { |
|
||||||
BinaryReadSuccess, |
|
||||||
DataReadSuccess, |
|
||||||
} from "../requester/results/success/ReadSuccess"; |
|
||||||
import type { ResourceSuccess } from "../requester/results/success/SuccessResult"; |
|
||||||
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; |
|
||||||
import { getParentUri } from "../util/rdfUtils"; |
|
||||||
import type { LeafUri } from "../util/uriTypes"; |
|
||||||
import type { Container } from "./Container"; |
|
||||||
import type { SharedStatuses } from "./Resource"; |
|
||||||
import { Resource } from "./Resource"; |
|
||||||
import type { ResourceResult } from "./resourceResult/ResourceResult"; |
|
||||||
import type { NoRootContainerError } from "../requester/results/error/NoRootContainerError"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Represents the current status of a specific Leaf on a Pod as known by LDO. |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const leaf = solidLdoDataset |
|
||||||
* .getResource("https://example.com/container/resource.ttl"); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
export class Leaf extends Resource { |
|
||||||
/** |
|
||||||
* The URI of the leaf |
|
||||||
*/ |
|
||||||
readonly uri: LeafUri; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Batched Requester for the Leaf |
|
||||||
*/ |
|
||||||
protected requester: LeafBatchedRequester; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that this resource is a leaf resource |
|
||||||
*/ |
|
||||||
readonly type = "leaf" as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* Indicates that this resource is not an error |
|
||||||
*/ |
|
||||||
readonly isError = false as const; |
|
||||||
|
|
||||||
/** |
|
||||||
* The status of the last request made for this leaf |
|
||||||
*/ |
|
||||||
status: |
|
||||||
| SharedStatuses |
|
||||||
| ReadLeafResult |
|
||||||
| LeafCreateAndOverwriteResult |
|
||||||
| LeafCreateIfAbsentResult |
|
||||||
| UpdateResult; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* The raw binary data if this leaf is a Binary resource |
|
||||||
*/ |
|
||||||
protected binaryData: { blob: Blob; mimeType: string } | undefined; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param uri - The uri of the leaf |
|
||||||
* @param context - SolidLdoDatasetContext for the parent dataset |
|
||||||
*/ |
|
||||||
constructor(uri: LeafUri, context: SolidLdoDatasetContext) { |
|
||||||
super(context); |
|
||||||
const uriObject = new URL(uri); |
|
||||||
uriObject.hash = ""; |
|
||||||
this.uri = uriObject.toString() as LeafUri; |
|
||||||
this.requester = new LeafBatchedRequester(uri, context); |
|
||||||
this.status = { isError: false, type: "unfetched", uri }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* GETTERS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Checks to see if the resource is currently uploading data |
|
||||||
* @returns true if the current resource is uploading |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* leaf.uploadAndOverwrite(new Blob("some text"), "text/txt").then(() => { |
|
||||||
* // Logs "false"
|
|
||||||
* console.log(leaf.isUploading()) |
|
||||||
* }); |
|
||||||
* // Logs "true"
|
|
||||||
* console.log(leaf.isUploading()); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
isUploading(): boolean { |
|
||||||
return this.requester.isUploading(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Checks to see if the resource is currently updating data |
|
||||||
* @returns true if the current resource is updating |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* leaf.update(datasetChanges).then(() => { |
|
||||||
* // Logs "false"
|
|
||||||
* console.log(leaf.isUpdating()) |
|
||||||
* }); |
|
||||||
* // Logs "true"
|
|
||||||
* console.log(leaf.isUpdating()); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
isUpdating(): boolean { |
|
||||||
return this.requester.isUpdating(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* If this resource is a binary resource, returns the mime type |
|
||||||
* @returns The mime type if this resource is a binary resource, undefined |
|
||||||
* otherwise |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* // Logs "text/txt"
|
|
||||||
* console.log(leaf.getMimeType()); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
getMimeType(): string | undefined { |
|
||||||
return this.binaryData?.mimeType; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* If this resource is a binary resource, returns the Blob |
|
||||||
* @returns The Blob if this resource is a binary resource, undefined |
|
||||||
* otherwise |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* // Logs "some text."
|
|
||||||
* console.log(leaf.getBlob()?.toString()); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
getBlob(): Blob | undefined { |
|
||||||
return this.binaryData?.blob; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Check if this resource is a binary resource |
|
||||||
* @returns True if this resource is a binary resource, false if not, |
|
||||||
* undefined if unknown |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* // Logs "undefined"
|
|
||||||
* console.log(leaf.isBinary()); |
|
||||||
* const result = await leaf.read(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Logs "true"
|
|
||||||
* console.log(leaf.isBinary()); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
isBinary(): boolean | undefined { |
|
||||||
if (!this.didInitialFetch) { |
|
||||||
return undefined; |
|
||||||
} |
|
||||||
return !!this.binaryData; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Check if this resource is a data (RDF) resource |
|
||||||
* @returns True if this resource is a data resource, false if not, undefined |
|
||||||
* if unknown |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* // Logs "undefined"
|
|
||||||
* console.log(leaf.isDataResource()); |
|
||||||
* const result = await leaf.read(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Logs "true"
|
|
||||||
* console.log(leaf.isDataResource()); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
isDataResource(): boolean | undefined { |
|
||||||
if (!this.didInitialFetch) { |
|
||||||
return undefined; |
|
||||||
} |
|
||||||
return !this.binaryData; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* READ METHODS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* A helper method updates this leaf's internal state upon read success |
|
||||||
* @param result - the result of the read success |
|
||||||
*/ |
|
||||||
protected updateWithReadSuccess( |
|
||||||
result: BinaryReadSuccess | DataReadSuccess | AbsentReadSuccess, |
|
||||||
): void { |
|
||||||
super.updateWithReadSuccess(result); |
|
||||||
if (result.type === "binaryReadSuccess") { |
|
||||||
this.binaryData = { blob: result.blob, mimeType: result.mimeType }; |
|
||||||
} else { |
|
||||||
this.binaryData = undefined; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Reads the leaf by making a request |
|
||||||
* @returns A read result |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await leaf.read(); |
|
||||||
* if (result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async read(): Promise<ResourceResult<ReadLeafResult, Leaf>> { |
|
||||||
const result = (await this.handleRead()) as ReadLeafResult; |
|
||||||
if (result.isError) return result; |
|
||||||
return { ...result, resource: this }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Converts the current state of this leaf to a readResult |
|
||||||
* @returns a ReadLeafResult |
|
||||||
*/ |
|
||||||
protected toReadResult(): ResourceResult<ReadLeafResult, Leaf> { |
|
||||||
if (this.isAbsent()) { |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "absentReadSuccess", |
|
||||||
uri: this.uri, |
|
||||||
recalledFromMemory: true, |
|
||||||
resource: this, |
|
||||||
}; |
|
||||||
} else if (this.isBinary()) { |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "binaryReadSuccess", |
|
||||||
uri: this.uri, |
|
||||||
recalledFromMemory: true, |
|
||||||
blob: this.binaryData!.blob, |
|
||||||
mimeType: this.binaryData!.mimeType, |
|
||||||
resource: this, |
|
||||||
}; |
|
||||||
} else { |
|
||||||
return { |
|
||||||
isError: false, |
|
||||||
type: "dataReadSuccess", |
|
||||||
uri: this.uri, |
|
||||||
recalledFromMemory: true, |
|
||||||
resource: this, |
|
||||||
}; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Makes a request to read this leaf if it hasn't been fetched yet. If it has, |
|
||||||
* return the cached informtation |
|
||||||
* @returns a ReadLeafResult |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await leaf.read(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Will execute without making a request
|
|
||||||
* const result2 = await leaf.readIfUnfetched(); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async readIfUnfetched(): Promise<ResourceResult<ReadLeafResult, Leaf>> { |
|
||||||
return super.readIfUnfetched() as Promise< |
|
||||||
ResourceResult<ReadLeafResult, Leaf> |
|
||||||
>; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* PARENT CONTAINER METHODS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Gets the parent container for this leaf by making a request |
|
||||||
* @returns The parent container |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const leaf = solidLdoDataset |
|
||||||
* .getResource("https://example.com/container/resource.ttl"); |
|
||||||
* const leafParent = await leaf.getParentContainer(); |
|
||||||
* if (!leafParent.isError) { |
|
||||||
* // Logs "https://example.com/container/"
|
|
||||||
* console.log(leafParent.uri); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async getParentContainer(): Promise<Container> { |
|
||||||
const parentUri = getParentUri(this.uri)!; |
|
||||||
return this.context.resourceStore.get(parentUri); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Gets the root container for this leaf. |
|
||||||
* @returns The root container for this leaf |
|
||||||
* |
|
||||||
* @example |
|
||||||
* Suppose the root container is at `https://example.com/` |
|
||||||
* |
|
||||||
* ```typescript
|
|
||||||
* const leaf = ldoSolidDataset |
|
||||||
* .getResource("https://example.com/container/resource.ttl"); |
|
||||||
* const rootContainer = await leaf.getRootContainer(); |
|
||||||
* if (!rootContainer.isError) { |
|
||||||
* // logs "https://example.com/"
|
|
||||||
* console.log(rootContainer.uri); |
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async getRootContainer(): Promise< |
|
||||||
Container | CheckRootResultError | NoRootContainerError |
|
||||||
> { |
|
||||||
// Check to see if this document has a pim:storage if so, use that
|
|
||||||
|
|
||||||
// If not, traverse the tree
|
|
||||||
const parent = await this.getParentContainer(); |
|
||||||
return parent.getRootContainer(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* DELETE METHODS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* A helper method updates this leaf's internal state upon delete success |
|
||||||
* @param result - the result of the delete success |
|
||||||
*/ |
|
||||||
public updateWithDeleteSuccess(result: DeleteSuccess) { |
|
||||||
super.updateWithDeleteSuccess(result); |
|
||||||
this.binaryData = undefined; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Deletes this leaf and all its contents |
|
||||||
* @returns A Delete result for this leaf |
|
||||||
* |
|
||||||
* ```typescript
|
|
||||||
* const result = await container.leaf(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async delete(): Promise<DeleteResult> { |
|
||||||
return this.handleDelete(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* CREATE METHODS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* A helper method updates this leaf's internal state upon create success |
|
||||||
* @param _result - the result of the create success |
|
||||||
*/ |
|
||||||
protected updateWithCreateSuccess(_result: ResourceSuccess): void { |
|
||||||
this.binaryData = undefined; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a leaf at this URI and overwrites any that already exists |
|
||||||
* @returns LeafCreateAndOverwriteResult |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await leaf.createAndOverwrite(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async createAndOverwrite(): Promise< |
|
||||||
ResourceResult<LeafCreateAndOverwriteResult, Leaf> |
|
||||||
> { |
|
||||||
const createResult = |
|
||||||
(await this.handleCreateAndOverwrite()) as LeafCreateAndOverwriteResult; |
|
||||||
if (createResult.isError) return createResult; |
|
||||||
return { ...createResult, resource: this }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a leaf at this URI if the leaf doesn't already exist |
|
||||||
* @returns LeafCreateIfAbsentResult |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await leaf.createIfAbsent(); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async createIfAbsent(): Promise< |
|
||||||
ResourceResult<LeafCreateIfAbsentResult, Leaf> |
|
||||||
> { |
|
||||||
const createResult = |
|
||||||
(await this.handleCreateIfAbsent()) as LeafCreateIfAbsentResult; |
|
||||||
if (createResult.isError) return createResult; |
|
||||||
return { ...createResult, resource: this }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* UPLOAD METHODS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Uploads a binary resource to this URI. If there is already a resource |
|
||||||
* present at this URI, it will be overwritten |
|
||||||
* |
|
||||||
* @param blob - the Blob of the binary |
|
||||||
* @param mimeType - the MimeType of the binary |
|
||||||
* @returns A LeafCreateAndOverwriteResult |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await leaf.uploadAndOverwrite( |
|
||||||
* new Blob("some text."), |
|
||||||
* "text/txt", |
|
||||||
* ); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async uploadAndOverwrite( |
|
||||||
blob: Blob, |
|
||||||
mimeType: string, |
|
||||||
): Promise<ResourceResult<LeafCreateAndOverwriteResult, Leaf>> { |
|
||||||
const result = await this.requester.upload(blob, mimeType, true); |
|
||||||
this.status = result; |
|
||||||
if (result.isError) return result; |
|
||||||
super.updateWithCreateSuccess(result); |
|
||||||
this.binaryData = { blob, mimeType }; |
|
||||||
this.emitThisAndParent(); |
|
||||||
return { ...result, resource: this }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Uploads a binary resource to this URI tf there not is already a resource |
|
||||||
* present at this URI. |
|
||||||
* |
|
||||||
* @param blob - the Blob of the binary |
|
||||||
* @param mimeType - the MimeType of the binary |
|
||||||
* @returns A LeafCreateIfAbsentResult |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const result = await leaf.uploadIfAbsent( |
|
||||||
* new Blob("some text."), |
|
||||||
* "text/txt", |
|
||||||
* ); |
|
||||||
* if (!result.isError) { |
|
||||||
* // Do something
|
|
||||||
* } |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async uploadIfAbsent( |
|
||||||
blob: Blob, |
|
||||||
mimeType: string, |
|
||||||
): Promise<ResourceResult<LeafCreateIfAbsentResult, Leaf>> { |
|
||||||
const result = await this.requester.upload(blob, mimeType); |
|
||||||
this.status = result; |
|
||||||
if (result.isError) return result; |
|
||||||
super.updateWithCreateSuccess(result); |
|
||||||
this.binaryData = { blob, mimeType }; |
|
||||||
this.emitThisAndParent(); |
|
||||||
return { ...result, resource: this }; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* UPDATE METHODS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* Updates a data resource with the changes provided |
|
||||||
* @param changes - Dataset changes that will be applied to the resoruce |
|
||||||
* @returns An UpdateResult |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* import { |
|
||||||
* updateDataResource, |
|
||||||
* transactionChanges, |
|
||||||
* changeData, |
|
||||||
* createSolidLdoDataset, |
|
||||||
* } from "@ldo/solid"; |
|
||||||
* |
|
||||||
* //...
|
|
||||||
* |
|
||||||
* // Get a Linked Data Object
|
|
||||||
* const profile = solidLdoDataset |
|
||||||
* .usingType(ProfileShapeType) |
|
||||||
* .fromSubject("https://example.com/profile#me"); |
|
||||||
* cosnt resource = solidLdoDataset |
|
||||||
* .getResource("https://example.com/profile"); |
|
||||||
* // Create a transaction to change data
|
|
||||||
* const cProfile = changeData(profile, resource); |
|
||||||
* cProfile.name = "John Doe"; |
|
||||||
* // Get data in "DatasetChanges" form
|
|
||||||
* const datasetChanges = transactionChanges(someLinkedDataObject); |
|
||||||
* // Use "update" to apply the changes
|
|
||||||
* cosnt result = resource.update(datasetChanges); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
async update( |
|
||||||
changes: DatasetChanges<Quad>, |
|
||||||
): Promise<ResourceResult<UpdateResult, Leaf>> { |
|
||||||
const result = await this.requester.updateDataResource(changes); |
|
||||||
this.status = result; |
|
||||||
if (result.isError) return result; |
|
||||||
this.binaryData = undefined; |
|
||||||
this.absent = false; |
|
||||||
this.emitThisAndParent(); |
|
||||||
return { ...result, resource: this }; |
|
||||||
} |
|
||||||
} |
|
@ -1,834 +0,0 @@ |
|||||||
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; |
|
||||||
import type { |
|
||||||
ContainerCreateAndOverwriteResult, |
|
||||||
ContainerCreateIfAbsentResult, |
|
||||||
LeafCreateAndOverwriteResult, |
|
||||||
LeafCreateIfAbsentResult, |
|
||||||
} from "../requester/requests/createDataResource"; |
|
||||||
import type { |
|
||||||
ReadContainerResult, |
|
||||||
ReadLeafResult, |
|
||||||
} from "../requester/requests/readResource"; |
|
||||||
import type { BatchedRequester } from "../requester/BatchedRequester"; |
|
||||||
import type { CheckRootResultError } from "../requester/requests/checkRootContainer"; |
|
||||||
import type TypedEmitter from "typed-emitter"; |
|
||||||
import EventEmitter from "events"; |
|
||||||
import { getParentUri } from "../util/rdfUtils"; |
|
||||||
import type { RequesterResult } from "../requester/results/RequesterResult"; |
|
||||||
import { |
|
||||||
updateDatasetOnSuccessfulDelete, |
|
||||||
type DeleteResult, |
|
||||||
} from "../requester/requests/deleteResource"; |
|
||||||
import type { ReadSuccess } from "../requester/results/success/ReadSuccess"; |
|
||||||
import { isReadSuccess } from "../requester/results/success/ReadSuccess"; |
|
||||||
import type { DeleteSuccess } from "../requester/results/success/DeleteSuccess"; |
|
||||||
import type { ResourceSuccess } from "../requester/results/success/SuccessResult"; |
|
||||||
import type { Unfetched } from "../requester/results/success/Unfetched"; |
|
||||||
import type { CreateSuccess } from "../requester/results/success/CreateSuccess"; |
|
||||||
import type { ResourceResult } from "./resourceResult/ResourceResult"; |
|
||||||
import type { Container } from "./Container"; |
|
||||||
import type { Leaf } from "./Leaf"; |
|
||||||
import type { WacRule } from "./wac/WacRule"; |
|
||||||
import type { GetWacUriError, GetWacUriResult } from "./wac/getWacUri"; |
|
||||||
import { getWacUri } from "./wac/getWacUri"; |
|
||||||
import { getWacRuleWithAclUri, type GetWacRuleResult } from "./wac/getWacRule"; |
|
||||||
import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError"; |
|
||||||
import { setWacRuleForAclUri, type SetWacRuleResult } from "./wac/setWacRule"; |
|
||||||
import type { LeafUri } from "../util/uriTypes"; |
|
||||||
import type { NoRootContainerError } from "../requester/results/error/NoRootContainerError"; |
|
||||||
import type { |
|
||||||
NotificationSubscription, |
|
||||||
SubscriptionCallbacks, |
|
||||||
} from "./notifications/NotificationSubscription"; |
|
||||||
import { Websocket2023NotificationSubscription } from "./notifications/Websocket2023NotificationSubscription"; |
|
||||||
import type { NotificationMessage } from "./notifications/NotificationMessage"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Statuses shared between both Leaf and Container |
|
||||||
*/ |
|
||||||
export type SharedStatuses = Unfetched | DeleteResult | CreateSuccess; |
|
||||||
|
|
||||||
/** |
|
||||||
* Represents the current status of a specific Resource on a Pod as known by LDO. |
|
||||||
*/ |
|
||||||
export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ |
|
||||||
update: () => void; |
|
||||||
notification: () => void; |
|
||||||
}>) { |
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* The SolidLdoDatasetContext from the Parent Dataset |
|
||||||
*/ |
|
||||||
protected readonly context: SolidLdoDatasetContext; |
|
||||||
|
|
||||||
/** |
|
||||||
* The uri of the resource |
|
||||||
*/ |
|
||||||
abstract readonly uri: string; |
|
||||||
|
|
||||||
/** |
|
||||||
* The type of resource (leaf or container) |
|
||||||
*/ |
|
||||||
abstract readonly type: string; |
|
||||||
|
|
||||||
/** |
|
||||||
* The status of the last request made for this resource |
|
||||||
*/ |
|
||||||
abstract status: RequesterResult; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Batched Requester for the Resource |
|
||||||
*/ |
|
||||||
protected abstract readonly requester: BatchedRequester; |
|
||||||
|
|
||||||
/** |
|
||||||
* @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?: LeafUri; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* If a wac rule was fetched, it is cached here |
|
||||||
*/ |
|
||||||
protected wacRule?: WacRule; |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Handles notification subscriptions |
|
||||||
*/ |
|
||||||
protected notificationSubscription: NotificationSubscription; |
|
||||||
|
|
||||||
/** |
|
||||||
* @param context - SolidLdoDatasetContext for the parent dataset |
|
||||||
*/ |
|
||||||
constructor(context: SolidLdoDatasetContext) { |
|
||||||
super(); |
|
||||||
this.context = context; |
|
||||||
this.notificationSubscription = new Websocket2023NotificationSubscription( |
|
||||||
this, |
|
||||||
this.onNotification.bind(this), |
|
||||||
this.context, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* 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(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* 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; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* 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.resourceStore.get(parentUri); |
|
||||||
parentContainer.emit("update"); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* 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.absent = result.type === "absentReadSuccess"; |
|
||||||
this.didInitialFetch = true; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @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; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Converts the current state of this resource to a readResult |
|
||||||
* @returns a ReadResult |
|
||||||
*/ |
|
||||||
protected abstract toReadResult(): ResourceResult< |
|
||||||
ReadLeafResult | ReadContainerResult, |
|
||||||
Container | Leaf |
|
||||||
>; |
|
||||||
|
|
||||||
/** |
|
||||||
* Reads the resource |
|
||||||
*/ |
|
||||||
abstract read(): Promise< |
|
||||||
ResourceResult<ReadLeafResult | ReadContainerResult, Container | Leaf> |
|
||||||
>; |
|
||||||
|
|
||||||
/** |
|
||||||
* Reads the resource if it isn't fetched yet |
|
||||||
* @returns a ReadResult |
|
||||||
*/ |
|
||||||
async readIfUnfetched(): Promise< |
|
||||||
ResourceResult<ReadLeafResult | ReadContainerResult, Container | Leaf> |
|
||||||
> { |
|
||||||
if (this.didInitialFetch) { |
|
||||||
const readResult = this.toReadResult(); |
|
||||||
this.status = readResult; |
|
||||||
return readResult; |
|
||||||
} |
|
||||||
return this.read(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* 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.absent = true; |
|
||||||
this.didInitialFetch = true; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Helper method that handles the core functions for deleting a resource |
|
||||||
* @returns DeleteResult |
|
||||||
*/ |
|
||||||
protected async handleDelete(): Promise<DeleteResult> { |
|
||||||
const result = await this.requester.delete(); |
|
||||||
this.status = result; |
|
||||||
if (result.isError) return result; |
|
||||||
this.updateWithDeleteSuccess(result); |
|
||||||
this.emitThisAndParent(); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* CREATE METHODS |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* 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.absent = false; |
|
||||||
this.didInitialFetch = true; |
|
||||||
if (isReadSuccess(result)) { |
|
||||||
this.updateWithReadSuccess(result); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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< |
|
||||||
ResourceResult< |
|
||||||
ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult, |
|
||||||
Leaf | Container |
|
||||||
> |
|
||||||
>; |
|
||||||
|
|
||||||
/** |
|
||||||
* @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; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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< |
|
||||||
ResourceResult< |
|
||||||
ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult, |
|
||||||
Leaf | Container |
|
||||||
> |
|
||||||
>; |
|
||||||
|
|
||||||
/** |
|
||||||
* @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; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* 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< |
|
||||||
Container | CheckRootResultError | NoRootContainerError |
|
||||||
>; |
|
||||||
|
|
||||||
abstract getParentContainer(): Promise< |
|
||||||
Container | 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.fetch, |
|
||||||
}); |
|
||||||
if (wacUriResult.isError) { |
|
||||||
return wacUriResult; |
|
||||||
} |
|
||||||
this.wacUri = wacUriResult.wacUri; |
|
||||||
return wacUriResult; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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.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.uri, |
|
||||||
`Resource "${this.uri}" has no Effective ACL resource`, |
|
||||||
); |
|
||||||
} |
|
||||||
return parentResource.getWac(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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.fetch, |
|
||||||
}, |
|
||||||
); |
|
||||||
if (result.isError) return result; |
|
||||||
this.wacRule = result.wacRule; |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* 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, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @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(); |
|
||||||
} |
|
||||||
} |
|
@ -1,10 +0,0 @@ |
|||||||
/** |
|
||||||
* A message sent from the Pod as a notification |
|
||||||
*/ |
|
||||||
export interface NotificationMessage { |
|
||||||
"@context": string | string[]; |
|
||||||
id: string; |
|
||||||
type: "Update" | "Delete" | "Remove" | "Add"; |
|
||||||
object: string; |
|
||||||
published: string; |
|
||||||
} |
|
@ -1,144 +0,0 @@ |
|||||||
import type { SolidLdoDatasetContext } from "../../SolidLdoDatasetContext"; |
|
||||||
import type { Resource } from "../Resource"; |
|
||||||
import type { NotificationMessage } from "./NotificationMessage"; |
|
||||||
import type { NotificationCallbackError } from "./results/NotificationErrors"; |
|
||||||
import { v4 } from "uuid"; |
|
||||||
|
|
||||||
export interface SubscriptionCallbacks { |
|
||||||
onNotification?: (message: NotificationMessage) => void; |
|
||||||
// TODO: make notification errors more specific
|
|
||||||
onNotificationError?: (error: Error) => void; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @internal |
|
||||||
* Abstract class for notification subscription methods. |
|
||||||
*/ |
|
||||||
export abstract class NotificationSubscription { |
|
||||||
protected resource: Resource; |
|
||||||
protected parentSubscription: (message: NotificationMessage) => void; |
|
||||||
protected context: SolidLdoDatasetContext; |
|
||||||
protected subscriptions: Record<string, SubscriptionCallbacks> = {}; |
|
||||||
private isOpen: boolean = false; |
|
||||||
|
|
||||||
constructor( |
|
||||||
resource: Resource, |
|
||||||
parentSubscription: (message: NotificationMessage) => void, |
|
||||||
context: SolidLdoDatasetContext, |
|
||||||
) { |
|
||||||
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: NotificationMessage): 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"); |
|
||||||
} |
|
||||||
} |
|
@ -1,134 +0,0 @@ |
|||||||
import { UnexpectedResourceError } from "../../requester/results/error/ErrorResult"; |
|
||||||
import { NotificationSubscription } from "./NotificationSubscription"; |
|
||||||
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"; |
|
||||||
|
|
||||||
const CHANNEL_TYPE = |
|
||||||
"http://www.w3.org/ns/solid/notifications#WebSocketChannel2023"; |
|
||||||
|
|
||||||
export class Websocket2023NotificationSubscription extends NotificationSubscription { |
|
||||||
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); |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
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; |
|
||||||
} |
|
@ -1,19 +0,0 @@ |
|||||||
import type { RequesterResult } from "../../requester/results/RequesterResult"; |
|
||||||
import type { Container } from "../Container"; |
|
||||||
import type { Leaf } from "../Leaf"; |
|
||||||
|
|
||||||
/** |
|
||||||
* Adds an additional field "resource" to SuccessResults. |
|
||||||
*/ |
|
||||||
export type ResourceSuccess< |
|
||||||
Result extends RequesterResult, |
|
||||||
ResourceType extends Leaf | Container, |
|
||||||
> = Result & { resource: ResourceType }; |
|
||||||
|
|
||||||
/** |
|
||||||
* Adds an additional field "resource" to Results. |
|
||||||
*/ |
|
||||||
export type ResourceResult< |
|
||||||
Result extends RequesterResult, |
|
||||||
ResourceType extends Leaf | Container, |
|
||||||
> = Result extends Error ? Result : ResourceSuccess<Result, ResourceType>; |
|
@ -1,18 +0,0 @@ |
|||||||
/** |
|
||||||
* 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>; |
|
||||||
} |
|
@ -1,118 +0,0 @@ |
|||||||
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, |
|
||||||
}; |
|
||||||
} |
|
@ -1,70 +0,0 @@ |
|||||||
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); |
|
||||||
} |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
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; |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
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; |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
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; |
|
||||||
} |
|
@ -1,8 +0,0 @@ |
|||||||
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"; |
|
||||||
} |
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue