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