Merge pull request #6 from o-development/solid-test-coverage

Solid test coverage
main
jaxoncreed 2 years ago committed by GitHub
commit 1def1750f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .gitignore
  2. 3
      Readme.md
  3. 2
      lerna.json
  4. 28032
      package-lock.json
  5. 4
      package.json
  6. 55
      packages/cli/README.md
  7. 4
      packages/cli/package.json
  8. 4
      packages/cli/src/index.ts
  9. 8
      packages/demo-react/package.json
  10. 13
      packages/demo-react/src/App-old.tsx
  11. 64
      packages/demo-react/src/App.tsx
  12. 4
      packages/jsonld-dataset-proxy/package.json
  13. 288
      packages/ldo/README.md
  14. 77
      packages/ldo/example/example.ts
  15. 26
      packages/ldo/example/ldo/foafProfile.context.ts
  16. 93
      packages/ldo/example/ldo/foafProfile.schema.ts
  17. 19
      packages/ldo/example/ldo/foafProfile.shapeTypes.ts
  18. 33
      packages/ldo/example/ldo/foafProfile.typings.ts
  19. 15
      packages/ldo/example/shapes/foafProfile.shex
  20. 13
      packages/ldo/package.json
  21. 187
      packages/ldo/src/LdoBuilder.ts
  22. 21
      packages/ldo/src/LdoDataset.ts
  23. 24
      packages/ldo/src/LdoDatasetFactory.ts
  24. 49
      packages/ldo/src/ShapeType.ts
  25. 32
      packages/ldo/src/createLdoDataset.ts
  26. 332
      packages/ldo/src/methods.ts
  27. 24
      packages/ldo/src/parseRdf.ts
  28. 5
      packages/ldo/src/util.ts
  29. 6
      packages/ldo/typedoc.json
  30. 7
      packages/solid-react/jest.config.js
  31. 2
      packages/solid-react/jest.setup.ts
  32. 27
      packages/solid-react/package.json
  33. 62
      packages/solid-react/src/UnauthenticatedSolidLdoProvider.tsx
  34. 1
      packages/solid-react/src/index.ts
  35. 56
      packages/solid-react/src/useLdoMethods.ts
  36. 1
      packages/solid-react/src/useResource.ts
  37. 11
      packages/solid-react/src/useRootContainer.ts
  38. 10
      packages/solid-react/src/util/TrackingProxyContext.ts
  39. 7
      packages/solid-react/src/util/createWrapperProxy.ts
  40. 6
      packages/solid-react/src/util/useForceReload.ts
  41. 32
      packages/solid-react/test/.ldo/post.context.ts
  42. 155
      packages/solid-react/test/.ldo/post.schema.ts
  43. 19
      packages/solid-react/test/.ldo/post.shapeTypes.ts
  44. 45
      packages/solid-react/test/.ldo/post.typings.ts
  45. 358
      packages/solid-react/test/Integration.test.tsx
  46. 110
      packages/solid-react/test/setUpServer.ts
  47. 52
      packages/solid-react/test/test-server/configs/components-config/unauthenticatedServer.json
  48. 8
      packages/solid-react/test/test-server/configs/solid-css-seed.json
  49. 13
      packages/solid-react/test/test-server/configs/template/wac/.acl.hbs
  50. 19
      packages/solid-react/test/test-server/configs/template/wac/profile/card.acl.hbs
  51. 7
      packages/solid-react/test/test-server/runServer.ts
  52. 42
      packages/solid-react/test/test-server/solidServer.helper.ts
  53. 5
      packages/solid-react/test/trivial.test.ts
  54. 227
      packages/solid/README.md
  55. 195
      packages/solid/example/example-methodBrainstorm.ts
  56. 22
      packages/solid/example/example.ts
  57. 13
      packages/solid/package.json
  58. 33
      packages/solid/src/ResourceStore.ts
  59. 126
      packages/solid/src/SolidLdoDataset.ts
  60. 20
      packages/solid/src/SolidLdoDatasetContext.ts
  61. 32
      packages/solid/src/createSolidLdoDataset.ts
  62. 12
      packages/solid/src/index.ts
  63. 90
      packages/solid/src/methods.ts
  64. 84
      packages/solid/src/requester/BatchedRequester.ts
  65. 44
      packages/solid/src/requester/ContainerBatchedRequester.ts
  66. 57
      packages/solid/src/requester/LeafBatchedRequester.ts
  67. 36
      packages/solid/src/requester/requests/checkRootContainer.ts
  68. 96
      packages/solid/src/requester/requests/createDataResource.ts
  69. 35
      packages/solid/src/requester/requests/deleteResource.ts
  70. 2
      packages/solid/src/requester/requests/getAccessRules.ts
  71. 44
      packages/solid/src/requester/requests/readResource.ts
  72. 17
      packages/solid/src/requester/requests/requestOptions.ts
  73. 2
      packages/solid/src/requester/requests/setAccessRules.ts
  74. 61
      packages/solid/src/requester/requests/updateDataResource.ts
  75. 39
      packages/solid/src/requester/requests/uploadResource.ts
  76. 3
      packages/solid/src/requester/results/RequesterResult.ts
  77. 9
      packages/solid/src/requester/results/error/AccessControlError.ts
  78. 58
      packages/solid/src/requester/results/error/ErrorResult.ts
  79. 65
      packages/solid/src/requester/results/error/HttpErrorResult.ts
  80. 4
      packages/solid/src/requester/results/error/InvalidUriError.ts
  81. 9
      packages/solid/src/requester/results/error/NoncompliantPodError.ts
  82. 7
      packages/solid/src/requester/results/success/CheckRootContainerSuccess.ts
  83. 7
      packages/solid/src/requester/results/success/CreateSuccess.ts
  84. 8
      packages/solid/src/requester/results/success/DeleteSuccess.ts
  85. 38
      packages/solid/src/requester/results/success/ReadSuccess.ts
  86. 16
      packages/solid/src/requester/results/success/SuccessResult.ts
  87. 3
      packages/solid/src/requester/results/success/Unfetched.ts
  88. 11
      packages/solid/src/requester/results/success/UpdateSuccess.ts
  89. 26
      packages/solid/src/requester/util/modifyQueueFuntions.ts
  90. 341
      packages/solid/src/resource/Container.ts
  91. 407
      packages/solid/src/resource/Leaf.ts
  92. 341
      packages/solid/src/resource/Resource.ts
  93. 6
      packages/solid/src/resource/resourceResult/ResourceResult.ts
  94. 86
      packages/solid/src/util/RequestBatcher.ts
  95. 7
      packages/solid/src/util/guaranteeFetch.ts
  96. 38
      packages/solid/src/util/rdfUtils.ts
  97. 19
      packages/solid/src/util/splitChangesByGraph.ts
  98. 34
      packages/solid/src/util/uriTypes.ts
  99. 31
      packages/solid/test/.ldo/post.context.ts
  100. 155
      packages/solid/test/.ldo/post.schema.ts
  101. Some files were not shown because too many files have changed in this diff Show More

2
.gitignore vendored

@ -16,3 +16,5 @@ yarn-debug.log*
yarn-error.log*
coverage/
docs/

@ -2,6 +2,9 @@
This is a monorepo that contains all libraries associated with Linked Data Objects (LDO).
## Documentation
Full documentation can be found at [ldo.js.org](https://ldo.js.org).
## Tutorial
[A tutorial for how to use LDO](./documentation/solid-react-tutorial.md) is available here.

@ -1,4 +1,4 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "0.0.1-alpha.17"
"version": "0.0.1-alpha.18"
}

