renamed solid-react to react

main
Jackson Morgan 6 months ago
parent 4e6e0355b7
commit 3b4efda881
  1. 0
      packages/react/.eslintrc
  2. 0
      packages/react/.gitignore
  3. 0
      packages/react/LICENSE.txt
  4. 32
      packages/react/README.md
  5. 0
      packages/react/jest.config.js
  6. 0
      packages/react/jest.setup.ts
  7. 4
      packages/react/package.json
  8. 0
      packages/react/src/BrowserSolidLdoProvider.tsx
  9. 0
      packages/react/src/SolidAuthContext.ts
  10. 0
      packages/react/src/SolidLdoProvider.tsx
  11. 0
      packages/react/src/UnauthenticatedSolidLdoProvider.tsx
  12. 0
      packages/react/src/index.ts
  13. 0
      packages/react/src/useLdoMethods.ts
  14. 0
      packages/react/src/useMatchObject.ts
  15. 0
      packages/react/src/useMatchSubject.ts
  16. 0
      packages/react/src/useResource.ts
  17. 0
      packages/react/src/useRootContainer.ts
  18. 0
      packages/react/src/useSubject.ts
  19. 0
      packages/react/src/useSubscribeToResource.ts
  20. 0
      packages/react/src/util/TrackingProxyContext.ts
  21. 0
      packages/react/src/util/TrackingSetProxy.ts
  22. 0
      packages/react/src/util/TrackingSubjectProxy.ts
  23. 0
      packages/react/src/util/useTrackingProxy.ts
  24. 0
      packages/react/test/.ldo/post.context.ts
  25. 0
      packages/react/test/.ldo/post.schema.ts
  26. 0
      packages/react/test/.ldo/post.shapeTypes.ts
  27. 0
      packages/react/test/.ldo/post.typings.ts
  28. 0
      packages/react/test/Integration.test.tsx
  29. 0
      packages/react/test/setUpServer.ts
  30. 0
      packages/react/test/test-server/configs/components-config/unauthenticatedServer.json
  31. 0
      packages/react/test/test-server/configs/solid-css-seed.json
  32. 0
      packages/react/test/test-server/configs/template/wac/.acl.hbs
  33. 0
      packages/react/test/test-server/configs/template/wac/profile/card.acl.hbs
  34. 0
      packages/react/test/test-server/runServer.ts
  35. 0
      packages/react/test/test-server/solidServer.helper.ts
  36. 0
      packages/react/tsconfig.build.json
  37. 3
      packages/solid/.eslintrc
  38. 1
      packages/solid/.gitignore
  39. 21
      packages/solid/LICENSE.txt
  40. 238
      packages/solid/README.md
  41. 1
      packages/solid/babel.config.js
  42. 11
      packages/solid/jest.config.js
  43. 61
      packages/solid/package.json
  44. 108
      packages/solid/src/.ldo/solid.context.ts
  45. 233
      packages/solid/src/.ldo/solid.schema.ts
  46. 37
      packages/solid/src/.ldo/solid.shapeTypes.ts
  47. 84
      packages/solid/src/.ldo/solid.typings.ts
  48. 78
      packages/solid/src/.ldo/wac.context.ts
  49. 169
      packages/solid/src/.ldo/wac.schema.ts
  50. 19
      packages/solid/src/.ldo/wac.shapeTypes.ts
  51. 73
      packages/solid/src/.ldo/wac.typings.ts
  52. 43
      packages/solid/src/.shapes/solid.shex
  53. 23
      packages/solid/src/.shapes/wac.shex
  54. 79
      packages/solid/src/ResourceStore.ts
  55. 169
      packages/solid/src/SolidLdoDataset.ts
  56. 20
      packages/solid/src/SolidLdoDatasetContext.ts
  57. 186
      packages/solid/src/SolidLdoTransactionDataset.ts
  58. 68
      packages/solid/src/createSolidLdoDataset.ts
  59. 31
      packages/solid/src/index.ts
  60. 84
      packages/solid/src/methods.ts
  61. 190
      packages/solid/src/requester/BatchedRequester.ts
  62. 79
      packages/solid/src/requester/ContainerBatchedRequester.ts
  63. 172
      packages/solid/src/requester/LeafBatchedRequester.ts
  64. 99
      packages/solid/src/requester/requests/checkRootContainer.ts
  65. 241
      packages/solid/src/requester/requests/createDataResource.ts
  66. 95
      packages/solid/src/requester/requests/deleteResource.ts
  67. 182
      packages/solid/src/requester/requests/readResource.ts
  68. 22
      packages/solid/src/requester/requests/requestOptions.ts
  69. 101
      packages/solid/src/requester/requests/updateDataResource.ts
  70. 121
      packages/solid/src/requester/requests/uploadResource.ts
  71. 7
      packages/solid/src/requester/results/RequesterResult.ts
  72. 18
      packages/solid/src/requester/results/error/AccessControlError.ts
  73. 125
      packages/solid/src/requester/results/error/ErrorResult.ts
  74. 160
      packages/solid/src/requester/results/error/HttpErrorResult.ts
  75. 13
      packages/solid/src/requester/results/error/InvalidUriError.ts
  76. 17
      packages/solid/src/requester/results/error/NoRootContainerError.ts
  77. 20
      packages/solid/src/requester/results/error/NoncompliantPodError.ts
  78. 19
      packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts
  79. 13
      packages/solid/src/requester/results/success/CreateSuccess.ts
  80. 14
      packages/solid/src/requester/results/success/DeleteSuccess.ts
  81. 71
      packages/solid/src/requester/results/success/ReadSuccess.ts
  82. 31
      packages/solid/src/requester/results/success/SuccessResult.ts
  83. 8
      packages/solid/src/requester/results/success/Unfetched.ts
  84. 24
      packages/solid/src/requester/results/success/UpdateSuccess.ts
  85. 26
      packages/solid/src/requester/util/modifyQueueFuntions.ts
  86. 599
      packages/solid/src/resource/Container.ts
  87. 559
      packages/solid/src/resource/Leaf.ts
  88. 834
      packages/solid/src/resource/Resource.ts
  89. 10
      packages/solid/src/resource/notifications/NotificationMessage.ts
  90. 144
      packages/solid/src/resource/notifications/NotificationSubscription.ts
  91. 134
      packages/solid/src/resource/notifications/Websocket2023NotificationSubscription.ts
  92. 30
      packages/solid/src/resource/notifications/results/NotificationErrors.ts
  93. 19
      packages/solid/src/resource/resourceResult/ResourceResult.ts
  94. 18
      packages/solid/src/resource/wac/WacRule.ts
  95. 118
      packages/solid/src/resource/wac/getWacRule.ts
  96. 70
      packages/solid/src/resource/wac/getWacUri.ts
  97. 13
      packages/solid/src/resource/wac/results/GetWacRuleSuccess.ts
  98. 13
      packages/solid/src/resource/wac/results/GetWacUriSuccess.ts
  99. 13
      packages/solid/src/resource/wac/results/SetWacRuleSuccess.ts
  100. 8
      packages/solid/src/resource/wac/results/WacRuleAbsent.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,6 +1,6 @@