28032
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -8,7 +8,9 @@
"test": "lerna run test",
"build": "lerna run build",
"demo-react": "lerna run start --scope @ldo/demo-react",
"lint": "lerna run lint"
"lint": "lerna run lint",
"clean": "lerna run clean",
"publish": "lerna publish --no-private"
},
"devDependencies": {
"@types/node": "^20.5.7",

@ -1,28 +1,61 @@
# LDO-CLI
# @ldo/cli
A command line interface for Linked Data Objects. LDO-CLI builds `.shex` shapes into LDO types.
The `@ldo/cli` is a command line interface for initializing LDO and building ShapeTypes.
## Setup
Install the CLI
### Automatic Setup
To setup LDO, `cd` into your typescript project and run `npx @ldo/cli init`.
```bash
npm i @ldo/cli --save-dev
cd my-typescript-project
npx @ldo/cli init
```
Set up a shapes folder
### Manual Setup
The following is handled by the __automatic setup__:
Install the LDO dependencies.
```bash
mkdir .shapes
npm install @ldo/ldo
npm install @ldo/cli --save-dev
```
Place ShexC shapes inside `.shex` files
Create a folder to store your ShEx shapes:
```bash
touch ./.shapes/example.shex
mkdir shapes
```
Create a script to build ShEx shapes and convert them into Linked Data Objects. You can put this script in `package.json`
```json
{
...
scripts: {
...
"build:ldo": "ldo build --input ./shapes --output ./ldo"
...
}
...
}
```
Build the shpaes
## Generating a ShapeType
@ldo/cli generates shape types using the `*.shex` files in the "input" folder. If you followed the instructions above, run the following command:
```bash
ldo build --input ./.shapes --output ./.ldo
npm run build:ldo
```
This will generate five files:
- `./ldo/foafProfile.shapeTypes.ts` <-- This is the important file
- `./ldo/foafProfile.typings.ts`
- `./ldo/foafProfile.schema.ts`
- `./ldo/foafProfile.context.ts`
## API Details
- [`init` command](https://ldo.js.org/api/cli/init/)
- [`build` command](https://ldo.js.org/api/cli/build/)
```
## Sponsorship

@ -1,6 +1,6 @@
{
"name": "@ldo/cli",
"version": "0.0.1-alpha.17",
"version": "0.0.1-alpha.18",
"description": "A Command Line Interface for Linked Data Objects",
"main": "./dist/index.js",
"bin": {
@ -10,7 +10,7 @@
"start": "node dist/index.js build",
"start:init": "node dist/index.js init",
"dev": "npm run build && npm run start:init",
"build": "npm run clean && npm run build:ts && npm run copy-files && npm run update-permission",
"build": "npm run clean && npm run build:ts && npm run copy-files",
"build:ts": "tsc --project tsconfig.build.json",
"clean": "rimraf dist/",
"copy-files": "copyfiles -u 1 \"./src/**/*.ejs\" dist/",

@ -12,8 +12,8 @@ program
program
.command("build")
.description("Build contents of a shex folder into Shape Types")
.option("-i, --input <inputPath>", "Provide the input path", "./shapes")
.option("-o, --output <outputPath>", "Provide the output path", "./ldo")
.option("-i, --input <inputPath>", "Provide the input path", "./.shapes")
.option("-o, --output <outputPath>", "Provide the output path", "./.ldo")
.action(build);
program

@ -1,13 +1,13 @@
{
"name": "@ldo/demo-react",
"version": "0.0.1-alpha.17",
"version": "0.0.1-alpha.18",
"dependencies": {
"@ldo/solid-react": "^0.0.1-alpha.17",
"@inrupt/solid-client-authn-browser": "^2.0.0",
"@ldo/solid-react": "^0.0.1-alpha.18",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"solid-authn-react-native": "^2.0.3",
"uuid": "^9.0.1"
},
"scripts": {
@ -37,7 +37,7 @@
},
"devDependencies": {
"@craco/craco": "^7.1.0",
"@ldo/cli": "^0.0.1-alpha.17",
"@ldo/cli": "^0.0.1-alpha.18",
"@types/jsonld": "^1.5.9",
"@types/react": "^18.2.21",
"@types/shexj": "^2.1.4",

@ -0,0 +1,13 @@
import type { FunctionComponent } from "react";
import React from "react";
import { Router } from "./Layout";
import { BrowserSolidLdoProvider } from "@ldo/solid-react";
const ProfileApp: FunctionComponent = () => {
return (
<BrowserSolidLdoProvider>
<Router />
</BrowserSolidLdoProvider>
);
};
export default ProfileApp;

@ -1,13 +1,65 @@
import type { FunctionComponent } from "react";
import React from "react";
import { Router } from "./Layout";
import { BrowserSolidLdoProvider } from "@ldo/solid-react";
import React, { useCallback } from "react";
import {
BrowserSolidLdoProvider,
useResource,
useSolidAuth,
useSubject,
} from "@ldo/solid-react";
import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes";
import { changeData, commitData } from "@ldo/solid";
const ProfileApp: FunctionComponent = () => {
// The base component for the app
const App: FunctionComponent = () => {
return (
/* The application should be surrounded with the BrowserSolidLdoProvider
this will set up all the underlying infrastructure for the application */
<BrowserSolidLdoProvider>
<Router />
<Login />
</BrowserSolidLdoProvider>
);
};
export default ProfileApp;
// A component that handles login
const Login: FunctionComponent = () => {
// Get login information using the "useSolidAuth" hook
const { login, logout, session } = useSolidAuth();
const onLogin = useCallback(() => {
const issuer = prompt("What is your Solid IDP?");
// Call the "login" function to initiate login
if (issuer) login(issuer);
}, []);
// You can use session.isLoggedIn to check if the user is logged in
if (session.isLoggedIn) {
return (
<div>
{/* Get the user's webId from session.webId */}
<p>Logged in as {session.webId}</p>
{/* Use the logout function to log out */}
<button onClick={logout}>Log Out</button>
<Profile />
</div>
);
}
return <button onClick={onLogin}>Log In</button>;
};
const Profile: FunctionComponent = () => {
const { session } = useSolidAuth();
const resource = useResource(session.webId);
const profile = useSubject(SolidProfileShapeShapeType, session.webId);
const onNameChange = useCallback(async (e) => {
// Ensure that the
if (!profile || !resource) return;
const cProfile = changeData(profile, resource);
cProfile.name = e.target.value;
await commitData(cProfile);
}, []);
return <input type="text" value={profile?.name} onChange={onNameChange} />;
};
export default App;

@ -1,6 +1,6 @@
{
"name": "@ldo/jsonld-dataset-proxy",
"version": "0.0.1-alpha.17",
"version": "0.0.1-alpha.18",
"description": "",
"main": "dist/index.js",
"scripts": {
@ -41,7 +41,7 @@
],
"dependencies": {
"@ldo/rdf-utils": "^0.0.1-alpha.17",
"@ldo/subscribable-dataset": "^0.0.1-alpha.17",
"@ldo/subscribable-dataset": "^0.0.1-alpha.18",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/dataset": "^1.1.0",
"jsonld2graphobject": "^0.0.4"

@ -1,89 +1,36 @@
# LDO (Linked Data Objects)
# @ldo/ldo
LDO (Linked Data Objects) is a library that lets you easily manipulate RDF as if it were a standard TypeScript object that follows a [ShEx](https://shex.io) shape you define.
`@ldo/ldo` is the primary interface for accessing Linked Data Objects given raw RDF.
For a full tutorial of using LDO to build React Solid applications, see [this tutorial](https://medium.com/@JacksonMorgan/building-solid-apps-with-ldo-6127a5a1979c).
## Guide
## Setup
A full walkthrough for using the `@ldo/ldo` library can be found in the [For RDF Usage Guide](https://ldo.js.org/raw_rdf/).
### Automatic Setup
To setup LDO, `cd` into your typescript project and run `npx @ldo/cli init`.
## Installation
```bash
cd my-typescript-project
npx @ldo/cli init
```
### Manual Setup
The following is handled by the __automatic setup__:
Install the LDO dependencies.
```bash
npm install @ldo/ldo
npm install @ldo/cli --save-dev
```
### Automatic Installation
Create a folder to store your ShEx shapes:
```bash
mkdir shapes
Navigate into your project's root folder and run the following command:
```
Create a script to build ShEx shapes and convert them into Linked Data Objects. You can put this script in `package.json`
```json
{
...
scripts: {
...
"build:ldo": "ldo build --input ./shapes --output ./ldo"
...
}
...
}
cd my_project/
npx run @ldo/cli init
```
## Creating ShEx Schemas
LDO uses [ShEx](https://shex.io) as a schema for the RDF data in your project. To add a ShEx schema to your project, simply create a file ending in `.shex` to the `shapes` folder.
For more information on writing ShEx schemas see the [ShEx Primer](http://shex.io/shex-primer/index.html).
### Manual Installation
`./shapes/foafProfile.shex`:
```shex
PREFIX ex: <https://example.com/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
If you already have generated ShapeTypes, you may install the `@ldo/ldo` library independently.
ex:FoafProfile EXTRA a {
a [ foaf:Person ]
// rdfs:comment "Defines the node as a Person (from foaf)" ;
foaf:name xsd:string ?
// rdfs:comment "Define a person's name." ;
foaf:img xsd:string ?
// rdfs:comment "Photo link but in string form" ;
foaf:knows @ex:FoafProfile *
// rdfs:comment "A list of WebIds for all the people this user knows." ;
}
```
To build the shape, run:
```bash
npm run build:ldo
npm i @ldo/ldo
```
This will generate five files:
- `./ldo/foafProfile.shapeTypes.ts` <-- This is the important file
- `./ldo/foafProfile.typings.ts`
- `./ldo/foafProfile.schema.ts`
- `./ldo/foafProfile.context.ts`
## Simple Example
Below is a simple example of LDO in a real use-case (changing the name on a Solid Pod)
Below is a simple example of LDO in a real use-case (changing the name on a Solid Pod). Assume that a ShapeType was previously generated and placed at `./.ldo/foafProfile.shapeTypes`.
```typescript
import { parseRdf, startTransaction, toSparqlUpdate, toTurtle } from "ldo";
import { FoafProfileShapeType } from "./ldo/foafProfile.shapeTypes";
import { parseRdf, startTransaction, toSparqlUpdate, toTurtle } from "@ldo/ldo";
import { FoafProfileShapeType } from "./.ldo/foafProfile.shapeTypes";
async function run() {
const rawTurtle = `
@ -161,200 +108,49 @@ async function run() {
run();
```
## Getting an LDO Dataset
An LDO Dataset is a kind of [RDF JS Dataset](https://rdf.js.org/dataset-spec/) that can create linked data objects.
LDO datasets can be created in two ways:
`createLdoDataset(initialDataset?: Dataset<Quad, Quad> | Quad[])`
```typescript
import { createLdoDataset } from "ldo";
const ldoDataset = createLdoDataset();
```
- `initialDataset`: An optional dataset or array of quads for the new dataset.
## API Details
`parseRdf(data: string, parserOptions?: ParserOptions)`
```typescript
import { parseRdf } from "ldo";
Types
const rawTurtle = "...";
const ldoDataset = parseRdf(rawTurtle, { baseIRI: "https://example.com/" });
```
- [`LdoBase`](https://ldo.js.org/api/ldo/LdoBase/)
- [`ShapeType`](https://ldo.js.org/api/ldo/ShapeType/)
- `data`: The raw data to parse as a `string`.
- `options` (optional): Parse options containing the following keys:
- `format` (optional): The format the data is in. The following are acceptable formats: `Turtle`, `TriG`, `N-Triples`, `N-Quads`, `N3`, `Notation3`.
- `baseIRI` (optional): If this data is hosted at a specific location, you can provide the baseIRI of that location.
- `blankNodePrefix` (optional): If blank nodes should have a prefix, that should be provided here.
- `factory` (optional): a RDF Data Factory from [`@rdfjs/data-model`](https://www.npmjs.com/package/@rdfjs/data-model).
Getting an LdoDataset
## Getting a Linked Data Object
Once you have an LdoDataset we can get a Linked Data Object. A linked data object feels just like a JavaScript object literal, but when you make modifications to it, it will affect the underlying LdoDataset.
- [`parseRdf`](https://ldo.js.org/api/ldo/parseRdf/)
- [`createLdoDatasetFactory`](https://ldo.js.org/api/ldo/createLdoDatasetFactory/)
- [`LdoDatasetFactory`](https://ldo.js.org/api/ldo/LdoDatasetFactory/)
- [`createLdoDataset`](https://ldo.js.org/api/ldo/createLdoDataset/)
- [`LdoDataset`](https://ldo.js.org/api/ldo/LdoDataset/)
Thie first step is defining which Shape Type you want to retrieve from the dataset. We can use the generated shape types and the `usingType()` method for this.
Getting a Linked Data Object
```typescript
import { FoafProfileShapeType } from "./ldo/foafProfile.shapeTypes.ts";
- [`LdoBuilder`](https://ldo.js.org/api/ldo/LdoBuilder/)
// ... Get the LdoDataset
Converting a Linked Data Object to Raw RDF
ldoDataset.usingType(FoafProfileShapeType);
```
- [`toTurtle`](https://ldo.js.org/api/ldo/toTurtle/)
- [`toNTriples`](https://ldo.js.org/api/ldo/toNTriples/)
- [`serialize`](https://ldo.js.org/api/ldo/serialize/)
Next, we want to identify exactly what part of the dataset we want to extract. We can do this in a few ways:
Transactions
### `.fromSubject(entryNode)`
`fromSubject` lets you define a an `entryNode`, the place of entry for the graph. The object returned by `jsonldDatasetProxy` will represent the given node. This parameter accepts both `namedNode`s and `blankNode`s. `fromSubject` takes a generic type representing the typescript type of the given subject.
- [transactions](https://ldo.js.org/api/ldo/transactions/)
- [`toSparqlUpdate`](https://ldo.js.org/api/ldo/toSparqlUpdate/)
```typescript
const profile = ldoDataset
.usingType(FoafProfileShapeType)
.fromSubject("http://example.com/Person1");
```
Language Tag Support
### `.matchSubject(predicate?, object?, graph?)`
`matchSubject` returns a Jsonld Dataset Proxy representing all subjects in the dataset matching the given predicate, object, and graph.
- [`languageOf`](https://ldo.js.org/api/ldo/languageOf/)
- [`setLanguagePreferences`](https://ldo.js.org/api/ldo/setLanguagePreferences/)
```typescript
const profiles = ldoDataset
.usingType(FoafProfileShapeType)
.matchSubject(
namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
namedNode("http://xmlns.com/foaf/0.1/Person")
);
profiles.forEach((person) => {
console.log(person.fn);
});
```
Graph Support
### `.matchObject(subject?, predicate?, object?)`
`matchObject` returns a Jsonld Dataset Proxy representing all objects in the dataset matching the given subject, predicate, and graph.
- [`graphOf`](https://ldo.js.org/api/ldo/graphOf/)
- [`write`](https://ldo.js.org/api/ldo/write/)
```typescript
const friendsOfPerson1 = ldoDataset
.usingType(FoafProfileShapeType)
.matchSubject(
namedNode("http://example.com/Person1"),
namedNode("http://xmlns.com/foaf/0.1/knows")
);
friendsOfPerson1.forEach((person) => {
console.log(person.fn);
});
```
### `.fromJson(inputData)`
`fromJson` will take any regular Json, add the information to the dataset, and return a Jsonld Dataset Proxy representing the given data.
Other Helper Functions
```typescript
const person2 = ldoDataset
.usingType(FoafProfileShapeType)
.fromJson({
"@id": "http://example.com/Person2",
fn: ["Jane Doe"],
});
```
## Getting and Setting Data on a Linked Data Object
Once you've created a Linked Data Object, you can get and set data as if it were a normal TypeScript Object. For specific details, see the documentation at [JSONLD Dataset Proxy](https://github.com/o-development/jsonld-dataset-proxy/blob/master/Readme.md).
```typescript
import { LinkedDataObject } from "ldo";
import { FoafProfileFactory } from "./ldo/foafProfile.ldoFactory.ts";
import { FoafProfile } from "./ldo/foafProfile.typings";
aysnc function start() {
const profile: FoafProfile = // Create LDO
// Logs "Aang"
console.log(profile.name);
// Logs "Person"
console.log(profile.type);
// Logs 1
console.log(profile.knows?.length);
// Logs "Katara"
console.log(profile.knows?.[0].name);
profile.name = "Bonzu Pippinpaddleopsicopolis III"
// Logs "Bonzu Pippinpaddleopsicopolis III"
console.log(profile.name);
profile.knows?.push({
type: "Person",
name: "Sokka"
});
// Logs 2
console.log(profile.knows?.length);
// Logs "Katara" and "Sokka"
profile.knows?.forEach((person) => console.log(person.name));
}
```
## Converting a Linked Data Object back to RDF
A linked data object can be converted into RDF in multiple ways:
### `toTurtle(linkedDataObject)`
```typescript
import { toTurtle } from "ldo"
// ...
const rawTurtle: string = await toTurtle(profile);
```
### `toNTiples(linkedDataObject)`
```typescript
import { toNTriples } from "ldo"
// ...
const rawNTriples: string = await toNTriples(profile);
```
### `serialize(linkedDataObject, options)`
```typescript
const rawTurtle: string = await profile.$serialize({
format: "Turtle",
prefixes: {
ex: "https://example.com/",
foaf: "http://xmlns.com/foaf/0.1/",
}
});
```
`serialize(linkedDataObject, options)` provides general serialization based on provided options:
- `foramt` (optional): The format to serialize to. The following are acceptable formats: `Turtle`, `TriG`, `N-Triples`, `N-Quads`, `N3`, `Notation3`.
- `prefixes`: The prefixes for those serializations that use prefixes.
## Transactions
Sometimes, you want to keep track of changes you make for the object. This is where transactions come in handy.
To start a transaction, use the `startTransaction(linkedDataObject)` function. From then on, all transactions will be tracked, but not added to the original ldoDataset. You can view the changes using the `transactionChanges(linkedDataObject)` or `toSparqlUpdate(linkedDataObject)` methods. When you're done with the transaction, you can run the `commitTransaction(linkedDataObject)` method to add the changes to the original ldoDataset.
```typescript
import {
startTransaction,
transactionChanges,
toSparqlUpdate,
commitTransaction,
} from "ldo";
// ... Get the profile linked data object
startTransaction(profile);
profile.name = "Kuzon"
const changes = transactionChanges(profile));
// Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Kuzon"
console.log(changes.added?.toString())
// Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Aang"
console.log(changes.removed?.toString())
console.log(await toSparqlUpdate(profile));
commitTransaction(profile);
```
## Other LDO Helper Functions
### `getDataset(linkedDataObject)`
Returns the Linked Data Object's underlying RDFJS dataset. Modifying this dataset will change the Linked Data Object as well.
```typescript
import { getDataset } from "ldo"
const dataset = getDataset(profile);
```
- [`getDataset`](https://ldo.js.org/api/ldo/getDataset/)
## 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,77 +0,0 @@
import { parseRdf, startTransaction, toSparqlUpdate, toTurtle } from "../src";
import { FoafProfileShapeType } from "./ldo/foafProfile.shapeTypes";
async function run() {
const rawTurtle = `
<#me> a <http://xmlns.com/foaf/0.1/Person>;
<http://xmlns.com/foaf/0.1/name> "Jane Doe".
`;
/**
* Step 1: Convert Raw RDF into a Linked Data Object
*/
const ldoDataset = await parseRdf(rawTurtle, {
baseIRI: "https://solidweb.me/jane_doe/profile/card",
});
// Create a linked data object by telling the dataset the type and subject of
// the object
const janeProfile = ldoDataset
// Tells the LDO dataset that we're looking for a FoafProfile
.usingType(FoafProfileShapeType)
// Says the subject of the FoafProfile
.fromSubject("https://solidweb.me/jane_doe/profile/card#me");
/**
* Step 2: Manipulate the Linked Data Object
*/
// Logs "Jane Doe"
console.log(janeProfile.name);
// Logs "Person"
console.log(janeProfile.type);
// Logs 0
console.log(janeProfile.knows?.length);
// Begins a transaction that tracks your changes
startTransaction(janeProfile);
janeProfile.name = "Jane Smith";
janeProfile.knows?.push({
"@id": "https://solidweb.me/john_smith/profile/card#me",
type: {
"@id": "Person",
},
name: "John Smith",
knows: [janeProfile],
});
// Logs "Jane Smith"
console.log(janeProfile.name);
// Logs "John Smith"
console.log(janeProfile.knows?.[0].name);
// Logs "Jane Smith"
console.log(janeProfile.knows?.[0].knows?.[0].name);
/**
* Step 3: Convert it back to RDF
*/
// Logs:
// <https://solidweb.me/jane_doe/profile/card#me> a <http://xmlns.com/foaf/0.1/Person>;
// <http://xmlns.com/foaf/0.1/name> "Jane Smith";
// <http://xmlns.com/foaf/0.1/knows> <https://solidweb.me/john_smith/profile/card#me>.
// <https://solidweb.me/john_smith/profile/card#me> a <http://xmlns.com/foaf/0.1/Person>;
// <http://xmlns.com/foaf/0.1/name> "John Smith";
// <http://xmlns.com/foaf/0.1/knows> <https://solidweb.me/jane_doe/profile/card#me>.
console.log(await toTurtle(janeProfile));
// Logs:
// DELETE DATA {
// <https://solidweb.me/jane_doe/profile/card#me> <http://xmlns.com/foaf/0.1/name> "Jane Doe" .
// };
// INSERT DATA {
// <https://solidweb.me/jane_doe/profile/card#me> <http://xmlns.com/foaf/0.1/name> "Jane Smith" .
// <https://solidweb.me/jane_doe/profile/card#me> <http://xmlns.com/foaf/0.1/knows> <https://solidweb.me/john_smith/profile/card#me> .
// <https://solidweb.me/john_smith/profile/card#me> <http://xmlns.com/foaf/0.1/name> "John Smith" .
// <https://solidweb.me/john_smith/profile/card#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .
// <https://solidweb.me/john_smith/profile/card#me> <http://xmlns.com/foaf/0.1/knows> <https://solidweb.me/jane_doe/profile/card#me> .
// }
console.log(await toSparqlUpdate(janeProfile));
}
run();

@ -1,26 +0,0 @@
import type { ContextDefinition } from "jsonld";
/**
* =============================================================================
* foafProfileContext: JSONLD Context for foafProfile
* =============================================================================
*/
export const foafProfileContext: ContextDefinition = {
type: {
"@id": "@type",
},
Person: "http://xmlns.com/foaf/0.1/Person",
name: {
"@id": "http://xmlns.com/foaf/0.1/name",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
img: {
"@id": "http://xmlns.com/foaf/0.1/img",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
knows: {
"@id": "http://xmlns.com/foaf/0.1/knows",
"@type": "@id",
"@container": "@set",
},
};

@ -1,93 +0,0 @@
import { Schema } from "shexj";
/**
* =============================================================================
* foafProfileSchema: ShexJ Schema for foafProfile
* =============================================================================
*/
export const foafProfileSchema: Schema = {
type: "Schema",
shapes: [
{
id: "https://example.com/FoafProfile",
type: "Shape",
expression: {
type: "EachOf",
expressions: [
{
type: "TripleConstraint",
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
valueExpr: {
type: "NodeConstraint",
values: ["http://xmlns.com/foaf/0.1/Person"],
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "Defines the node as a Person (from foaf)",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/name",
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: "Define a person's name.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/img",
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: "Photo link but in string form",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/knows",
valueExpr: "https://example.com/FoafProfile",
min: 0,
max: -1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "A list of WebIds for all the people this user knows.",
},
},
],
},
],
},
extra: ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"],
},
],
};

@ -1,19 +0,0 @@
import { ShapeType } from "../../lib";
import { foafProfileSchema } from "./foafProfile.schema";
import { foafProfileContext } from "./foafProfile.context";
import { FoafProfile } from "./foafProfile.typings";
/**
* =============================================================================
* LDO ShapeTypes foafProfile
* =============================================================================
*/
/**
* FoafProfile ShapeType
*/
export const FoafProfileShapeType: ShapeType<FoafProfile> = {
schema: foafProfileSchema,
shape: "https://example.com/FoafProfile",
context: foafProfileContext,
};

@ -1,33 +0,0 @@
import { ContextDefinition } from "jsonld";
/**
* =============================================================================
* Typescript Typings for foafProfile
* =============================================================================
*/
/**
* FoafProfile Type
*/
export interface FoafProfile {
"@id"?: string;
"@context"?: ContextDefinition;
/**
* Defines the node as a Person (from foaf)
*/
type: {
"@id": "Person";
};
/**
* Define a person's name.
*/
name?: string;
/**
* Photo link but in string form
*/
img?: string;
/**
* A list of WebIds for all the people this user knows.
*/
knows?: FoafProfile[];
}

@ -1,15 +0,0 @@
PREFIX ex: <https://example.com/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:FoafProfile EXTRA a {
a [ foaf:Person ]
// rdfs:comment "Defines the node as a Person (from foaf)" ;
foaf:name xsd:string ?
// rdfs:comment "Define a person's name." ;
foaf:img xsd:string ?
// rdfs:comment "Photo link but in string form" ;
foaf:knows @ex:FoafProfile *
// rdfs:comment "A list of WebIds for all the people this user knows." ;
}

@ -1,6 +1,6 @@
{
"name": "@ldo/ldo",
"version": "0.0.1-alpha.17",
"version": "0.0.1-alpha.18",
"description": "",
"main": "dist/index.js",
"scripts": {
@ -9,7 +9,8 @@
"test": "jest --coverage",
"test:watch": "jest --watch --coverage",
"prepublishOnly": "npm run test && npm run build",
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern"
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern",
"docs": "typedoc --plugin typedoc-plugin-markdown"
},
"repository": {
"type": "git",
@ -32,12 +33,14 @@
"cross-fetch": "^3.1.5",
"jest": "^27.4.5",
"ts-jest": "^27.1.2",
"ts-node": "^10.4.0"
"ts-node": "^10.4.0",
"typedoc": "^0.25.4",
"typedoc-plugin-markdown": "^3.17.1"
},
"dependencies": {
"@ldo/dataset": "^0.0.1-alpha.17",
"@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.17",
"@ldo/subscribable-dataset": "^0.0.1-alpha.17",
"@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.18",
"@ldo/subscribable-dataset": "^0.0.1-alpha.18",
"@rdfjs/data-model": "^1.2.0",
"buffer": "^6.0.3",
"readable-stream": "^4.3.0"

@ -8,13 +8,37 @@ import type { LdoBase } from "./util";
import { normalizeNodeName, normalizeNodeNames } from "./util";
/**
* A wrapper around Jsonld Dataset Proxy Builder with a slightly more friendly
* user experience that doesn't require the use of rdfjs datatypes.
* An LdoBuilder contains utility methods for building a Linked Data Object for a certain type.
*
* It is not recommended to instantiate an LdoDataset. Instead use the {@link createLdoDataset} function.
*
* @typeParam Type - The TypeScript type of the eventual Linked Data Object.
*
* @example
* ```typescript
* import { LdoDataset, createLdoDatasetFactory } from "@ldo/ldo";
* import { FoafProfileShapeType } from "./.ldo/foafProfile.shapeTypes";
*
* const ldoDataset = createLdoDataset();
* const ldoBuilder = ldoDataset.usingType(FoafProfileShapeType);
* const profile = ldoBuilder
* .write("https://example.com/someGraph")
* .fromSubject("https://example.com/profile#me");
* ```
*/
export class LdoBuilder<Type extends LdoBase> {
/**
* @internal
*/
private jsonldDatasetProxyBuilder: JsonldDatasetProxyBuilder;
private shapeType: ShapeType<Type>;
/**
* Initializes the LdoBuilder
*
* @param jsonldDatasetProxyBuilder - A base JsonldDatasetProxyBuilder that thios LdoBuilder wraps
* @param shapeType - The ShapeType for this builder
*/
constructor(
jsonldDatasetProxyBuilder: JsonldDatasetProxyBuilder,
shapeType: ShapeType<Type>,
@ -24,34 +48,18 @@ export class LdoBuilder<Type extends LdoBase> {
}
/**
* Designates that all Linked Data Objects created should write to the
* specified graphs
*/
write(...graphs: (GraphNode | string)[]): LdoBuilder<Type> {
return new LdoBuilder(
this.jsonldDatasetProxyBuilder.write(...normalizeNodeNames(graphs)),
this.shapeType,
);
}
/**
* Sets the order of language preferences for Language Strings. Acceptable
* values as EITF language tags, "@none" and "@other"
*/
setLanguagePreferences(
...languageOrdering: LanguageOrdering
): LdoBuilder<Type> {
return new LdoBuilder(
this.jsonldDatasetProxyBuilder.setLanguagePreferences(
...languageOrdering,
),
this.shapeType,
);
}
/**
* Creates a Linked Data Object that matches the given subject
* @param subject The node to match
* `fromSubject` lets you define a an `entryNode`, the place of entry for the graph. The object returned by `jsonldDatasetProxy` will represent the given node. This parameter accepts both `namedNode`s and `blankNode`s. `fromSubject` takes a generic type representing the typescript type of the given subject.
*
* @param subject - The node to match
*
* @returns A Linked Data Object for the provided subject.
*
* @example
* ```typescript
* const profile = ldoDataset
* .usingType(FoafProfileShapeType)
* .fromSubject("http://example.com/Person1");
* ```
*/
fromSubject(subject: SubjectNode | string): Type {
return this.jsonldDatasetProxyBuilder.fromSubject<Type>(
@ -60,11 +68,26 @@ export class LdoBuilder<Type extends LdoBase> {
}
/**
* Matches Subjects to provided predicates, objects, and graphs. Returns a
* JSON LD Dataset that can be read an modified.
* @param predicate The predicate to match
* @param object The object to match
* @param graph The graph to match
* `matchSubject` returns a Jsonld Dataset Proxy representing all subjects in the dataset matching the given predicate, object, and graph.
*
* @param predicate - A valid Predicate Node (NamedNode) or a string URI.
* @param object - A valid object node (NamedNode, Blank Node, or Literal) or a string URI.
* @param graph - A valid graph node (NamedNode or DefaultGraph) or a string URI.
*
* @returns A Linked Data Object Array with all subjects the match the provided nodes.
*
* @example
* ```typescript
* const profiles = ldoDataset
* .usingType(FoafProfileShapeType)
* .matchSubject(
* namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
* namedNode("http://xmlns.com/foaf/0.1/Person")
* );
* profiles.forEach((person) => {
* console.log(person.fn);
* });
* ```
*/
matchSubject(
predicate: QuadMatch[1] | string,
@ -79,11 +102,22 @@ export class LdoBuilder<Type extends LdoBase> {
}
/**
* Matches Objects to provided subjects, predicates, and graphs. Returns a
* collection of Linked Data Objects that can be read an modified.
* @param subject The subject to match
* @param predicate The predicate to match
* @param graph The graph to match
* `matchObject` returns a Jsonld Dataset Proxy representing all objects in the dataset matching the given subject, predicate, and graph.
*
* @param subject - A valid object node (NamedNode or Blank Node) or a string URI.
* @param predicate - A valid Predicate Node (NamedNode) or a string URI.
* @param graph - A valid graph node (NamedNode or DefaultGraph) or a string URI.
*
* @returns A Linked Data Object Array with all objects the match the provided nodes.
*
* @example
* ```typescript
* matchObject(
* subject?: SubjectNode | string,
* predicate?: PredicateNode | string,
* graph?: GraphNode | string,
* ): Type[]
* ```
*/
matchObject(
subject?: QuadMatch[0] | string,
@ -98,13 +132,78 @@ export class LdoBuilder<Type extends LdoBase> {
}
/**
* Takes a given object and places it in the dataset while returning a Linked
* Data Object representing the object.
* `fromJson` will take any regular Json, add the information to the dataset, and return a Jsonld Dataset Proxy representing the given data.
*
* @param inputData - Initial data matching the type
* @returns A linked data object or linked data object array depending on the input
*
* @param inputData Initial Data
* @param graph Optional graph to save this data to
* @example
* ```typescript
* const person2 = ldoDataset
* .usingType(FoafProfileShapeType)
* .fromJson({
* "@id": "http://example.com/Person2",
* fn: ["Jane Doe"],
* });
* ```
*/
fromJson(inputData: Type): Type {
return this.jsonldDatasetProxyBuilder.fromJson<Type>(inputData);
}
/**
* Designates that all Linked Data Objects created should write to the specified graphs. By default, all new quads are added to the default graph, but you can change the graph to which new quads are added.
*
* NOTE: These operations only dictate the graph for new triples. Any operations that delete triples will delete triples regardless of their graph.
*
* @param graphs - any number of Graph Nodes or string URIs that all add operations will be put in.
*
* @returns An LdoBuilder for constructor chaining
*
* @example
* ```typescript
* const person1 = ldoDataset.usingType(FoafShapeType)
* .write(namedNode("http://example.com/ExampleGraph"))
* .fromSubject(namedNode("http://example.com/Person1"));
* person1.name.push("Jack");
* console.log(dataset.toString());
* // Logs:
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "Jack" <http://example.com/ExampleGraph> .
* ```
*/
write(...graphs: (GraphNode | string)[]): LdoBuilder<Type> {
return new LdoBuilder(
this.jsonldDatasetProxyBuilder.write(...normalizeNodeNames(graphs)),
this.shapeType,
);
}
/**
* Sets the order of language preferences for Language Strings. Acceptable values are EITF language tags, "@none" and "@other"
*
* @param languageOrdering - The order languages will be selected. Acceptable values are EITF language tags, "@none" and "@other".
*
* @returns An LdoBuilder for constructor chaining
*
* @example
* ```typescript
* // Read Spansih first, then Korean, then language strings with no language
* // New writes are in Spanish
* ["es", "ko", "@none"]
*
* // Read any language other than french, then french
* // New writes are in French
* ["@other", "fr"]
* ```
*/
setLanguagePreferences(
...languageOrdering: LanguageOrdering
): LdoBuilder<Type> {
return new LdoBuilder(
this.jsonldDatasetProxyBuilder.setLanguagePreferences(
...languageOrdering,
),
this.shapeType,
);
}
}

@ -6,12 +6,27 @@ import type { ShapeType } from "./ShapeType";
import type { LdoBase } from "./index";
/**
* Utility for building a linked data object
* @category Getting an LdoDataset
*
* An LdoDataset is a utility for building a linked data object.
*
* It is not recommended to instantiate an LdoDataset. Instead use the {@link createLdoDataset} function.
*
* @example
* ```typescript
* import { LdoDataset, createLdoDatasetFactory } from "@ldo/ldo";
* import { FoafProfileShapeType } from "./.ldo/foafProfile.shapeTypes";
*
* const ldoDatasetFactory = createLdoDatasetFactory();
* const ldoDataset = new LdoDataset();
* const ldoBuilder = ldoDataset.usingType(FoafProfileShapeType);
* ```
*/
export class LdoDataset extends WrapperSubscribableDataset<Quad> {
/**
* Gets a builder for a given type
* @param shapeType A ShapeType
* Creates an LdoBuilder for a given shapeType
*
* @param shapeType - A ShapeType
* @returns A builder for the given type
*/
public usingType<Type extends LdoBase>(

@ -2,14 +2,36 @@ import type { DatasetFactory, Dataset, Quad } from "@rdfjs/types";
import { LdoDataset } from "./LdoDataset";
/**
* A DatasetFactory that creates an ExtendedDataset given a DatasetCoreFactory.
* @category Getting an LdoDataset
* `LdoDatasetFactory` is a helper class that includes methods for creating LdoDatasets.
*
* This class exists for parity with RDF/JS. Most developers will not use it. Instead, it's better to use {@link createLdoDataset}.
*
* @example
* ```typescript
* import { createLdoDatasetFactory } from "ldo";
*
* const datasetFactory = // some RDF/JS Dataset Factory
* const ldoDatasetFactory = new LdoDatasetFactory(datasetFactory);
* const ldoDataset = ldoDatasetFactory.dataset(initialDataset);
* ```
*/
export class LdoDatasetFactory implements DatasetFactory<Quad, Quad> {
private datasetFactory: DatasetFactory<Quad, Quad>;
/**
* @constructor
* @param datasetFactory - A generic dataset factory this factory will wrap
*/
constructor(datasetFactory: DatasetFactory<Quad, Quad>) {
this.datasetFactory = datasetFactory;
}
/**
* Creates an LdoDataset
* @param quads - A list of quads to initialize the dataset
* @returns an LdoDataset
*/
dataset(quads?: Dataset<Quad, Quad> | Quad[]): LdoDataset {
return new LdoDataset(
this.datasetFactory,

@ -2,12 +2,55 @@ import type { ContextDefinition } from "jsonld";
import type { Schema } from "shexj";
import type { LdoBase } from "./util";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/**
* @category Types
*
* A `ShapeType` is an interface that contains information required by LDO to create a Linked Data Object. ShapeTypes are generated by the `@ldo/cli` `build` command, and it contains the following data:
*
* - ShEx Schema
* - Generated Typescript Typings
* - Generated Context
*
* @example
* The following is the ShapeType generated for the example `FoafProfile.shex` file.
* ```typescript
* import { ShapeType } from "ldo";
* import { foafProfileSchema } from "./foafProfile.schema";
* import { foafProfileContext } from "./foafProfile.context";
* import { FoafProfile } from "./foafProfile.typings";
*
* export const FoafProfileShapeType: ShapeType<FoafProfile> = {
* schema: foafProfileSchema,
* shape: "https://example.com/FoafProfile",
* context: foafProfileContext,
* };
* ```
*
* This can be imported and used to create a Linked Data Object.
* ```typescript
* import { FoafProfileShapeType } from "./.ldo/foafProfile.shapeTypes";
* import { FoafProfile } from "./.ldo/foafProfile.typings";
*
* const profile: FoafProfile = ldoDataset
* .usingType(FoafProfileShapeType)
* .fromSubject("https://example.com/profile.ttl#me");
* ```
*/
export type ShapeType<Type extends LdoBase> = {
/**
* The ShEx Schema
*/
schema: Schema;
/**
* The key of the shape within the schema to which this ShapeType refers
*/
shape: string;
/**
* A JSON-LD context generated for this Schema
*/
context: ContextDefinition;
// This field is optional. It's main point is to allow the typescript parser to
// understand that this shape type is of a specific type.
/**
* This field is optional and the `@ldo/cli` `build` command will not provide this field. The `exampleData` field's primary purpose is to force the TypeScript parser to recognize the provided `Type` so it can be auto-inferenced in the `LdoDataset.usingType` method.
*/
exampleData?: Type;
};

@ -1,7 +1,21 @@
import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types";
import { createDataset } from "@ldo/dataset";
import { LdoDatasetFactory } from "./LdoDatasetFactory";
import type { LdoDataset } from "./LdoDataset";
/**
* @category Getting an LdoDataset
*
* A helper function that creates an LdoDatasetFactory.
* This function exists for parity with RDF/JS. Most developers will not use it. Instead, it's better to use {@link createLdoDataset}.
*
* @returns An LDO Dataset Factory
*
* @example
* ```typescript
* createLdoDatasetFactory(): Promise<LdoDatasetFactory>
* ```
*/
export function createLdoDatasetFactory() {
const datasetFactory: DatasetFactory<Quad> = {
dataset: (quads?: Dataset<Quad> | Quad[]): Dataset<Quad> => {
@ -11,9 +25,25 @@ export function createLdoDatasetFactory() {
return new LdoDatasetFactory(datasetFactory);
}
/**
* @category Getting an LdoDataset
*
* A function that initializes an LdoDataset.
*
* @param initialDataset - An optional dataset or array of quads for the new dataset
*
* @returns An LDO Dataset initialized with the intitial dataset if any
*
* @example
* ```typescript
* import { createLdoDataset } from "@ldo/ldo";
*
* const ldoDataset = createLdoDataset();
* ```
*/
export function createLdoDataset(
initialDataset?: Dataset<Quad, Quad> | Quad[],
) {
): LdoDataset {
const ldoDatasetFactory = createLdoDatasetFactory();
return ldoDatasetFactory.dataset(initialDataset);
}

@ -18,19 +18,216 @@ import {
normalizeNodeNames,
} from "./util";
export {
graphOf,
languagesOf,
setLanguagePreferences,
import {
graphOf as graphOfImport,
languagesOf as languagesOfImport,
setLanguagePreferences as setLanguagePreferencesImport,
} from "@ldo/jsonld-dataset-proxy";
/**
* The graph of specific information can be detected using the `graphOf(subject, predicate, object)` function.
*
* @param subject - A Linked Data Object that represents the subject of a quad.
* @param predicate - A field on the given Linked Data Object
* @param object - An optional parameter that represents the direct object of a statement. This could be a Linked Data Object or a number to indicate the location in an array. This argument can be left blank if the given field is not an array.
*
* @returns A GraphNode (defaultGraph or namedNode).
*
* @example
* ```typescript
* import { graphOf } from "@ldo/ldo";
* graphOf(person, "name", 0); // returns defaultGraph()
* graphOf(person, "age"); // returns defaultGraph()
* ```
*/
export const graphOf = graphOfImport;
/**
* The `languageOf` function lets you view and modify the language strings directly. `languageOf` takes two properties:
*
* It returns a mapping of languages to strings or sets of strings depending on the cardinality of the JSON-LD context.
*
* @param ldo - Any Linked Data Object
* @param field - Any field on the provided Linked Data Object
*
* @returns `languageOf` returns either a `LanguageSetMap` if the given field is an array, or a `LanguageMap` if the given field is a singular value. For example, `languageOf(profile, "friends")` would return a `LanguageSetMap` because there the `listOfFriendNames` field has a cardinality over 1, but `languageOf(profile, "familyName")` would return a `LanguageMap` because it has a cardinality of 1.
*
* @example
* This example uses a `LanguageMap`. The `LanguageMap` is a mapping between various language tags (including the `@none` tag) and the singular language value for that tag. Modifying the `LanguageMap` will automatically update the underlying dataset.
* ```typescript
* const labelLanguages = languagesOf(hospitalInfo, "label");
* // labelLanguages: { '@none': 'Hospital', fr: 'Hôpital', ko: '병원' }
* // logs "병원"
* console.log(labelLanguages.ko);
* // Adds a Chinese label
* labelLanguages.zh = "医院";
* // Changes the no-language label from to "Super Hospital"
* labelLanguages["@none"] = "Super Hospital";
* // Removes the French label
* delete labelLanguages.fr;
* ```
* @example
* This example uses a `LanguageSetMap` The `LanguageSetMap` is a mapping between various language tags (including the `@none` tag) and a JavaScript Set of all values for that tag. Modifying the `LanguageSetMap` will automatically update the underlying dataset.
* ```typescript
* const descriptionLanguages = languagesOf(hospitalInfo, "description");
* // descriptionLanguages:
* // {
* // '@none': Set(2) { 'Heals patients', 'Has doctors' },
* // fr: Set(2) { 'Guérit les malades', 'A des médecins' },
* // ko: Set(2) { '환자를 치료하다', '의사 있음' }
* // }
* // Logs: 환자를 치료하다\n의사 있음
* Array.from(descriptionLanguages.ko).forEach((str) => console.log(str));
* // Adds a Hindi description
* descriptionLanguages.hi?.add("रिक करत");
* // Checks to see if the korean label contains "의사 있음"
* descriptionLanguages.ko?.has("의사 있음"); // returns true
* // Removes "Has Doctors" from the no-language description
* descriptionLanguages["@none"]?.delete("Has Doctors");
* ```
*/
export const languagesOf = languagesOfImport;
/**
* A language preference is an ordered list telling the Linked Data Object the language you prefer as well as callbacks.
*
* For read operations, the Linked Data Object will search for values in order of the preference. Write operations will choose the first language in the language preference, unless that language is `@other`, in which case it will choose the next language.
*
* @example
* A language ordering is an ordering of preferred languages. Valid values for the language preferences includes any [IETF Language Tag](https://en.wikipedia.org/wiki/IETF_language_tag) as well as the special tags `@none` and `@other`. `@none` represents any language literal that doesn't have a language tag. `@other` represents any language literal that isn't listed among the language preferences.
*
* ```typescript
* // Read Spansih first, then Korean, then language strings with no language
* // New writes are in Spanish
* ["es", "ko", "@none"]
*
* // Read any language other than french, then french
* // New writes are in French
* ["@other", "fr"]
* ```
*
* @example
* The `setLanguagePreferences(...).using(...)` function sets the language preferences for a set of Linked Data Objects.
*
* ```typescript
* import { setLanguagePreferences } from "@ldo/ldo";
*
* setLanguagePreferences("fr", "ko").using(hospitalInfo);
* console.log(hospitalInfo.label); // Logs "Hôpital"
* setLanguagePreferences("@none").using(hospitalInfo);
* console.log(hospitalInfo.label); // Logs "Hospital"
* ```
*
* @example
* The `setLanguagePreferences(...).usingCopy(...)` function returns a copy of the provided Linked Data Objects with the given language preferences.
*
* ```typescript
* import { setLanguagePreferences } from "@ldo/ldo";
*
* // ...
*
* const [frenchPreference] = setLanguagePreferences("fr").usingCopy(hospitalInfo);
* const [koreanPreference] = setLanguagePreferences("ko").usingCopy(hospitalInfo);
* console.log(frenchPreference.label); // Logs "Hôpital"
* console.log(koreanPreference.label); // Logs "병원"
* ```
*/
export const setLanguagePreferences = setLanguagePreferencesImport;
/**
* By default, all new quads are added to the default graph, but you can change the graph to which new quads are added using the `write` function.
*
* @example
* The `write(...).using(...)` function lets you define the graphs you wish to write to using specific jsonldDatasetProxies.
*
* ```typescript
* import { write } from "@ldo/ldo";
*
* // Now all additions with person1 will be on ExampleGraph1
* write(namedNode("http://example.com/ExampleGraph1")).using(person1);
* person1.name.push("Jack");
* // Now all additions with person1 will be on ExampleGraph2
* write(namedNode("http://example.com/ExampleGraph2")).using(person1);
* person1.name.push("Spicer");
*
* console.log(dataset.toString());
* // Logs:
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "Jack" <http://example.com/ExampleGraph1> .
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "Spicer" <http://example.com/ExampleGraph2> .
* ```
*
* The function also returns an `end` function that will reset the graph to what it was before. This is useful for nesting graph modifications.
*
* ```typescript
* person1.name.push("default");
* const end1 = write(namedNode("http://example.com/Graph1")).using(person1);
* person1.name.push("1");
* const end2 = write(namedNode("http://example.com/Graph2")).using(person1);
* person1.name.push("2");
* const end3 = write(namedNode("http://example.com/Graph3")).using(person1);
* person1.name.push("3");
* end3();
* person1.name.push("2 again");
* end2();
* person1.name.push("1 again");
* end1();
* person1.name.push("default again");
* console.log(dataset.toString());
* // Logs:
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "default" .
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "default again" .
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "1" <http://example.com/Graph1> .
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "1 again" <http://example.com/Graph1> .
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "2" <http://example.com/Graph2> .
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "2 again" <http://example.com/Graph2> .
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "3" <http://example.com/Graph3> .
* ```
* @example
* If you would like a new variable to write to without modifying the original Jsonld Dataset Proxy, you can use `write(...).usingCopy(...)`.
*
* ```typescript
* const [person1WritingToNewGraph] = write(
* namedNode("http://example.com/NewGraph")
* ).usingCopy(person1);
* person1WritingToNewGraph.name.push("Brandon");
* person1.name.push("Sanderson");
* console.log(dataset.toString());
* // Logs:
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "Brandon" <http://example.com/NewGraph> .
* // <http://example.com/Person1> <http://xmlns.com/foaf/0.1/name> "Sanderson" .
* ```
*/
export function write(...graphs: (GraphNode | string)[]): InteractOptions {
return writeDependency(...normalizeNodeNames(graphs));
}
/**
* Begins a transaction for the given linked data object
* @param ldo
* Begins a transaction for a Linked Data Object. After this function is run, the Linked Data Object is considered to be "transactable" where all modifications are not written to the underlying dataset are stored separately as a delta.
*
* Note: If a Linked Data Object is "transactable", it cannot be passed into `startTransaction` a second time.
*
* @param ldo - Any linked data object that is not currently "transactable"
*
* @example
* ```typescript
* import {
* startTransaction,
* transactionChanges,
* toSparqlUpdate,
* commitTransaction,
* } from "@ldo/ldo";
*
* // ... Get the profile linked data object
*
* startTransaction(profile);
* profile.name = "Kuzon"
* const changes = transactionChanges(profile));
* // Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Kuzon"
* console.log(changes.added?.toString())
* // Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Aang"
* console.log(changes.removed?.toString())
* commitTransaction(profile);
* ```
*/
export function startTransaction(ldo: LdoBase): void {
const proxy = getProxyFromObject(ldo);
@ -45,8 +242,30 @@ export function startTransaction(ldo: LdoBase): void {
}
/**
* Ends a transaction and commits the
* @param ldo
* Commits a transaction, writing all the stored changes to the underlying dataset. After this function is run, the Linked Data Object is considered to be NOT "transactable" all further modifications will be written directly to the underlying dataset.
*
* @param ldo - Any linked data object that is currently "transactable"
*
* @example
* ```typescript
* import {
* startTransaction,
* transactionChanges,
* toSparqlUpdate,
* commitTransaction,
* } from "@ldo/ldo";
*
* // ... Get the profile linked data object
*
* startTransaction(profile);
* profile.name = "Kuzon"
* const changes = transactionChanges(profile));
* // Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Kuzon"
* console.log(changes.added?.toString())
* // Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Aang"
* console.log(changes.removed?.toString())
* commitTransaction(profile);
* ```
*/
export function commitTransaction(ldo: LdoBase): void {
const [dataset, proxy] = getTransactionalDatasetFromLdo(ldo);
@ -57,22 +276,93 @@ export function commitTransaction(ldo: LdoBase): void {
});
}
/**
* Returns the changes that are made on a "transactable" Linked Data Object.
*
* @param ldo - Any linked data object that is currently "transactable"
*
* @returns Dataset changes with all quads added during this transaction and all quads removed during this transaction
*
* @example
* ```typescript
* import {
* startTransaction,
* transactionChanges,
* toSparqlUpdate,
* commitTransaction,
* } from "@ldo/ldo";
*
* // ... Get the profile linked data object
*
* startTransaction(profile);
* profile.name = "Kuzon"
* const changes = transactionChanges(profile));
* // Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Kuzon"
* console.log(changes.added?.toString())
* // Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Aang"
* console.log(changes.removed?.toString())
* commitTransaction(profile);
* ```
*/
export function transactionChanges(ldo: LdoBase): DatasetChanges<Quad> {
const [dataset] = getTransactionalDatasetFromLdo(ldo);
return dataset.getChanges();
}
/**
* Returns the Linked Data Object's underlying RDFJS dataset. Modifying this dataset will change the Linked Data Object as well.
*
* @param ldo - The Linked Data Object from which the RDFJS dataset should be extracted.
*
* @returns An RDFJS dataset
*
* @example
* ```typescript
* import { getDataset } from "@ldo/ldo"
* const dataset = getDataset(profile);
* ```
*/
export function getDataset(ldo: LdoBase): Dataset {
const proxy = getProxyFromObject(ldo);
return proxy[_getUnderlyingDataset];
}
/**
* Converts a "transactable" Linked Data Object (A Linked Data Object that has been passed as a parameter to the `startTransaction` function) to a SPARQL/Update string.
*
* @param ldo - Any linked data object that is currently "transactable"
*
* @returns A SPARQL Update string
*/
export async function toSparqlUpdate(ldo: LdoBase): Promise<string> {
const [dataset] = getTransactionalDatasetFromLdo(ldo);
const changes = dataset.getChanges();
return changesToSparqlUpdate(changes);
}
/**
* Converts a Linked Data Object to a string representation based on a provided configuration.
*
* @param ldo - Any linked data object
* @param options - WriterOptions from N3
* @param options.format - `string | MimeFormat | undefined` The name of the format to serialize.
* @param options.prefixes - `Prefixes<RDF.NamedNode | string> | - undefined` A list of prefixes that should be in the document.
*
* @returns Serialized N3 RDF
*
* @example
* ```typescript
* import { serialize } from "@ldo/ldo"
* // ...
* const rawTurtle: string = await serialize(profile, {
* format: "Turtle",
* prefixes: {
* ex: "https://example.com/",
* foaf: "http://xmlns.com/foaf/0.1/",
* }
* });
* ```
*/
export async function serialize(
ldo: LdoBase,
options: WriterOptions,
@ -81,6 +371,19 @@ export async function serialize(
return datasetToString(dataset, options);
}
/**
* Converts a Linked Data Object to a Turtle string
*
* @param ldo - Any linked data object
* @returns Serialized Turtle
*
* @example
* ```typescript
* import { toTurtle } from "@ldo/ldo"
* // ...
* const rawTurtle: string = await toTurtle(profile);
* ```
*/
export async function toTurtle(ldo: LdoBase): Promise<string> {
const dataset = getProxyFromObject(ldo)[_getUnderlyingDataset];
return datasetToString(dataset, {});
@ -90,6 +393,19 @@ export async function toJsonLd(_ldo: LdoBase): Promise<JsonLdDocument> {
throw new Error("Not Implemented");
}
/**
* Converts a Linked Data Object to a NTriples string
*
* @param ldo - Any linked data object
* @returns An N-Triple string
*
* @example
* ```typescript
* import { toNTriples } from "@ldo/ldo"
* // ...
* const rawNTriples: string = await toNTriples(profile);
* ```
*/
export async function toNTriples(ldo: LdoBase): Promise<string> {
const dataset = getProxyFromObject(ldo)[_getUnderlyingDataset];
return datasetToString(dataset, { format: "N-Triples" });

@ -1,10 +1,32 @@
import type { Dataset } from "@rdfjs/types";
import type { JsonLdDocument } from "jsonld";
import type { ParserOptions } from "@ldo/rdf-utils";
import type { ParserOptions } from "n3";
import { createDatasetFromSerializedInput } from "@ldo/dataset";
import { createLdoDataset, createLdoDatasetFactory } from "./createLdoDataset";
import type { LdoDataset } from "./LdoDataset";
/**
* @category Getting an LdoDataset
*
* Parses raw RDF and puts its results into an LdoDataset.
*
* @param data - The raw data to parse as a `string`.
* @param parserOptions - Parser options from n3
* @param parserOptions.format - The format the data is in. The following are acceptable formats: `Turtle`, `TriG`, `N-Triples`, `N-Quads`, `N3`, `Notation3`.
* @param parserOptions.baseIRI - If this data is hosted at a specific location, you can provide the baseIRI of that location.
* @param parserOptions.blankNodePrefix - If blank nodes should have a prefix, that should be provided here.
* @param parserOptions.factory - a RDF Data Factory from [`@rdfjs/data-model`](https://www.npmjs.com/package/@rdfjs/data-model).
*
* @returns An LdoDataset containing the parsed triples
*
* @example
* ```typescript
* import { parseRdf } from "ldo";
*
* const rawTurtle = "...";
* const ldoDataset = parseRdf(rawTurtle, { baseIRI: "https://example.com/" });
* ```
*/
export async function parseRdf(
data: string | JsonLdDocument | Dataset,
parserOptions?: ParserOptions,

@ -12,7 +12,10 @@ import type {
TransactionalDataset,
} from "@ldo/subscribable-dataset";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/**
* @category Types
* `LdoBase` is an interface defining that a Linked Data Object is a JavaScript Object Literal.
*/
export type LdoBase = Record<string, any>;
/**

@ -0,0 +1,6 @@
{
"entryPoints": ["src/index.ts"],
"out": "docs",
"allReflectionsHaveOwnDocument": true,
"hideInPageTOC": true
}

@ -1,5 +1,6 @@
const sharedConfig = require('../../jest.config.js');
const sharedConfig = require("../../jest.config.js");
module.exports = {
...sharedConfig,
'rootDir': './',
}
rootDir: "./",
testEnvironment: "jsdom",
};

@ -0,0 +1,2 @@
import "@inrupt/jest-jsdom-polyfills";
globalThis.fetch = async () => new Response();

@ -1,16 +1,19 @@
{
"name": "@ldo/solid-react",
"version": "0.0.1-alpha.17",
"version": "0.0.1-alpha.18",
"description": "A React library for LDO and Solid",
"main": "dist/index.js",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"watch": "tsc --watch",
"test": "jest --coverage",
"test": "npm run test:integration",
"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"
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern",
"test:integration": "start-server-and-test start-test-server http://localhost:3001 start-integration-test",
"start-test-server": "ts-node ./test/test-server/runServer.ts",
"start-integration-test": "jest --coverage"
},
"repository": {
"type": "git",
@ -23,20 +26,22 @@
},
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid-react#readme",
"devDependencies": {
"@inrupt/solid-client-authn-core": "^1.17.1",
"@ldo/rdf-utils": "^0.0.1-alpha.17",
"@rdfjs/types": "^1.0.1",
"@testing-library/react": "^14.1.2",
"@types/jest": "^29.0.3",
"ts-jest": "^29.0.2"
"jest-environment-jsdom": "^29.7.0",
"start-server-and-test": "^2.0.3",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2"
},
"dependencies": {
"@inrupt/solid-client": "^1.29.0",
"@inrupt/solid-client-authn-browser": "^1.17.1",
"@inrupt/solid-client": "^2.0.0",
"@ldo/dataset": "^0.0.1-alpha.17",
"@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.17",
"@ldo/ldo": "^0.0.1-alpha.17",
"@ldo/solid": "^0.0.1-alpha.17",
"@ldo/subscribable-dataset": "^0.0.1-alpha.17",
"@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.18",
"@ldo/ldo": "^0.0.1-alpha.18",
"@ldo/solid": "^0.0.1-alpha.18",
"@ldo/subscribable-dataset": "^0.0.1-alpha.18",
"@rdfjs/data-model": "^1.2.0",
"cross-fetch": "^3.1.6"
},

@ -0,0 +1,62 @@
/* istanbul ignore file */
import React, { useCallback, useMemo } from "react";
import type { FunctionComponent, PropsWithChildren } from "react";
import type { LoginOptions, SessionInfo } from "./SolidAuthContext";
import { SolidAuthContext } from "./SolidAuthContext";
import libraryFetch from "cross-fetch";
import { SolidLdoProvider } from "./SolidLdoProvider";
const DUMMY_SESSION: SessionInfo = {
isLoggedIn: false,
webId: undefined,
clientAppId: undefined,
sessionId: "no_session",
expirationDate: undefined,
};
export const UnauthenticatedSolidLdoProvider: FunctionComponent<
PropsWithChildren
> = ({ children }) => {
const login = useCallback(
async (_issuer: string, _options?: LoginOptions) => {
throw new Error(
"login is not available for a UnauthenticatedSolidLdoProvider",
);
},
[],
);
const logout = useCallback(async () => {
throw new Error(
"logout is not available for a UnauthenticatedSolidLdoProvider",
);
}, []);
const signUp = useCallback(
async (_issuer: string, _options?: LoginOptions) => {
throw new Error(
"signUp is not available for a UnauthenticatedSolidLdoProvider",
);
},
[],
);
const solidAuthFunctions = useMemo(
() => ({
runInitialAuthCheck: () => {},
login,
logout,
signUp,
session: DUMMY_SESSION,
ranInitialAuthCheck: true,
fetch: libraryFetch,
}),
[login, logout, signUp],
);
return (
<SolidAuthContext.Provider value={solidAuthFunctions}>
<SolidLdoProvider>{children}</SolidLdoProvider>
</SolidAuthContext.Provider>
);
};

@ -1,4 +1,5 @@
export * from "./BrowserSolidLdoProvider";
export * from "./UnauthenticatedSolidLdoProvider";
export * from "./SolidAuthContext";
export { useLdo } from "./SolidLdoProvider";

@ -1,10 +1,7 @@
import type { LdoBase, ShapeType } from "@ldo/ldo";
import { transactionChanges } from "@ldo/ldo";
import { write } from "@ldo/ldo";
import { startTransaction } from "@ldo/ldo";
import type { DatasetChanges, SubjectNode } from "@ldo/rdf-utils";
import type { SubjectNode } from "@ldo/rdf-utils";
import type { Resource, SolidLdoDataset } from "@ldo/solid";
import type { Quad } from "@rdfjs/types";
import { changeData, commitData } from "@ldo/solid";
export interface UseLdoMethods {
dataset: SolidLdoDataset;
@ -12,13 +9,18 @@ export interface UseLdoMethods {
getSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type | Error;
): Type;
createData<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
...resources: Resource[]
resource: Resource,
...additionalResources: Resource[]
): Type;
changeData<Type extends LdoBase>(
input: Type,
resource: Resource,
...additionalResources: Resource[]
): Type;
changeData<Type extends LdoBase>(input: Type, ...resources: Resource[]): Type;
commitData(input: LdoBase): ReturnType<SolidLdoDataset["commitChangesToPod"]>;
}
@ -38,7 +40,7 @@ export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods {
getSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type | Error {
): Type {
return dataset.usingType(shapeType).fromSubject(subject);
},
/**
@ -51,42 +53,26 @@ export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods {
createData<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
...resources: Resource[]
resource: Resource,
...additionalResources: Resource[]
): Type {
const linkedDataObject = dataset
.usingType(shapeType)
.write(...resources.map((r) => r.uri))
.fromSubject(subject);
startTransaction(linkedDataObject);
return linkedDataObject;
return dataset.createData(
shapeType,
subject,
resource,
...additionalResources,
);
},
/**
* Begins tracking changes to eventually commit
* @param input A linked data object to track changes on
* @param resources
*/
changeData<Type extends LdoBase>(
input: Type,
...resources: Resource[]
): Type {
// 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;
},
changeData: changeData,
/**
* Commits the transaction to the global dataset, syncing all subscribing
* components and Solid Pods
*/
commitData(
input: LdoBase,
): ReturnType<SolidLdoDataset["commitChangesToPod"]> {
const changes = transactionChanges(input);
return dataset.commitChangesToPod(changes as DatasetChanges<Quad>);
},
commitData: commitData,
};
}

@ -70,6 +70,7 @@ export function useResource(
},
[resource],
);
useEffect(() => {
// Remove listeners for the previous resource
if (pastResource.current?.resource) {

@ -1,8 +1,13 @@
import type { Container, ContainerUri } from "@ldo/solid";
import { useEffect, useState } from "react";
import type { UseResourceOptions } from "./useResource";
import { useResource } from "./useResource";
import { useLdo } from "./SolidLdoProvider";
export function useRootContainerFor(uri?: string): Container | undefined {
export function useRootContainerFor(
uri?: string,
options?: UseResourceOptions,
): Container | undefined {
const { getResource } = useLdo();
const [rootContainerUri, setRootContainerUri] = useState<
@ -17,8 +22,10 @@ export function useRootContainerFor(uri?: string): Container | undefined {
setRootContainerUri(result.uri);
}
});
} else {
setRootContainerUri(undefined);
}
}, [uri]);
return useResource(rootContainerUri);
return useResource(rootContainerUri, options);
}

@ -9,10 +9,20 @@ import type { SubscribableDataset } from "@ldo/subscribable-dataset";
import { namedNode } from "@rdfjs/data-model";
import type { Quad } from "@rdfjs/types";
/**
* @internal
* Options to be passed to the tracking proxy
*/
export interface TrackingProxyContextOptions extends ProxyContextOptions {
dataset: SubscribableDataset<Quad>;
}
/**
* @internal
* This proxy exists to ensure react components rerender at the right time. It
* keeps track of every key accessed in a Linked Data Object and only when the
* dataset is updated with that key does it rerender the react component.
*/
export class TrackingProxyContext extends ProxyContext {
private listener: () => void;
private subscribableDataset: SubscribableDataset<Quad>;

@ -1,7 +0,0 @@
import type { Resource } from "@ldo/solid";
export function createWrapperProxy<ResourceType extends Resource>(
target: ResourceType,
): ResourceType {
return new Proxy(target, {});
}

@ -1,6 +0,0 @@
import { useState, useCallback } from "react";
export function useForceReload() {
const [, setValue] = useState(0);
return useCallback(() => setValue((value) => value + 1), []);
}

@ -0,0 +1,32 @@
import { ContextDefinition } from "jsonld";
/**
* =============================================================================
* postContext: JSONLD Context for post
* =============================================================================
*/
export const postContext: ContextDefinition = {
type: {
"@id": "@type",
},
SocialMediaPosting: "http://schema.org/SocialMediaPosting",
CreativeWork: "http://schema.org/CreativeWork",
Thing: "http://schema.org/Thing",
articleBody: {
"@id": "http://schema.org/articleBody",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
uploadDate: {
"@id": "http://schema.org/uploadDate",
"@type": "http://www.w3.org/2001/XMLSchema#date",
},
image: {
"@id": "http://schema.org/image",
"@type": "@id",
},
publisher: {
"@id": "http://schema.org/publisher",
"@type": "@id",
"@container": "@set",
},
};

@ -0,0 +1,155 @@
import { Schema } from "shexj";
/**
* =============================================================================
* postSchema: ShexJ Schema for post
* =============================================================================
*/
export const postSchema: Schema = {
type: "Schema",
shapes: [
{
id: "https://example.com/PostSh",
type: "ShapeDecl",
shapeExpr: {
type: "Shape",
expression: {
type: "EachOf",
expressions: [
{
type: "TripleConstraint",
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
valueExpr: {
type: "NodeConstraint",
values: [
"http://schema.org/SocialMediaPosting",
"http://schema.org/CreativeWork",
"http://schema.org/Thing",
],
},
},
{
type: "TripleConstraint",
predicate: "http://schema.org/articleBody",
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#label",
object: {
value: "articleBody",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The actual body of the article. ",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://schema.org/uploadDate",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#date",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#label",
object: {
value: "uploadDate",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"Date when this media object was uploaded to this site.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://schema.org/image",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#label",
object: {
value: "image",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"A media object that encodes this CreativeWork. This property is a synonym for encoding.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://schema.org/publisher",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#label",
object: {
value: "publisher",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The publisher of the creative work.",
},
},
],
},
],
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#label",
object: {
value: "SocialMediaPost",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"A post to a social media platform, including blog posts, tweets, Facebook posts, etc.",
},
},
],
},
},
],
};

@ -0,0 +1,19 @@
import { ShapeType } from "@ldo/ldo";
import { postSchema } from "./post.schema";
import { postContext } from "./post.context";
import { PostSh } from "./post.typings";
/**
* =============================================================================
* LDO ShapeTypes post
* =============================================================================
*/
/**
* PostSh ShapeType
*/
export const PostShShapeType: ShapeType<PostSh> = {
schema: postSchema,
shape: "https://example.com/PostSh",
context: postContext,
};

@ -0,0 +1,45 @@
import { ContextDefinition } from "jsonld";
/**
* =============================================================================
* Typescript Typings for post
* =============================================================================
*/
/**
* PostSh Type
*/
export interface PostSh {
"@id"?: string;
"@context"?: ContextDefinition;
type:
| {
"@id": "SocialMediaPosting";
}
| {
"@id": "CreativeWork";
}
| {
"@id": "Thing";
};
/**
* The actual body of the article.
*/
articleBody?: string;
/**
* Date when this media object was uploaded to this site.
*/
uploadDate: string;
/**
* A media object that encodes this CreativeWork. This property is a synonym for encoding.
*/
image?: {
"@id": string;
};
/**
* The publisher of the creative work.
*/
publisher: {
"@id": string;
}[];
}

@ -0,0 +1,358 @@
import React, { useEffect, useState } from "react";
import type { FunctionComponent } from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import {
SAMPLE_BINARY_URI,
SAMPLE_DATA_URI,
SERVER_DOMAIN,
setUpServer,
} from "./setUpServer";
import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider";
import { useResource } from "../src/useResource";
import { useRootContainerFor } from "../src/useRootContainer";
import { useLdo } from "../src/SolidLdoProvider";
import { PostShShapeType } from "./.ldo/post.shapeTypes";
import type { PostSh } from "./.ldo/post.typings";
import { useSubject } from "../src/useSubject";
// Use an increased timeout, since the CSS server takes too much setup time.
jest.setTimeout(40_000);
describe("Integration Tests", () => {
setUpServer();
/**
* ===========================================================================
* useResource
* ===========================================================================
*/
describe("useResource", () => {
it("Fetches a resource and indicates it is loading while doing so", async () => {
const UseResourceTest: FunctionComponent = () => {
const resource = useResource(SAMPLE_DATA_URI);
if (resource?.isLoading()) return <p>Loading</p>;
return <p role="status">{resource.status.type}</p>;
};
render(
<UnauthenticatedSolidLdoProvider>
<UseResourceTest />
</UnauthenticatedSolidLdoProvider>,
);
await screen.findByText("Loading");
const resourceStatus = await screen.findByRole("status");
expect(resourceStatus.innerHTML).toBe("dataReadSuccess");
});
it("returns undefined when no uri is provided, then rerenders when one is", async () => {
const UseResourceUndefinedTest: FunctionComponent = () => {
const [uri, setUri] = useState<string | undefined>(undefined);
const resource = useResource(uri, { suppressInitialRead: true });
if (!resource)
return (
<div>
<p>Undefined</p>
<button onClick={() => setUri(SAMPLE_DATA_URI)}>Next</button>
</div>
);
return <p role="status">{resource.status.type}</p>;
};
render(
<UnauthenticatedSolidLdoProvider>
<UseResourceUndefinedTest />
</UnauthenticatedSolidLdoProvider>,
);
await screen.findByText("Undefined");
fireEvent.click(screen.getByText("Next"));
const resourceStatus = await screen.findByRole("status");
expect(resourceStatus.innerHTML).toBe("unfetched");
});
it("Reloads the data on mount", async () => {
const ReloadTest: FunctionComponent = () => {
const resource = useResource(SAMPLE_DATA_URI, { reloadOnMount: true });
if (resource?.isLoading()) return <p>Loading</p>;
return <p role="status">{resource.status.type}</p>;
};
const ReloadParent: FunctionComponent = () => {
const [showComponent, setShowComponent] = useState(true);
return (
<div>
<button onClick={() => setShowComponent(!showComponent)}>
Show Component
</button>
{showComponent ? <ReloadTest /> : <p>Hidden</p>}
</div>
);
};
render(
<UnauthenticatedSolidLdoProvider>
<ReloadParent />
</UnauthenticatedSolidLdoProvider>,
);
await screen.findByText("Loading");
const resourceStatus1 = await screen.findByRole("status");
expect(resourceStatus1.innerHTML).toBe("dataReadSuccess");
fireEvent.click(screen.getByText("Show Component"));
await screen.findByText("Hidden");
fireEvent.click(screen.getByText("Show Component"));
await screen.findByText("Loading");
const resourceStatus2 = await screen.findByRole("status");
expect(resourceStatus2.innerHTML).toBe("dataReadSuccess");
});
it("handles swapping to a new resource", async () => {
const SwapResourceTest: FunctionComponent = () => {
const [uri, setUri] = useState(SAMPLE_DATA_URI);
const resource = useResource(uri);
if (resource?.isLoading()) return <p>Loading</p>;
return (
<div>
<p role="status">{resource.status.type}</p>
<button onClick={() => setUri(SAMPLE_BINARY_URI)}>
Update URI
</button>
</div>
);
};
render(
<UnauthenticatedSolidLdoProvider>
<SwapResourceTest />
</UnauthenticatedSolidLdoProvider>,
);
await screen.findByText("Loading");
const resourceStatus1 = await screen.findByRole("status");
expect(resourceStatus1.innerHTML).toBe("dataReadSuccess");
fireEvent.click(screen.getByText("Update URI"));
await screen.findByText("Loading");
const resourceStatus2 = await screen.findByRole("status");
expect(resourceStatus2.innerHTML).toBe("binaryReadSuccess");
});
});
describe("useRootContainer", () => {
it("gets the root container for a sub-resource", async () => {
const RootContainerTest: FunctionComponent = () => {
const rootContainer = useRootContainerFor(SAMPLE_DATA_URI, {
suppressInitialRead: true,
});
return rootContainer ? (
<p role="root">{rootContainer?.uri}</p>
) : undefined;
};
render(
<UnauthenticatedSolidLdoProvider>
<RootContainerTest />
</UnauthenticatedSolidLdoProvider>,
);
const container = await screen.findByRole("root");
expect(container.innerHTML).toBe(SERVER_DOMAIN);
});
it("returns undefined when a URI is not provided", async () => {
const RootContainerTest: FunctionComponent = () => {
const rootContainer = useRootContainerFor(undefined, {
suppressInitialRead: true,
});
return rootContainer ? (
<p role="root">{rootContainer?.uri}</p>
) : (
<p role="undefined">Undefined</p>
);
};
render(
<UnauthenticatedSolidLdoProvider>
<RootContainerTest />
</UnauthenticatedSolidLdoProvider>,
);
const container = await screen.findByRole("undefined");
expect(container.innerHTML).toBe("Undefined");
});
});
describe("useLdoMethod", () => {
it("uses get subject to get a linked data object", async () => {
const GetSubjectTest: FunctionComponent = () => {
const [subject, setSubject] = useState<PostSh | undefined>();
const { getSubject } = useLdo();
useEffect(() => {
const someSubject = getSubject(
PostShShapeType,
"https://example.com/subject",
);
setSubject(someSubject);
}, []);
return subject ? <p role="subject">{subject["@id"]}</p> : undefined;
};
render(
<UnauthenticatedSolidLdoProvider>
<GetSubjectTest />
</UnauthenticatedSolidLdoProvider>,
);
const container = await screen.findByRole("subject");
expect(container.innerHTML).toBe("https://example.com/subject");
});
it("uses createData to create a new data object", async () => {
const GetSubjectTest: FunctionComponent = () => {
const [subject, setSubject] = useState<PostSh | undefined>();
const { createData, getResource } = useLdo();
useEffect(() => {
const someSubject = createData(
PostShShapeType,
"https://example.com/subject",
getResource("https://example.com/"),
);
someSubject.articleBody = "Cool Article";
setSubject(someSubject);
}, []);
return subject ? (
<p role="subject">{subject.articleBody}</p>
) : undefined;
};
render(
<UnauthenticatedSolidLdoProvider>
<GetSubjectTest />
</UnauthenticatedSolidLdoProvider>,
);
const container = await screen.findByRole("subject");
expect(container.innerHTML).toBe("Cool Article");
});
});
describe("useSubject", () => {
it("renders the article body from the useSubject value", async () => {
const UseSubjectTest: FunctionComponent = () => {
useResource(SAMPLE_DATA_URI);
const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);
return <p role="article">{post.articleBody}</p>;
};
render(
<UnauthenticatedSolidLdoProvider>
<UseSubjectTest />
</UnauthenticatedSolidLdoProvider>,
);
await screen.findByText("test");
});
it("renders the array value from the useSubject value", async () => {
const UseSubjectTest: FunctionComponent = () => {
const resource = useResource(SAMPLE_DATA_URI);
const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);
if (resource.isLoading() || !post) return <p>loading</p>;
return (
<div>
<p role="single">{post.publisher[0]["@id"]}</p>
<ul role="list">
{post.publisher.map((publisher) => {
return <li key={publisher["@id"]}>{publisher["@id"]}</li>;
})}
</ul>
</div>
);
};
render(
<UnauthenticatedSolidLdoProvider>
<UseSubjectTest />
</UnauthenticatedSolidLdoProvider>,
);
const single = await screen.findByRole("single");
expect(single.innerHTML).toBe("https://example.com/Publisher1");
const list = await screen.findByRole("list");
expect(list.children[0].innerHTML).toBe("https://example.com/Publisher1");
expect(list.children[1].innerHTML).toBe("https://example.com/Publisher2");
});
it("returns undefined in the subject URI is undefined", async () => {
const UseSubjectTest: FunctionComponent = () => {
useResource(SAMPLE_DATA_URI, { suppressInitialRead: true });
const post = useSubject(PostShShapeType, undefined);
return (
<p role="article">
{post === undefined ? "Undefined" : "Not Undefined"}
</p>
);
};
render(
<UnauthenticatedSolidLdoProvider>
<UseSubjectTest />
</UnauthenticatedSolidLdoProvider>,
);
const article = await screen.findByRole("article");
expect(article.innerHTML).toBe("Undefined");
});
it("returns nothing if a symbol key is provided", async () => {
const UseSubjectTest: FunctionComponent = () => {
const resource = useResource(SAMPLE_DATA_URI);
const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);
if (resource.isLoading() || !post) return <p>loading</p>;
return <p role="value">{typeof post[Symbol.hasInstance]}</p>;
};
render(
<UnauthenticatedSolidLdoProvider>
<UseSubjectTest />
</UnauthenticatedSolidLdoProvider>,
);
const article = await screen.findByRole("value");
expect(article.innerHTML).toBe("undefined");
});
it("returns an id if an id key is provided", async () => {
const UseSubjectTest: FunctionComponent = () => {
const resource = useResource(SAMPLE_DATA_URI);
const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);
if (resource.isLoading() || !post) return <p>loading</p>;
return <p role="value">{post["@id"]}</p>;
};
render(
<UnauthenticatedSolidLdoProvider>
<UseSubjectTest />
</UnauthenticatedSolidLdoProvider>,
);
const article = await screen.findByRole("value");
expect(article.innerHTML).toBe(`${SAMPLE_DATA_URI}#Post1`);
});
it("does not set a value if a value is attempted to be set", async () => {
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
const UseSubjectTest: FunctionComponent = () => {
const resource = useResource(SAMPLE_DATA_URI);
const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);
if (resource.isLoading() || !post) return <p>loading</p>;
return (
<div>
<p role="value">{post.articleBody}</p>
<button onClick={() => (post.articleBody = "bad")}>
Attempt Change
</button>
</div>
);
};
render(
<UnauthenticatedSolidLdoProvider>
<UseSubjectTest />
</UnauthenticatedSolidLdoProvider>,
);
const article = await screen.findByRole("value");
expect(article.innerHTML).toBe(`test`);
fireEvent.click(screen.getByText("Attempt Change"));
expect(article.innerHTML).not.toBe("bad");
expect(warn).toHaveBeenCalledWith(
"You've attempted to set a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.",
);
warn.mockReset();
});
});
});

@ -0,0 +1,110 @@
import type { ContainerUri, LeafUri } from "@ldo/solid";
import fetch from "cross-fetch";
export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/";
export const ROOT_ROUTE = process.env.ROOT_CONTAINER || "example/";
export const ROOT_CONTAINER = `${SERVER_DOMAIN}${ROOT_ROUTE}`;
export const TEST_CONTAINER_SLUG = "test_ldo/";
export const TEST_CONTAINER_URI =
`${ROOT_CONTAINER}${TEST_CONTAINER_SLUG}` as ContainerUri;
export const SAMPLE_DATA_URI = `${TEST_CONTAINER_URI}sample.ttl` as LeafUri;
export const SAMPLE2_DATA_SLUG = "sample2.ttl";
export const SAMPLE2_DATA_URI =
`${TEST_CONTAINER_URI}${SAMPLE2_DATA_SLUG}` as LeafUri;
export const SAMPLE_BINARY_URI = `${TEST_CONTAINER_URI}sample.txt` as LeafUri;
export const SAMPLE2_BINARY_SLUG = `sample2.txt`;
export const SAMPLE2_BINARY_URI =
`${TEST_CONTAINER_URI}${SAMPLE2_BINARY_SLUG}` as LeafUri;
export const SAMPLE_CONTAINER_URI =
`${TEST_CONTAINER_URI}sample_container/` as ContainerUri;
export const EXAMPLE_POST_TTL = `@prefix schema: <http://schema.org/> .
<#Post1>
a schema:CreativeWork, schema:Thing, schema:SocialMediaPosting ;
schema:image <https://example.com/postImage.jpg> ;
schema:articleBody "test" ;
schema:publisher <https://example.com/Publisher1>, <https://example.com/Publisher2> .`;
export const TEST_CONTAINER_TTL = `@prefix dc: <http://purl.org/dc/terms/>.
@prefix ldp: <http://www.w3.org/ns/ldp#>.
@prefix posix: <http://www.w3.org/ns/posix/stat#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
<> <urn:npm:solid:community-server:http:slug> "sample.txt";
a ldp:Container, ldp:BasicContainer, ldp:Resource;
dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime.
<sample.ttl> a ldp:Resource, <http://www.w3.org/ns/iana/media-types/text/turtle#Resource>;
dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime.
<sample.txt> a ldp:Resource, <http://www.w3.org/ns/iana/media-types/text/plain#Resource>;
dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime.
<> posix:mtime 1697810234;
ldp:contains <sample.ttl>, <sample.txt>.
<sample.ttl> posix:mtime 1697810234;
posix:size 522.
<sample.txt> posix:mtime 1697810234;
posix:size 10.`;
export interface SetUpServerReturn {
authFetch: typeof fetch;
fetchMock: jest.Mock<
Promise<Response>,
[input: RequestInfo | URL, init?: RequestInit | undefined]
>;
}
export function setUpServer(): SetUpServerReturn {
// Ignore to build s
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const s: SetUpServerReturn = {};
beforeAll(async () => {
// s.authFetch = await getAuthenticatedFetch();
s.authFetch = fetch;
});
beforeEach(async () => {
s.fetchMock = jest.fn(s.authFetch);
// Create a new document called sample.ttl
await s.authFetch(ROOT_CONTAINER, {
method: "POST",
headers: {
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"',
slug: TEST_CONTAINER_SLUG,
},
});
await Promise.all([
s.authFetch(TEST_CONTAINER_URI, {
method: "POST",
headers: { "content-type": "text/turtle", slug: "sample.ttl" },
body: EXAMPLE_POST_TTL,
}),
s.authFetch(TEST_CONTAINER_URI, {
method: "POST",
headers: { "content-type": "text/plain", slug: "sample.txt" },
body: "some text.",
}),
]);
});
afterEach(async () => {
await Promise.all([
s.authFetch(SAMPLE_DATA_URI, {
method: "DELETE",
}),
s.authFetch(SAMPLE2_DATA_URI, {
method: "DELETE",
}),
s.authFetch(SAMPLE_BINARY_URI, {
method: "DELETE",
}),
s.authFetch(SAMPLE2_BINARY_URI, {
method: "DELETE",
}),
s.authFetch(SAMPLE_CONTAINER_URI, {
method: "DELETE",
}),
]);
});
return s;
}

@ -0,0 +1,52 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/initialize-prefilled-root.json",
"css:config/app/setup/optional.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",
"css:config/http/middleware/default.json",
"css:config/http/notifications/all.json",
"css:config/http/server-factory/http.json",
"css:config/http/static/default.json",
"css:config/identity/access/public.json",
"css:config/identity/email/default.json",
"css:config/identity/handler/default.json",
"css:config/identity/ownership/token.json",
"css:config/identity/pod/static.json",
"css:config/identity/registration/enabled.json",
"css:config/ldp/authentication/dpop-bearer.json",
"css:config/ldp/authorization/webacl.json",
"css:config/ldp/handler/default.json",
"css:config/ldp/metadata-parser/default.json",
"css:config/ldp/metadata-writer/default.json",
"css:config/ldp/modes/default.json",
"css:config/storage/backend/memory.json",
"css:config/storage/key-value/resource-store.json",
"css:config/storage/middleware/default.json",
"css:config/util/auxiliary/acl.json",
"css:config/util/identifiers/suffix.json",
"css:config/util/index/default.json",
"css:config/util/logging/winston.json",
"css:config/util/representation-conversion/default.json",
"css:config/util/resource-locker/memory.json",
"css:config/util/variables/default.json"
],
"@graph": [
{
"comment": "A Solid server that stores its resources in memory and uses WAC for authorization."
},
{
"comment": "The location of the new pod templates folder.",
"@type": "Override",
"overrideInstance": {
"@id": "urn:solid-server:default:PodResourcesGenerator"
},
"overrideParameters": {
"@type": "StaticFolderGenerator",
"templateFolder": "./test/test-server/configs/template"
}
}
]
}

@ -0,0 +1,8 @@
[
{
"podName": "example",
"email": "hello@example.com",
"password": "abc123",
"template": "./template"
}
]

@ -0,0 +1,13 @@
@prefix : <#>.
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
@prefix eve: <./>.
@prefix c: <./profile/card#>.
:ControlReadWrite
a acl:Authorization;
acl:accessTo eve:;
acl:agent c:me, <mailto:info@o.team>;
acl:agentClass foaf:Agent;
acl:default eve:;
acl:mode acl:Control, acl:Read, acl:Write.

@ -0,0 +1,19 @@
# ACL resource for the WebID profile document
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
# The WebID profile is readable by the public.
# This is required for discovery and verification,
# e.g. when checking identity providers.
<#public>
a acl:Authorization;
acl:agentClass foaf:Agent;
acl:accessTo <./card>;
acl:mode acl:Read.
# The owner has full access to the profile
<#owner>
a acl:Authorization;
acl:agent <{{webId}}>;
acl:accessTo <./card>;
acl:mode acl:Read, acl:Write, acl:Control.

@ -0,0 +1,7 @@
import { createApp } from "./solidServer.helper";
async function run() {
const app = await createApp();
await app.start();
}
run();

@ -0,0 +1,42 @@
// Taken from https://github.com/comunica/comunica/blob/b237be4265c353a62a876187d9e21e3bc05123a3/engines/query-sparql/test/QuerySparql-solid-test.ts#L9
import * as path from "path";
import type { App } from "@solid/community-server";
import { AppRunner, resolveModulePath } from "@solid/community-server";
export async function createApp(): Promise<App> {
if (process.env.SERVER) {
return {
start: () => {},
stop: () => {},
} as App;
}
const appRunner = new AppRunner();
return appRunner.create(
{
mainModulePath: resolveModulePath(""),
typeChecking: false,
},
path.join(
__dirname,
"configs",
"components-config",
"unauthenticatedServer.json",
),
{},
{
port: 3_001,
loggingLevel: "off",
seededPodConfigJson: path.join(
__dirname,
"configs",
"solid-css-seed.json",
),
},
);
}
export interface ISecretData {
id: string;
secret: string;
}

@ -1,5 +0,0 @@
describe("Trivial", () => {
it("Trivial", () => {
expect(true).toBe(true);
});
});

@ -1,13 +1,228 @@
# @ldo/solid
Alpha
@ldo/solid is a client that implements the Solid specification with the use of Linked Data Objects.
## Notes:
- Any quads in the default graph will not be synced to Solid Pods, but will be added to the dataset without any syncing
## Installation
## TODO:
- Add Documentation
- Access rule stuff just doesn't work. I might need to program a custom implementation for that
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
```
### 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
```
## 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/api/solid/functions/createSolidLdoDataset/)
- [SolidLdoDataset](https://ldo.js.org/api/solid/classes/SolidLdoDataset/)
Resources (Manage batching requests)
- [LeafUri](https://ldo.js.org/api/solid/types/LeafUri/)
- [ContainerUri](https://ldo.js.org/api/solid/types/ContainerUri/)
- [Leaf](https://ldo.js.org/api/solid/classes/Leaf/)
- [Container](https://ldo.js.org/api/solid/classes/Container/)
Standalone Functions
- [checkRootContainter](https://ldo.js.org/api/solid/functions/checkRootContainer/)
- [createDataResource](https://ldo.js.org/api/solid/functions/createDataResource/)
- [deleteResource](https://ldo.js.org/api/solid/functions/deleteResource/)
- [readResource](https://ldo.js.org/api/solid/functions/readResource/)
- [updateResource](https://ldo.js.org/api/solid/functions/updateResource/)
- [uploadResource](https://ldo.js.org/api/solid/functions/uploadResource/)
Data Functions
- [changeData](https://ldo.js.org/api/solid/functions/changeData/)
- [commitData](https://ldo.js.org/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/).

@ -1,195 +0,0 @@
const resourceMethods: Record<string, string[]> = {
RootContainerResource: [
"uri",
"isLoading",
"didInitialFetch",
"ldoDataset",
"getIsRootContainer",
"createContainerIn",
"createDataResourceIn",
"uploadBinaryIn",
"createOrOverwrite",
"read",
"reload",
"load",
"clear",
"clearIfPresent",
],
ContainerResource: [
"uri",
"isLoading",
"didInitialFetch",
"parentContainer",
"getIsRootContainer",
"getParentContainer",
"childResources",
"getRootContainer",
"createContainerIn",
"createDataResourceIn",
"uploadBinaryIn",
"ldoDataset",
"createOrOverwrite",
"read",
"reload",
"load",
"delete",
"deleteIfPresent",
"clear",
"clearIfPresent",
],
ChildDataResource: [
"uri",
"isLoading",
"didInitialFetch",
"parentContainer",
"getParentContainer",
"hasData",
"ldoDataset",
"getRootContainer",
"createOrOverwrite",
"read",
"reload",
"load",
"delete",
"deleteIfPresent",
],
BinaryResource: [
"uri",
"isLoading",
"didInitialFetch",
"mimeType",
"fileExtension",
"getRootContainer",
"getParentContainer",
"uploadOrOverwrite",
"read",
"reload",
"load",
"delete",
"deleteIfPresent",
],
AbsentContainerResource: [
"uri",
"isLoading",
"didInitialFetch",
"parentContainer",
"getIsRootContainer",
"getParentContainer",
"getRootContainer",
"createContainerIn",
"createDataResourceIn",
"uploadBinaryIn",
"createOrOverwrite",
"create",
"createIfAbsent",
"read",
"reload",
"load",
"deleteIfPresent",
"clearIfPresent",
],
AbsentChildDataResource: [
"uri",
"isLoading",
"didInitialFetch",
"parentContainer",
"getParentContainer",
"getRootContainer",
"createOrOverwrite",
"create",
"createIfAbsent",
"read",
"reload",
"load",
"deleteIfPresent",
],
AbsentBinaryResource: [
"uri",
"isLoading",
"didInitialFetch",
"parentContainer",
"getParentContainer",
"getRootContainer",
"uploadOrOverwrite",
"upload",
"uploadIfAbsent",
"read",
"reload",
"load",
"deleteIfPresent",
],
UnfetchedContainerResource: [
"uri",
"isLoading",
"didInitialFetch",
"getIsRootContainer",
"getParentContainer",
"getRootContainer",
"createContainerIn",
"createDataResourceIn",
"uploadBinaryIn",
"createOrOverwrite",
"createIfAbsent",
"read",
"reload",
"load",
"clearIfPresent",
],
UnfetchedChildDataResource: [
"parentContainer",
"getParentContainer",
"uri",
"isLoading",
"didInitialFetch",
"getRootContainer",
"createOrOverwrite",
"createIfAbsent",
"read",
"reload",
"load",
"deleteIfPresent",
],
UnfetchedBinaryResource: [
"uri",
"isLoading",
"didInitialFetch",
"parentContainer",
"getParentContainer",
"getRootContainer",
"uploadOrOverwrite",
"createOrOverwrite",
"uploadIfAbsent",
"read",
"reload",
"load",
"deleteIfPresent",
],
};
function processTypes() {
const usedKeys = new Set();
const interfaces = Object.keys(resourceMethods);
const groupMap: Record<string, string[]> = {};
interfaces.forEach((interfaceName) => {
resourceMethods[interfaceName].forEach((methodKey) => {
if (!usedKeys.has(methodKey)) {
usedKeys.add(methodKey);
let groupName = "";
interfaces.forEach((interfaceName) => {
if (resourceMethods[interfaceName].includes(methodKey)) {
groupName += `${interfaceName}|`;
}
});
if (!groupMap[groupName]) {
groupMap[groupName] = [];
}
groupMap[groupName].push(methodKey);
}
});
});
console.log(groupMap);
}
processTypes();

@ -1,22 +0,0 @@
import { Mixin } from "ts-mixer";
class Foo {
protected makeFoo() {
return "foo";
}
}
class Bar {
protected makeFoo() {
return "bar";
}
}
class FooBar extends Mixin(Foo, Bar) {
public makeFooBar() {
return this.makeFoo() + this.makeFoo();
}
}
const fooBar = new FooBar();
console.log(fooBar.makeFooBar());

@ -1,6 +1,6 @@
{
"name": "@ldo/solid",
"version": "0.0.1-alpha.17",
"version": "0.0.1-alpha.18",
"description": "A library for LDO and Solid",
"main": "dist/index.js",
"scripts": {
@ -11,7 +11,8 @@
"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"
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern",
"docs": "typedoc --plugin typedoc-plugin-markdown"
},
"repository": {
"type": "git",
@ -25,7 +26,7 @@
"homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid#readme",
"devDependencies": {
"@inrupt/solid-client-authn-core": "^1.17.1",
"@ldo/cli": "^0.0.1-alpha.17",
"@ldo/cli": "^0.0.1-alpha.18",
"@rdfjs/data-model": "^1.2.0",
"@rdfjs/types": "^1.0.1",
"@solid/community-server": "^6.0.2",
@ -34,12 +35,14 @@
"jest-rdf": "^1.8.0",
"ts-jest": "^29.0.2",
"ts-node": "^10.9.1",
"typed-emitter": "^2.1.0"
"typed-emitter": "^2.1.0",
"typedoc": "^0.25.4",
"typedoc-plugin-markdown": "^3.17.1"
},
"dependencies": {
"@inrupt/solid-client": "^1.30.0",
"@ldo/dataset": "^0.0.1-alpha.17",
"@ldo/ldo": "^0.0.1-alpha.17",
"@ldo/ldo": "^0.0.1-alpha.18",
"@ldo/rdf-utils": "^0.0.1-alpha.17",
"@types/parse-link-header": "^2.0.1",
"cross-fetch": "^3.1.6",

@ -4,19 +4,52 @@ 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;

@ -1,5 +1,6 @@
import { LdoDataset } from "@ldo/ldo";
import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils";
import type { LdoBase, ShapeType } from "@ldo/ldo";
import { LdoDataset, startTransaction } from "@ldo/ldo";
import type { DatasetChanges, GraphNode, SubjectNode } from "@ldo/rdf-utils";
import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types";
import type {
UpdateResult,
@ -8,7 +9,10 @@ import type {
import { AggregateError } from "./requester/results/error/ErrorResult";
import { InvalidUriError } from "./requester/results/error/InvalidUriError";
import type { AggregateSuccess } from "./requester/results/success/SuccessResult";
import type { UpdateSuccess } from "./requester/results/success/UpdateSuccess";
import type {
UpdateDefaultGraphSuccess,
UpdateSuccess,
} from "./requester/results/success/UpdateSuccess";
import type { Container } from "./resource/Container";
import type { Leaf } from "./resource/Leaf";
import type { ResourceResult } from "./resource/resourceResult/ResourceResult";
@ -17,10 +21,44 @@ import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext";
import { splitChangesByGraph } from "./util/splitChangesByGraph";
import type { ContainerUri, LeafUri } from "./util/uriTypes";
import { isContainerUri } from "./util/uriTypes";
import type { Resource } from "./resource/Resource";
/**
* 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 {
/**
* @internal
*/
public context: SolidLdoDatasetContext;
/**
* @param context - SolidLdoDatasetContext
* @param datasetFactory - An optional dataset factory
* @param initialDataset - A set of triples to initialize this dataset
*/
constructor(
context: SolidLdoDatasetContext,
datasetFactory: DatasetFactory,
@ -30,6 +68,23 @@ export class SolidLdoDataset extends LdoDataset {
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;
@ -37,17 +92,46 @@ export class SolidLdoDataset extends LdoDataset {
return this.context.resourceStore.get(uri, options);
}
/**
* Given dataset changes, commit all changes made to the proper place
* on Solid Pods.
*
* @param changes - A set of changes that should be applied to Solid Pods
*
* @returns an AggregateSuccess if successful and an AggregateError if not
*
* @example
* ```typescript
* const result = await solidLdoDataset.commitChangesToPod({
* added: createDataset([
* quad(namedNode("a"), namedNode("b"), namedNode("d"));
* ]),
* removed: createDataset([
* quad(namedNode("a"), namedNode("b"), namedNode("c"));
* ])
* });
* if (result.isError()) {
* // handle error
* }
* ```
*/
async commitChangesToPod(
changes: DatasetChanges<Quad>,
): Promise<
| AggregateSuccess<ResourceResult<UpdateSuccess, Leaf>>
| AggregateSuccess<
ResourceResult<UpdateSuccess | UpdateDefaultGraphSuccess, Leaf>
>
| AggregateError<UpdateResultError | InvalidUriError>
> {
// Optimistically add changes to the datastore
// this.bulk(changes);
const changesByGraph = splitChangesByGraph(changes);
// Iterate through all changes by graph in
const results: [
GraphNode,
DatasetChanges<Quad>,
UpdateResult | InvalidUriError | { type: "defaultGraph"; isError: false },
UpdateResult | InvalidUriError | UpdateDefaultGraphSuccess,
][] = await Promise.all(
Array.from(changesByGraph.entries()).map(
async ([graph, datasetChanges]) => {
@ -57,7 +141,10 @@ export class SolidLdoDataset extends LdoDataset {
return [
graph,
datasetChanges,
{ type: "defaultGraph", isError: false },
{
type: "updateDefaultGraphSuccess",
isError: false,
} as UpdateDefaultGraphSuccess,
];
}
if (isContainerUri(graph.value)) {
@ -78,6 +165,7 @@ export class SolidLdoDataset extends LdoDataset {
// If one has errored, return error
const errors = results.filter((result) => result[2].isError);
if (errors.length > 0) {
return new AggregateError(
errors.map(
@ -92,8 +180,32 @@ export class SolidLdoDataset extends LdoDataset {
.map((result) => result[2])
.filter(
(result): result is ResourceResult<UpdateSuccess, Leaf> =>
result.type === "updateSuccess",
result.type === "updateSuccess" ||
result.type === "updateDefaultGraphSuccess",
),
};
}
/**
* 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;
}
}

@ -1,16 +1,20 @@
// import type TypedEmitter from "typed-emitter";
import type { ResourceStore } from "./ResourceStore";
import type { SolidLdoDataset } from "./SolidLdoDataset";
// import type { DocumentError } from "./document/errors/DocumentError";
// export type OnDocumentErrorCallback = (error: DocumentError) => void;
// export type DocumentEventEmitter = TypedEmitter<{
// documentError: OnDocumentErrorCallback;
// }>;
/**
* 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;
}

@ -2,20 +2,48 @@ import type { Dataset, DatasetFactory } from "@rdfjs/types";
import { SolidLdoDataset } from "./SolidLdoDataset";
import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext";
import crossFetch from "cross-fetch";
import { createDataset, createDatasetFactory } from "@ldo/dataset";
import { ResourceStore } from "./ResourceStore";
import { guaranteeFetch } from "./util/guaranteeFetch";
/**
* 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 = options?.fetch || crossFetch;
const finalFetch = guaranteeFetch(options?.fetch);
const finalDatasetFactory = options?.datasetFactory || createDatasetFactory();
const finalDataset = options?.dataset || createDataset();

@ -7,3 +7,15 @@ 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/getAccessRules";
export * from "./requester/requests/readResource";
export * from "./requester/requests/requestOptions";
export * from "./requester/requests/setAccessRules";
export * from "./requester/requests/updateDataResource";
export * from "./requester/requests/uploadResource";

@ -0,0 +1,90 @@
import {
startTransaction,
type LdoBase,
write,
transactionChanges,
getDataset,
} from "@ldo/ldo";
import type { DatasetChanges } from "@ldo/rdf-utils";
import type { Resource } from "./resource/Resource";
import type { SolidLdoDataset } from "./SolidLdoDataset";
import type { Quad } from "@rdfjs/types";
import { _proxyContext, getProxyFromObject } from "@ldo/jsonld-dataset-proxy";
import type { SubscribableDataset } from "@ldo/subscribable-dataset";
/**
* 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/proifle#me");
* const resource = solidLdoDataset.getResource("https://example.com/profile");
*
* const cProfile = changeData(profile, resource);
* cProfile.name = "My New Name";
* 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/proifle#me");
* const resource = solidLdoDataset.getResource("https://example.com/profile");
*
* const cProfile = changeData(profile, resource);
* cProfile.name = "My New Name";
* await commitData(cProfile);
* ```
*/
export function commitData(
input: LdoBase,
): ReturnType<SolidLdoDataset["commitChangesToPod"]> {
const changes = transactionChanges(input);
// 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>,
});
const dataset = getDataset(input) as SolidLdoDataset;
return dataset.commitChangesToPod(changes as DatasetChanges<Quad>);
}

@ -14,37 +14,78 @@ import type {
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";
export abstract class Requester {
/**
* @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();
// All intance variables
/**
* 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();
@ -52,23 +93,19 @@ export abstract class Requester {
name: READ_KEY,
args: [this.uri, { dataset: transaction, fetch: this.context.fetch }],
perform: readResource,
modifyQueue: (queue, currentlyLoading) => {
if (queue.length === 0 && currentlyLoading?.name === READ_KEY) {
return currentlyLoading;
} else if (queue[queue.length - 1]?.name === READ_KEY) {
return queue[queue.length - 1];
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys(READ_KEY),
after: (result) => {
if (!result.isError) {
transaction.commit();
}
return undefined;
},
});
if (!result.isError) {
transaction.commit();
}
return result;
}
/**
* Delete this resource
* @returns A DeleteResult
*/
async delete(): Promise<DeleteResult> {
const transaction = this.context.solidLdoDataset.startTransaction();
@ -76,25 +113,22 @@ export abstract class Requester {
name: DELETE_KEY,
args: [this.uri, { dataset: transaction, fetch: this.context.fetch }],
perform: deleteResource,
modifyQueue: (queue, currentlyLoading) => {
if (queue.length === 0 && currentlyLoading?.name === DELETE_KEY) {
return currentlyLoading;
} else if (queue[queue.length - 1]?.name === DELETE_KEY) {
return queue[queue.length - 1];
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys(DELETE_KEY),
after: (result) => {
if (!result.isError) {
transaction.commit();
}
return undefined;
},
});
if (!result.isError) {
transaction.commit();
}
return result;
}
/**
* Creates a Resource
* @param overwrite: If true, this will orverwrite the resource if it already
* @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,
@ -145,10 +179,12 @@ export abstract class Requester {
}
return undefined;
},
after: (result) => {
if (!result.isError) {
transaction.commit();
}
},
});
if (!result.isError) {
transaction.commit();
}
return result;
}
}

@ -1,6 +1,6 @@
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import type { ContainerUri } from "../util/uriTypes";
import { Requester } from "./Requester";
import { BatchedRequester } from "./BatchedRequester";
import type { CheckRootResult } from "./requests/checkRootContainer";
import { checkRootContainer } from "./requests/checkRootContainer";
import type {
@ -8,21 +8,43 @@ import type {
ContainerCreateIfAbsentResult,
} from "./requests/createDataResource";
import type { ReadContainerResult } from "./requests/readResource";
import { modifyQueueByMergingEventsWithTheSameKeys } from "./util/modifyQueueFuntions";
export const IS_ROOT_CONTAINER_KEY = "isRootContainer";
export class ContainerRequester extends Requester {
/**
* @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>;
@ -40,22 +62,18 @@ export class ContainerRequester extends Requester {
>;
}
/**
* 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: (queue, currentlyLoading) => {
if (
queue.length === 0 &&
currentlyLoading?.name === IS_ROOT_CONTAINER_KEY
) {
return currentlyLoading;
} else if (queue[queue.length - 1]?.name === IS_ROOT_CONTAINER_KEY) {
return queue[queue.length - 1];
}
return undefined;
},
modifyQueue: modifyQueueByMergingEventsWithTheSameKeys(
IS_ROOT_CONTAINER_KEY,
),
});
}
}

@ -3,7 +3,7 @@ import { mergeDatasetChanges } from "@ldo/subscribable-dataset";
import type { Quad } from "@rdfjs/types";
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import type { LeafUri } from "../util/uriTypes";
import { Requester } from "./Requester";
import { BatchedRequester } from "./BatchedRequester";
import type {
LeafCreateAndOverwriteResult,
LeafCreateIfAbsentResult,
@ -16,26 +16,55 @@ import { uploadResource } from "./requests/uploadResource";
export const UPDATE_KEY = "update";
export const UPLOAD_KEY = "upload";
export class LeafRequester extends Requester {
/**
* @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(
@ -51,23 +80,17 @@ export class LeafRequester extends Requester {
/**
* Update the data on this resource
* @param changes
* @param changes - DatasetChanges that should be applied to the Pod
*/
async updateDataResource(
changes: DatasetChanges<Quad>,
): Promise<UpdateResult> {
const transaction = this.context.solidLdoDataset.startTransaction();
transaction.addAll(changes.added || []);
changes.removed?.forEach((quad) => transaction.delete(quad));
// Commit data optimistically
transaction.commit();
const result = await this.requestBatcher.queueProcess({
name: UPDATE_KEY,
args: [
this.uri,
changes,
{ fetch: this.context.fetch, onRollback: () => transaction.rollback() },
{ fetch: this.context.fetch, dataset: this.context.solidLdoDataset },
],
perform: updateDataResource,
modifyQueue: (queue, currentlyProcessing, [, changes]) => {
@ -84,9 +107,9 @@ export class LeafRequester extends Requester {
}
/**
* Upload a binary
* @param blob
* @param mimeType
* 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(
@ -138,10 +161,12 @@ export class LeafRequester extends Requester {
}
return undefined;
},
after: (result) => {
if (!result.isError) {
transaction.commit();
}
},
});
if (!result.isError) {
transaction.commit();
}
return result;
}
}

@ -11,13 +11,29 @@ 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
| NoncompliantPodError
| 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,
@ -39,6 +55,26 @@ export function checkHeadersForRootContainer(
};
}
/**
* 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,

@ -21,25 +21,117 @@ import type {
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,
@ -102,10 +194,12 @@ export async function createDataResource(
> {
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);
@ -139,7 +233,7 @@ export async function createDataResource(
isError: false,
type: "createSuccess",
uri,
didOverwrite: !!overwrite,
didOverwrite,
};
} catch (err) {
return UnexpectedResourceError.fromThrown(uri, err);

@ -8,9 +8,44 @@ import { HttpErrorResult } from "../results/error/HttpErrorResult";
import type { DeleteSuccess } from "../results/success/DeleteSuccess";
import type { DatasetRequestOptions } from "./requestOptions";
/**
* 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,

@ -1,3 +1,5 @@
/* istanbul ignore file */
export async function getAccessRules(): Promise<undefined> {
throw new Error("Not Implemented");
// const [publicAccess, agentAccess] = await Promise.all([

@ -21,21 +21,65 @@ import { guaranteeFetch } from "../../util/guaranteeFetch";
import { UnexpectedResourceError } from "../results/error/ErrorResult";
import { checkHeadersForRootContainer } from "./checkRootContainer";
/**
* 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,

@ -1,9 +1,22 @@
import type { Dataset, Quad } from "@rdfjs/types";
import type { BulkEditableDataset } 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 {
dataset?: Dataset<Quad>;
/**
* A dataset to be modified with any new information obtained from a request
*/
dataset?: BulkEditableDataset<Quad>;
}

@ -1,3 +1,5 @@
/* istanbul ignore file */
import type { AclDataset, WithChangeLog } from "@inrupt/solid-client";
import {
getSolidDatasetWithAcl,

@ -7,17 +7,67 @@ 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 { BasicRequestOptions } from "./requestOptions";
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?: BasicRequestOptions & { onRollback?: () => void },
options?: DatasetRequestOptions,
): Promise<UpdateResult> {
try {
// Optimistically add data
options?.dataset?.bulk(datasetChanges);
const fetch = guaranteeFetch(options?.fetch);
// Make request
@ -32,8 +82,11 @@ export async function updateDataResource(
const httpError = HttpErrorResult.checkResponse(uri, response);
if (httpError) {
// Handle error rollback
if (options?.onRollback) {
options.onRollback();
if (options?.dataset) {
options.dataset.bulk({
added: datasetChanges.removed,
removed: datasetChanges.added,
});
}
return httpError;
}

@ -15,6 +15,38 @@ 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,
@ -45,10 +77,12 @@ export async function uploadResource(
): 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);
@ -78,9 +112,10 @@ export async function uploadResource(
isError: false,
type: "createSuccess",
uri,
didOverwrite: !!overwrite,
didOverwrite,
};
} catch (err) {
return UnexpectedResourceError.fromThrown(uri, err);
const thing = UnexpectedResourceError.fromThrown(uri, err);
return thing;
}
}

@ -1,3 +1,6 @@
/**
* A type returned by all request functions
*/
export interface RequesterResult {
type: string;
isError: boolean;

@ -1,8 +1,17 @@
/* 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,27 +1,61 @@
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 error unkown error was encountered.");
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 error unkown error for ${uri}`);
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,
@ -47,15 +81,35 @@ export class AggregateError<ErrorType extends ErrorResult> extends ErrorResult {
}
}
/**
* 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);

@ -1,15 +1,37 @@
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;
/**
* 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,
@ -21,21 +43,26 @@ export abstract class HttpErrorResult extends ResourceError {
this.response = response;
}
async getBodyForDebug(): Promise<string> {
if (this.response.bodyUsed) {
return `Could not get body for ${this.uri} that yeilded status ${this.status}. The body stream has already been consumed.`;
}
return await this.response.text();
}
/**
* 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 >= 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);
@ -50,21 +77,43 @@ export abstract class HttpErrorResult extends ResourceError {
}
}
/**
* 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;
}
}
/**
* 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,5 +1,9 @@
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;

@ -1,7 +1,16 @@
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,

@ -1,6 +1,13 @@
import type { ResourceSuccess } 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;
}

@ -1,6 +1,13 @@
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,6 +1,14 @@
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,28 +1,66 @@
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" ||

@ -1,15 +1,31 @@
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,5 +1,8 @@
import type { ResourceSuccess } from "./SuccessResult";
/**
* Indicates that a specific resource is unfetched
*/
export interface Unfetched extends ResourceSuccess {
type: "unfetched";
}

@ -1,5 +1,16 @@
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";
}

@ -0,0 +1,26 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { WaitingProcess } from "../../util/RequestBatcher";
/**
* @internal
*
* A helper function for a common way to modify the batch queue. This merges
* the incoming request with the currently executing request or the last request
* in the queue if its keys are the same.
*
* @param key - the key of the incoming request
* @returns a modifyQueue function
*/
export function modifyQueueByMergingEventsWithTheSameKeys(key: string) {
return (
queue: WaitingProcess<any[], any>[],
currentlyLoading: WaitingProcess<any[], any> | undefined,
) => {
if (queue.length === 0 && currentlyLoading?.name === key) {
return currentlyLoading;
} else if (queue[queue.length - 1]?.name === key) {
return queue[queue.length - 1];
}
return undefined;
};
}

@ -1,5 +1,5 @@
import { namedNode } from "@rdfjs/data-model";
import { ContainerRequester } from "../requester/ContainerRequester";
import { ContainerBatchedRequester } from "../requester/ContainerBatchedRequester";
import type {
CheckRootResult,
CheckRootResultError,
@ -32,12 +32,47 @@ import type { SharedStatuses } from "./Resource";
import { Resource } from "./Resource";
import type { ResourceResult } from "./resourceResult/ResourceResult";
/**
* 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;
protected requester: ContainerRequester;
/**
* @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
@ -45,18 +80,48 @@ export class Container extends Resource {
| 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 ContainerRequester(uri, context);
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
/**
* ===========================================================================
* 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 {
@ -66,12 +131,29 @@ export class Container extends Resource {
}
}
/**
* 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 {
@ -93,6 +175,20 @@ export class Container extends Resource {
}
}
/**
* 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>
> {
@ -101,7 +197,17 @@ export class Container extends Resource {
>;
}
// Parent Container Methods
/**
* ===========================================================================
* 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>
> {
@ -113,29 +219,61 @@ export class Container extends Resource {
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
*
* @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> {
if (this.rootContainer === undefined) {
const checkResult = await this.checkIfIsRootContainer();
if (checkResult.isError) return checkResult;
}
if (this.rootContainer === true) {
const parentContainerResult = await this.getParentContainer();
if (parentContainerResult?.isError) return parentContainerResult;
if (!parentContainerResult) {
return this;
}
const parentUri = getParentUri(this.uri);
if (!parentUri) {
return new NoncompliantPodError(
this.uri,
"Resource does not have a root container",
);
}
return this.context.resourceStore.get(parentUri).getRootContainer();
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
> {
const checkResult = await this.checkIfIsRootContainer();
if (checkResult.isError) return checkResult;
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) {
@ -147,6 +285,20 @@ export class Container extends Resource {
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),
@ -159,6 +311,23 @@ export class Container extends Resource {
});
}
/**
* 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;
@ -166,7 +335,29 @@ export class Container extends Resource {
return this.context.resourceStore.get(`${this.uri}${slug}`);
}
// Child Creators
/**
* ===========================================================================
* 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>>;
@ -192,6 +383,23 @@ export class Container extends Resource {
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>>;
@ -217,6 +425,27 @@ export class Container extends Resource {
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,
@ -225,6 +454,27 @@ export class Container extends Resource {
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,
@ -233,6 +483,20 @@ export class Container extends Resource {
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>>
@ -267,6 +531,17 @@ export class Container extends Resource {
};
}
/**
* 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>,
@ -280,6 +555,18 @@ export class Container extends Resource {
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>
> {
@ -289,6 +576,18 @@ export class Container extends Resource {
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>
> {

@ -1,6 +1,6 @@
import type { DatasetChanges } from "@ldo/rdf-utils";
import type { Quad } from "@rdfjs/types";
import { LeafRequester } from "../requester/LeafRequester";
import { LeafBatchedRequester } from "../requester/LeafBatchedRequester";
import type { CheckRootResultError } from "../requester/requests/checkRootContainer";
import type {
LeafCreateAndOverwriteResult,
@ -24,11 +24,40 @@ import type { SharedStatuses } from "./Resource";
import { Resource } from "./Resource";
import type { ResourceResult } from "./resourceResult/ResourceResult";
/**
* 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;
protected requester: LeafRequester;
/**
* @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
@ -36,34 +65,134 @@ export class Leaf extends Resource {
| 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);
this.uri = uri;
this.requester = new LeafRequester(uri, context);
this.requester = new LeafBatchedRequester(uri, context);
this.status = { isError: false, type: "unfetched", uri };
}
// Getters
/**
* ===========================================================================
* 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;
@ -71,7 +200,17 @@ export class Leaf extends Resource {
return !this.binaryData;
}
// Read Methods
/**
* ===========================================================================
* 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 {
@ -83,12 +222,29 @@ export class Leaf extends Resource {
}
}
/**
* 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 {
@ -119,34 +275,185 @@ export class Leaf extends Resource {
}
}
/**
* 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
/**
* ===========================================================================
* 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 = leaf.getParentContainer();
* if (!leafParent.isError) {
* // Logs "https://example.com/container/"
* console.log(leafParent.uri);
* }
* ```
*/
getParentContainer(): 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);
* }
* ```
*/
getRootContainer(): Promise<Container | CheckRootResultError> {
const parentUri = getParentUri(this.uri)!;
const parent = this.context.resourceStore.get(parentUri);
const parent = this.getParentContainer();
return parent.getRootContainer();
}
// Delete Methods
/**
* ===========================================================================
* DELETE METHODS
* ===========================================================================
*/
/**
* @internal
* A helper method updates this leaf's internal state upon delete success
* @param result - the result of the delete success
*/
protected updateWithDeleteSuccess(_result: DeleteSuccess) {
this.binaryData = undefined;
}
// Create Methods
/**
* 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;
}
// Upload Methods
/**
* 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,
@ -160,6 +467,25 @@ export class Leaf extends Resource {
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,
@ -173,6 +499,43 @@ export class Leaf extends Resource {
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>> {
@ -184,26 +547,4 @@ export class Leaf extends Resource {
this.emitThisAndParent();
return { ...result, resource: this };
}
async delete(): Promise<DeleteResult> {
return this.handleDelete();
}
async createAndOverwrite(): Promise<
ResourceResult<LeafCreateAndOverwriteResult, Leaf>
> {
const createResult =
(await this.handleCreateAndOverwrite()) as LeafCreateAndOverwriteResult;
if (createResult.isError) return createResult;
return { ...createResult, resource: this };
}
async createIfAbsent(): Promise<
ResourceResult<LeafCreateIfAbsentResult, Leaf>
> {
const createResult =
(await this.handleCreateIfAbsent()) as LeafCreateIfAbsentResult;
if (createResult.isError) return createResult;
return { ...createResult, resource: this };
}
}

@ -9,7 +9,7 @@ import type {
ReadContainerResult,
ReadLeafResult,
} from "../requester/requests/readResource";
import type { Requester } from "../requester/Requester";
import type { BatchedRequester } from "../requester/BatchedRequester";
import type { CheckRootResultError } from "../requester/requests/checkRootContainer";
import type { AccessRule } from "../requester/results/success/AccessRule";
import type { SetAccessRulesResult } from "../requester/requests/setAccessRules";
@ -29,60 +29,271 @@ import type { ResourceResult } from "./resourceResult/ResourceResult";
import type { Container } from "./Container";
import type { Leaf } from "./Leaf";
/**
* 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;
}>) {
// All intance variables
/**
* @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;
protected abstract readonly requester: Requester;
/**
* @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;
/**
* @param context - SolidLdoDatasetContext for the parent dataset
*/
constructor(context: SolidLdoDatasetContext) {
super();
this.context = context;
}
// Loading Methods
/**
* ===========================================================================
* 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
/**
* ===========================================================================
* 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 = 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 = 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;
}
// Helper Methods
/**
* ===========================================================================
* HELPER METHODS
* ===========================================================================
*/
/**
* @internal
* Emits an update event for both this resource and the parent
*/
protected emitThisAndParent() {
this.emit("update");
const parentUri = getParentUri(this.uri);
@ -92,12 +303,27 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
}
}
// Read Methods
/**
* ===========================================================================
* 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;
@ -107,15 +333,27 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
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>
> {
@ -127,12 +365,27 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
return this.read();
}
// Delete Methods
/**
* ===========================================================================
* DELETE METHODS
* ===========================================================================
*/
/**
* @internal
* A helper method updates this resource's internal state upon delete success
* @param result - the result of the delete success
*/
protected 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;
@ -142,7 +395,16 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
return result;
}
// Create Methods
/**
* ===========================================================================
* 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;
@ -151,6 +413,18 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
}
}
/**
* 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,
@ -158,6 +432,12 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
>
>;
/**
* @internal
* Helper method that handles the core functions for creating and overwriting
* a resource
* @returns DeleteResult
*/
protected async handleCreateAndOverwrite(): Promise<
ContainerCreateAndOverwriteResult | LeafCreateAndOverwriteResult
> {
@ -169,6 +449,18 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
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,
@ -176,6 +468,12 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
>
>;
/**
* @internal
* Helper method that handles the core functions for creating a resource if
* absent
* @returns DeleteResult
*/
protected async handleCreateIfAbsent(): Promise<
ContainerCreateIfAbsentResult | LeafCreateIfAbsentResult
> {
@ -187,13 +485,36 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
return result;
}
// Parent Container Methods -- Remember to change for Container
/**
* ===========================================================================
* 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>;
// Access Rules Methods
// async getAccessRules(): Promise<AccessRuleResult | AccessRuleFetchError> {
// return getAccessRules({ uri: this.uri, fetch: this.context.fetch });
// }
/* istanbul ignore next */
async setAccessRules(
newAccessRules: AccessRule,
): Promise<ResourceResult<SetAccessRulesResult, Leaf | Container>> {

@ -2,11 +2,17 @@ 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,

@ -5,60 +5,105 @@ export interface WaitingProcess<Args extends any[], Return> {
perform: (...args: Args) => Promise<Return>;
awaitingResolutions: ((returnValue: Return) => void)[];
awaitingRejections: ((err: any) => void)[];
after?: (result: Return) => void;
}
export const ANY_KEY = "any";
/**
* Options for processes that are waiting to execute
*/
export interface WaitingProcessOptions<Args extends any[], Return> {
/**
* The name of the process like "read" or "delete"
*/
name: string;
/**
* The arguements supplied to the process
*/
args: Args;
/**
* A function that will be triggered when it's time to execute this process
* @param args - arguments supplied to the process
* @returns a return type
*/
perform: (...args: Args) => Promise<Return>;
/**
*
* @param processQueue The current process queue
* @param currentlyProcessing: The Process that is currently executing
* @param args provided args
* @returns A WaitingProcess that this request should listen to, or undefined if it should create its own
* A custom function to modify the queue based on the current state of the
* queue
* @param processQueue - The current process queue
* @param currentlyProcessing - The Process that is currently executing
* @param args - provided args
* @returns A WaitingProcess that this request should listen to, or undefined
* if it should create its own
*/
modifyQueue: (
processQueue: WaitingProcess<any[], any>[],
currentlyProcessing: WaitingProcess<any[], any> | undefined,
args: Args,
) => WaitingProcess<any[], any> | undefined;
after?: (result: Return) => void;
}
/**
* Request Batcher
* @internal
* A utility for batching a request
*/
export class RequestBatcher {
/**
* A mapping between a process key and the last time in UTC a process of that
* key was executed.
*/
private lastRequestTimestampMap: Record<string, number> = {};
/**
* A pointer to the current process the batcher is working on
*/
private currentlyProcessing: WaitingProcess<any[], any> | undefined =
undefined;
/**
* A queue of upcoming processes
*/
private processQueue: WaitingProcess<any[], any>[] = [];
public shouldBatchAllRequests: boolean;
/**
* The amount of time (in milliseconds) between requests of the same key
*/
public batchMillis: number;
/**
* @param options - options, including the value for batchMillis
*/
constructor(
options?: Partial<{
shouldBatchAllRequests: boolean;
batchMillis: number;
}>,
) {
this.shouldBatchAllRequests = options?.shouldBatchAllRequests || false;
this.batchMillis = options?.batchMillis || 1000;
}
/**
* Check if the request batcher is currently working on a process
* @param key - the key of the process to check
* @returns true if the batcher is currently working on the provided process
*/
public isLoading(key: string): boolean {
if (key === ANY_KEY) return !!this.currentlyProcessing;
return this.currentlyProcessing?.name === key;
}
/**
* Triggers the next process in the queue or triggers a timeout to wait to
* execute the next process in the queue if not enough time has passed since
* the last process was triggered.
*/
private triggerOrWaitProcess() {
if (!this.processQueue[0]) {
if (!this.processQueue[0] || this.currentlyProcessing) {
return;
}
const processName = this.shouldBatchAllRequests
? ANY_KEY
: this.processQueue[0].name;
this.currentlyProcessing = this.processQueue.shift();
const processName = this.currentlyProcessing!.name;
// Set last request timestamp if not available
if (!this.lastRequestTimestampMap[processName]) {
@ -69,18 +114,19 @@ export class RequestBatcher {
const timeSinceLastTrigger = Date.now() - lastRequestTimestamp;
const triggerProcess = async () => {
if (this.currentlyProcessing) {
return;
}
this.lastRequestTimestampMap[processName] = Date.now();
this.lastRequestTimestampMap[ANY_KEY] = Date.now();
const processToTrigger = this.processQueue.shift();
// Remove the process from the queue
const processToTrigger = this.currentlyProcessing;
if (processToTrigger) {
this.currentlyProcessing = processToTrigger;
try {
const returnValue = await processToTrigger.perform(
...processToTrigger.args,
);
if (processToTrigger.after) {
processToTrigger.after(returnValue);
}
processToTrigger.awaitingResolutions.forEach((callback) => {
callback(returnValue);
});
@ -102,6 +148,11 @@ export class RequestBatcher {
}
}
/**
* Adds a process to the queue and waits for the process to be complete
* @param options - WaitingProcessOptions
* @returns A promise that resolves when the process resolves
*/
public async queueProcess<Args extends any[], ReturnType>(
options: WaitingProcessOptions<Args, ReturnType>,
): Promise<ReturnType> {
@ -124,6 +175,7 @@ export class RequestBatcher {
perform: options.perform,
awaitingResolutions: [resolve],
awaitingRejections: [reject],
after: options.after,
};
// HACK: Ugly cast
this.processQueue.push(

@ -1,5 +1,12 @@
import crossFetch from "cross-fetch";
/**
* @internal
* Guantees that some kind of fetch is available
*
* @param fetchInput - A potential fetch object
* @returns a proper fetch object. Cross-fetch is default
*/
export function guaranteeFetch(fetchInput?: typeof fetch): typeof fetch {
return fetchInput || crossFetch;
}

@ -16,6 +16,13 @@ export const ldpBasicContainer = namedNode(
"http://www.w3.org/ns/ldp#BasicContainer",
);
/**
* @internal
* Gets the URI of a parent according the the Solid Spec
*
* @param uri - the child URI
* @returns A parent URI or undefined if not possible
*/
export function getParentUri(uri: string): ContainerUri | undefined {
const urlObject = new URL(uri);
const pathItems = urlObject.pathname.split("/");
@ -33,12 +40,26 @@ export function getParentUri(uri: string): ContainerUri | undefined {
return urlObject.toString() as ContainerUri;
}
/**
* @internal
* Gets the slug (last part of the path) for a given URI
*
* @param uri - the full URI
* @returns the slug of the URI
*/
export function getSlug(uri: string): string {
const urlObject = new URL(uri);
const pathItems = urlObject.pathname.split("/");
return pathItems[pathItems.length - 1] || pathItems[pathItems.length - 2];
}
/**
* @internal
* Deletes mention of a resource from the provided dataset
*
* @param resourceUri - the resource to delete
* @param dataset - dataset to modify
*/
export function deleteResourceRdfFromContainer(
resourceUri: string,
dataset: Dataset,
@ -54,6 +75,13 @@ export function deleteResourceRdfFromContainer(
}
}
/**
* @internal
* Adds a resource to a container in an RDF dataset
*
* @param resourceUri - the resource to add
* @param dataset - the dataset to modify
*/
export function addResourceRdfToContainer(
resourceUri: string,
dataset: Dataset,
@ -74,6 +102,14 @@ export function addResourceRdfToContainer(
}
}
/**
* @internal
* Adds raw turtle to the provided dataset
* @param rawTurtle - String of raw turtle
* @param dataset - the dataset to modify
* @param baseUri - base URI to parsing turtle
* @returns Undefined if successful, noncompliantPodError if not
*/
export async function addRawTurtleToDataset(
rawTurtle: string,
dataset: Dataset,
@ -88,7 +124,7 @@ export async function addRawTurtleToDataset(
const error = UnexpectedResourceError.fromThrown(baseUri, err);
return new NoncompliantPodError(
baseUri,
`Request at ${baseUri} returned noncompliant turtle: ${error.message}`,
`Request returned noncompliant turtle: ${error.message}`,
);
}

@ -3,16 +3,35 @@ import type { GraphNode, DatasetChanges } from "@ldo/rdf-utils";
import type { Quad } from "@rdfjs/types";
import { defaultGraph, namedNode, quad as createQuad } from "@rdfjs/data-model";
/**
* @internal
* Converts an RDFJS Graph Node to a string hash
* @param graphNode - the node to convert
* @returns a unique string corresponding to the node
*/
export function graphNodeToString(graphNode: GraphNode): string {
return graphNode.termType === "DefaultGraph"
? "defaultGraph()"
: graphNode.value;
}
/**
* @internal
* Converts a unique string to a GraphNode
* @param input - the unique string
* @returns A graph node
*/
export function stringToGraphNode(input: string): GraphNode {
return input === "defaultGraph()" ? defaultGraph() : namedNode(input);
}
/**
* Splits all changes in a DatasetChanges into individual DatasetChanges grouped
* by the quad graph.
* @param changes - Changes to split
* @returns A map between the quad graph and the changes associated with that
* graph
*/
export function splitChangesByGraph(
changes: DatasetChanges<Quad>,
): Map<GraphNode, DatasetChanges<Quad>> {

@ -1,17 +1,47 @@
export type ContainerUri = `${string}/${NonPathnameEnding}`;
/**
* A LeafUri is any URI that has a pahtname that ends in a "/". It represents a
* container.
*/
// The & {} allows for alias preservation
// eslint-disable-next-line @typescript-eslint/ban-types
export type ContainerUri = `${string}/${NonPathnameEnding}` & {};
/**
* A LeafUri is any URI that does not have a pahtname that ends in a "/". It
* represents a data resource or a binary resource. Not a container.
*/
export type LeafUri =
`${string}${EveryLegalPathnameCharacterOtherThanSlash}${NonPathnameEnding}`;
// The & {} allows for alias preservation
// eslint-disable-next-line @typescript-eslint/ban-types
`${string}${EveryLegalPathnameCharacterOtherThanSlash}${NonPathnameEnding}` & {};
/**
* Checks if a provided string is a Container URI
* @param uri - the string to check
* @returns true if the string is a container URI
*/
export function isContainerUri(uri: string): uri is ContainerUri {
const url = new URL(uri);
return url.pathname.endsWith("/");
}
/**
* Checks if a provided string is a leaf URI
* @param uri - the string to check
* @returns true if the string is a leaf URI
*/
export function isLeafUri(uri: string): uri is LeafUri {
return !isContainerUri(uri);
}
/**
* @internal
*/
type NonPathnameEnding = "" | `?${string}` | `#${string}`;
/**
* @internal
*/
type EveryLegalPathnameCharacterOtherThanSlash =
| "A"
| "B"

@ -0,0 +1,31 @@
import { ContextDefinition } from "jsonld";
/**
* =============================================================================
* postContext: JSONLD Context for post
* =============================================================================
*/
export const postContext: ContextDefinition = {
type: {
"@id": "@type",
},
SocialMediaPosting: "http://schema.org/SocialMediaPosting",
CreativeWork: "http://schema.org/CreativeWork",
Thing: "http://schema.org/Thing",
articleBody: {
"@id": "http://schema.org/articleBody",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
uploadDate: {
"@id": "http://schema.org/uploadDate",
"@type": "http://www.w3.org/2001/XMLSchema#date",
},
image: {
"@id": "http://schema.org/image",
"@type": "@id",
},
publisher: {
"@id": "http://schema.org/publisher",
"@type": "@id",
},
};

@ -0,0 +1,155 @@
import { Schema } from "shexj";
/**
* =============================================================================
* postSchema: ShexJ Schema for post
* =============================================================================
*/
export const postSchema: Schema = {
type: "Schema",
shapes: [
{
id: "https://example.com/PostSh",
type: "ShapeDecl",
shapeExpr: {
type: "Shape",
expression: {
type: "EachOf",
expressions: [
{
type: "TripleConstraint",
predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
valueExpr: {
type: "NodeConstraint",
values: [
"http://schema.org/SocialMediaPosting",
"http://schema.org/CreativeWork",
"http://schema.org/Thing",
],
},
},
{
type: "TripleConstraint",
predicate: "http://schema.org/articleBody",
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#label",
object: {
value: "articleBody",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The actual body of the article. ",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://schema.org/uploadDate",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#date",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#label",
object: {
value: "uploadDate",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"Date when this media object was uploaded to this site.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://schema.org/image",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
min: 0,
max: 1,
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#label",
object: {
value: "image",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"A media object that encodes this CreativeWork. This property is a synonym for encoding.",
},
},
],
},
{
type: "TripleConstraint",
predicate: "http://schema.org/publisher",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#label",
object: {
value: "publisher",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value: "The publisher of the creative work.",
},
},
],
},
],
},
annotations: [
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#label",
object: {
value: "SocialMediaPost",
},
},
{
type: "Annotation",
predicate: "http://www.w3.org/2000/01/rdf-schema#comment",
object: {
value:
"A post to a social media platform, including blog posts, tweets, Facebook posts, etc.",
},
},
],
},
},
],
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save