# @ldo/solid-react
# @ldo/react
`@ldo/solid-react` provides tool and hooks for easily building Solid applications using react.
`@ldo/react` provides tool and hooks for easily building Solid applications using react.
## Guide
@ -17,7 +17,7 @@ npx run @ldo/cli init
Now install the @ldo/solid library
```
npm i @ldo/solid @ldo/solid-react
npm i @ldo/solid @ldo/react
```
<details>
@ -28,13 +28,13 @@ Manual Installation
If you already have generated ShapeTypes, you may install the `@ldo/ldo` and `@ldo/solid` libraries independently.
```
npm i @ldo/ldo @ldo/solid @ldo/solid-react
npm i @ldo/ldo @ldo/solid @ldo/react
```
</details>
## Simple Example
Below is a simple example of @ldo/solid-react in a real use-case. Assume that a ShapeType was previously generated and placed at `./.ldo/solidProfile.shapeTypess`.
Below is a simple example of @ldo/react in a real use-case. Assume that a ShapeType was previously generated and placed at `./.ldo/solidProfile.shapeTypess`.
```typescript
@ -45,7 +45,7 @@ import {
useResource,
useSolidAuth,
useSubject,
} from "@ldo/solid-react";
} from "@ldo/react";
import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes";
import { changeData, commitData } from "@ldo/solid";
@ -115,18 +115,18 @@ export default App;
Providers
- [BrowserSolidLdoProvider](https://ldo.js.org/latest/api/solid-react/BrowserSolidLdoProvider/)
- [SolidLdoProvider](https://ldo.js.org/latest/api/solid-react/SolidLdoProvider/)
- [BrowserSolidLdoProvider](https://ldo.js.org/latest/api/react/BrowserSolidLdoProvider/)
- [SolidLdoProvider](https://ldo.js.org/latest/api/react/SolidLdoProvider/)
Hooks
- [useLdo](https://ldo.js.org/latest/api/solid-react/useLdo/)
- [useResource](https://ldo.js.org/latest/api/solid-react/useResource/)
- [useRootContainer](https://ldo.js.org/latest/api/solid-react/useRootContainer/)
- [useSolidAuth](https://ldo.js.org/latest/api/solid-react/useSolidAuth/)
- [useSubject](https://ldo.js.org/latest/api/solid-react/useSubject/)
- [useMatchSubject](https://ldo.js.org/latest/api/solid-react/useMatchSubject/)
- [useMatchObject](https://ldo.js.org/latest/api/solid-react/useMatchSubject/)
- [useSubscribeToResource](https://ldo.js.org/latest/api/solid-react/useMatchSubject/)
- [useLdo](https://ldo.js.org/latest/api/react/useLdo/)
- [useResource](https://ldo.js.org/latest/api/react/useResource/)
- [useRootContainer](https://ldo.js.org/latest/api/react/useRootContainer/)
- [useSolidAuth](https://ldo.js.org/latest/api/react/useSolidAuth/)
- [useSubject](https://ldo.js.org/latest/api/react/useSubject/)
- [useMatchSubject](https://ldo.js.org/latest/api/react/useMatchSubject/)
- [useMatchObject](https://ldo.js.org/latest/api/react/useMatchSubject/)
- [useSubscribeToResource](https://ldo.js.org/latest/api/react/useMatchSubject/)
## 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/).

@ -1,5 +1,5 @@
{
"name": "@ldo/solid-react",
"name": "@ldo/react",
"version": "1.0.0-alpha.1",
"description": "A React library for LDO and Solid",
"main": "dist/index.js",
@ -40,7 +40,7 @@
"@ldo/dataset": "^1.0.0-alpha.1",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1",
"@ldo/ldo": "^1.0.0-alpha.1",
"@ldo/solid": "^1.0.0-alpha.1",
"@ldo/connected": "^1.0.0-alpha.1",
"@ldo/subscribable-dataset": "^1.0.0-alpha.1",
"@rdfjs/data-model": "^1.2.0",
"cross-fetch": "^3.1.6"

@ -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…
Cancel
Save