├── .gitignore ├── CNAME ├── README.md ├── common └── main.js ├── fission ├── Glossary.md ├── README.md ├── fission.js ├── index.html ├── main.js └── vite.config.js ├── index.html ├── remotestorage ├── Glossary.md ├── README.md ├── index.html ├── main.js ├── remotestorage-module.js ├── remotestorage.js └── vite.config.js ├── solid ├── Glossary.md ├── README.md ├── index.html ├── solid-file-client │ ├── README.md │ ├── index.html │ ├── main.js │ ├── solid.js │ └── vite.config.js ├── solid-rest-api │ ├── README.md │ ├── index.html │ ├── main.js │ ├── solid.js │ └── vite.config.js └── vite.config.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | solid-pod 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | hello.0data.app -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 0data Hello World 2 | 3 | These are simple 0data apps that demonstrate CRUD operations for /todos only using HTML, CSS, and JavaScript (minimal dependencies, no build systems). Please consult the README for any protocol to learn more: 4 | 5 | - [Fission](fission) 6 | - [remoteStorage](remotestorage) 7 | - [Solid](solid) 8 | 9 | ## Usage instructions 10 | 11 | If you want to modify the code, you'll also need to serve the application in a url. You could just open the `index.html` file in a browser, but unfortunately that will not work because the authentication flow performs a redirect and that won't work with a website being served with the `file://` protocol. 12 | 13 | Any tool to run a local server will work, for example you could use [ViteJS](https://vitejs.dev/): 14 | 15 | ```sh 16 | npm install -g vite 17 | vite . 18 | ``` 19 | -------------------------------------------------------------------------------- /common/main.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const user = await restoreSession(); 3 | 4 | document.getElementById('loading').setAttribute('hidden', ''); 5 | 6 | if (!user) { 7 | document.getElementById('auth-guest').removeAttribute('hidden'); 8 | 9 | return; 10 | } 11 | 12 | document.getElementById('username').innerHTML = `${user.name}`; 13 | document.getElementById('auth-user').removeAttribute('hidden'); 14 | 15 | const tasks = await loadTasks(); 16 | 17 | for (const task of tasks) { 18 | appendTaskItem(task); 19 | } 20 | } 21 | 22 | function login() { 23 | const loginUrl = getLoginUrl(); 24 | 25 | if (!loginUrl) 26 | return; 27 | 28 | performLogin(loginUrl); 29 | } 30 | 31 | async function logout() { 32 | document.getElementById('logout-button').setAttribute('disabled', ''); 33 | 34 | await performLogout(); 35 | 36 | document.getElementById('auth-guest').removeAttribute('hidden'); 37 | document.getElementById('auth-user').setAttribute('hidden', ''); 38 | document.getElementById('logout-button').removeAttribute('disabled'); 39 | } 40 | 41 | async function createTask() { 42 | const description = prompt('Task description'); 43 | 44 | if (!description) 45 | return; 46 | 47 | const task = await performTaskCreation(description); 48 | 49 | appendTaskItem(task); 50 | } 51 | 52 | async function updateTask(taskUrl, button) { 53 | const done = button.innerText === 'Complete'; 54 | button.setAttribute('disabled', ''); 55 | 56 | await performTaskUpdate(taskUrl, done); 57 | 58 | button.removeAttribute('disabled'); 59 | button.innerText = done ? 'Undo' : 'Complete'; 60 | } 61 | 62 | async function deleteTask(taskUrl, taskElement, button) { 63 | button.setAttribute('disabled', ''); 64 | 65 | await performTaskDeletion(taskUrl); 66 | 67 | taskElement.remove(); 68 | } 69 | 70 | function appendTaskItem(task) { 71 | const taskItem = document.createElement('li'); 72 | 73 | taskItem.innerHTML = ` 74 | 80 | 87 | ${task.description} 88 | `; 89 | 90 | document.getElementById('tasks').appendChild(taskItem); 91 | } 92 | 93 | // ------------------------------------------------------------------ 94 | 95 | main(); 96 | 97 | window.onunhandledrejection = (error) => alert(`Error: ${error.reason?.message}`); 98 | -------------------------------------------------------------------------------- /fission/Glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | - **Webnative**: The name of the Fission SDK. Currently focused on Fission infrastructure, the goal is to be fully decentralised. [Learn more](https://guide.fission.codes/developers/webnative). 4 | - **WNFS**: Short for Web Native File System. A filesystem with both encrypted and public files. Access is granted through private keys. [Learn more](https://guide.fission.codes/developers/webnative/file-system-wnfs). 5 | - **Auth lobby**: The authentication/authorisation lobby. Here lives your private key that has access to everything in your filesystem. From here you can link other devices and grant apps to specific parts of your data. [Learn more](https://guide.fission.codes/developers/webnative/auth). 6 | - **DID**: Decentralised identifier, used to identify things or people. Fission accounts have both an associated username and a DID. [Learn more](https://en.wikipedia.org/wiki/Decentralized_identifiers). 7 | - **UCAN**: User Controlled Authorization Network, is a specialised JWT (JSON Web Token) that allows you to do decentralised authorisation (not needing a centralised server). The lobby issues UCANs to apps and your other devices. [Learn more](https://github.com/ucan-wg/ts-ucan). 8 | - **IPFS**: A distributed system for storing and accessing content-addressed data. Webnative and Fission uses this for networking and data transfer. [Learn more](https://docs.ipfs.io/concepts/what-is-ipfs/). 9 | 10 | Learn more about Fission at https://fission.codes. 11 | -------------------------------------------------------------------------------- /fission/README.md: -------------------------------------------------------------------------------- 1 | # Fission Hello World 2 | 3 | This is a simple application illustrating how to get started with [Fission](https://fission.codes). 4 | 5 | It only has one dependency: the [webnative SDK](https://github.com/fission-suite/webnative). Everything else is plain HTML, CSS and JavaScript. All the functionality related with Fission is contained in a single file; `fission.js`. 6 | 7 | ## Documentation 8 | 9 | You can find the documentation in the `fission.js` file, and some general Fission concepts in [the glossary](Glossary.md). 10 | 11 | The `index.html` and `main.js` files are not documented, but they should be fairly easy to understand if you're already familiar with HTML and JavaScript. The application doesn't have any custom CSS as it is using a classless CSS framework called [Simple.css](https://simplecss.org). 12 | 13 | ## Usage instructions 14 | 15 | If you want to play around with the application, you'll need to log into a [Fission account](https://auth.fission.codes). 16 | 17 | If you want to modify the code, you'll also need to serve the application in a url. You could just open the `index.html` file in a browser, but unfortunately that will not work because the authentication flow performs a redirect and that won't work with a website being served with the `file://` protocol. 18 | 19 | Any tool to run a local server will work, for example you could use [ViteJS](https://vitejs.dev/): 20 | 21 | ```sh 22 | npm install -g vite 23 | vite . 24 | ``` 25 | -------------------------------------------------------------------------------- /fission/fission.js: -------------------------------------------------------------------------------- 1 | let publicLink = null; 2 | let username, goToLobby, wnfs; 3 | 4 | async function init() { 5 | // we work with the webnative library through the `state` variable returned from this function 6 | await webnative.initialize({ 7 | permissions: { 8 | // there is also an alternate 'Apps' directory for which you can claim permission, see https://guide.fission.codes/developers/webnative/auth 9 | fs: { 10 | private: [webnative.path.directory('todos')] 11 | } 12 | } 13 | }).then(async state => { 14 | switch (state.scenario) { 15 | case webnative.Scenario.AuthSucceeded: 16 | case webnative.Scenario.Continuation: 17 | // authorized 18 | username = state.username; 19 | wnfs = state.fs; 20 | 21 | break; 22 | 23 | case webnative.Scenario.NotAuthorised: 24 | case webnative.Scenario.AuthCancelled: 25 | // not authorized 26 | goToLobby = function () { 27 | webnative.redirectToLobby(state.permissions); 28 | }; 29 | 30 | break; 31 | } 32 | }).catch(error => { 33 | switch (error) { 34 | // private mode and non-localhost:3000 may cause issues 35 | case webnative.InitialisationError.UnsupportedBrowser: 36 | window.alert('Unsupported browser.') 37 | break; 38 | 39 | case webnative.InitialisationError.InsecureContext: 40 | window.alert('Insecure context.') 41 | break; 42 | } 43 | }) 44 | } 45 | 46 | async function restoreSession() { 47 | // wait for library ready event 48 | await init(); 49 | 50 | try { 51 | if (!username) 52 | return false; 53 | 54 | return { 55 | name: username, 56 | url: username, 57 | }; 58 | } catch (error) { 59 | alert(error.message); 60 | 61 | return false; 62 | } 63 | } 64 | 65 | function getLoginUrl() { 66 | return 'TO_BE_DETERMINED'; 67 | } 68 | 69 | function performLogin(loginUrl) { 70 | goToLobby(); 71 | } 72 | 73 | async function performLogout() { 74 | await webnative.leave({ 75 | withoutRedirect: true, 76 | }); 77 | } 78 | 79 | async function performTaskCreation(description) { 80 | const url = Date.now().toString(36).toLowerCase(); 81 | const item = { 82 | url, 83 | description, 84 | }; 85 | 86 | await wnfs.write(webnative.path.file('private', 'todos', url), JSON.stringify(item)); 87 | await wnfs.publish(); 88 | 89 | return item; 90 | } 91 | 92 | async function performTaskUpdate(taskUrl, done) { 93 | const item = JSON.parse(await wnfs.cat(webnative.path.file('private', 'todos', taskUrl))); 94 | 95 | await wnfs.write(webnative.path.file('private', 'todos', item.url), JSON.stringify(Object.assign(item, { 96 | done, 97 | }))); 98 | await wnfs.publish(); 99 | 100 | return item; 101 | } 102 | 103 | async function performTaskDeletion(taskUrl) { 104 | await wnfs.rm(webnative.path.file('private', 'todos', taskUrl)) 105 | await wnfs.publish(); 106 | } 107 | 108 | async function loadTasks() { 109 | return (await Promise.all(Object.keys(await wnfs.ls(webnative.path.directory('private', 'todos'))).map(function (e) { 110 | return wnfs.cat(webnative.path.file('private', 'todos', e)); 111 | }))).map(JSON.parse); 112 | } 113 | -------------------------------------------------------------------------------- /fission/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fission Hello World 8 | 9 | 10 | 11 | 12 |

Fission Hello World

13 | 14 |
15 |

Loading...

16 |
17 | 18 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | Or, go back to the homepage. 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /fission/main.js: -------------------------------------------------------------------------------- 1 | ../common/main.js -------------------------------------------------------------------------------- /fission/vite.config.js: -------------------------------------------------------------------------------- 1 | ../vite.config.js -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 0data Hello World 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

0data Hello World

17 | 18 |

These are simple 0data apps that demonstrate CRUD operations for /todos only using HTML, CSS, and JavaScript (minimal dependencies, no build systems). Choose one of the protocols to give it a try:

19 | 20 | 25 | 26 |

This project came out of Zero Data Swap #4.

27 | 28 | All CSS styles are from Simple.css. 29 | You may also be interested in the source code. 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /remotestorage/Glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | - **Solid POD:** This is your personal storage where Solid applications store files. 4 | - **Identity provider:** This is the service used to perform authentication, it is often served in the same url as your POD (but not always). 5 | - **WebId:** The url that identifies you as a person, for example `https://noeldemartin.solidcommunity.net/profile/card#me`. WebIds can also identify organizations and groups, it's not limited to individuals. 6 | - **Solid document:** Data stored in your POD can either be a binary, like an image or video, or an RDF document. Although these are called documents, this doesn't mean that they are stored in a text file. Documents are represented by a url, and the Solid POD can persist them in any way (text files, database, etc.). 7 | - **Solid container:** A collection of documents and binary resources. [Learn more](https://www.w3.org/TR/ldp-primer/). 8 | - **Solid Profile:** This is the document that contains information about you. It is the document that contains your webId. For example `https://noeldemartin.solidcommunity.net/profile/card`. 9 | - **RDF:** Resource Definition Framework, the abstract data representation for data in Solid. [Learn more](https://www.w3.org/TR/rdf11-concepts/). 10 | - **Turtle:** A specific RDF encoding format. [Learn more](https://www.w3.org/TR/turtle/). 11 | - **SPARQL:** Query language used to query RDF data. [Learn more](https://www.w3.org/TR/sparql11-query/). 12 | 13 | Learn more about Solid at https://solidproject.org. 14 | -------------------------------------------------------------------------------- /remotestorage/README.md: -------------------------------------------------------------------------------- 1 | # remoteStorage Hello World 2 | 3 | This is a simple application illustrating how to get started with [remoteStorage](https://remotestorage.io). 4 | 5 | It only has one dependency: [the remoteStorage.js library](https://github.com/remotestorage/remotestorage.js). Everything else is plain HTML, CSS and JavaScript. All the functionality related with remoteStorage is contained in a single file; `remotestorage.js`. 6 | 7 | ## Documentation 8 | 9 | You can find the documentation in the `remotestorage.js` file, and some general remoteStorage concepts in [the glossary](Glossary.md). 10 | 11 | The `index.html` and `main.js` files are not documented, but they should be fairly easy to understand if you're already familiar with HTML and JavaScript. The application doesn't have any custom CSS as it is using a classless CSS framework called [Simple.css](https://simplecss.org). 12 | 13 | ## Usage instructions 14 | 15 | If you want to play around with the application, you'll need to log into a [remoteStorage server](https://remotestorage.io/servers). 16 | 17 | If you want to modify the code, you'll also need to serve the application in a url. You could just open the `index.html` file in a browser, but unfortunately that will not work because the authentication flow performs a redirect and that won't work with a website being served with the `file://` protocol. 18 | 19 | Any tool to run a local server will work, for example you could use [ViteJS](https://vitejs.dev/): 20 | 21 | ```sh 22 | npm install -g vite 23 | vite . 24 | ``` 25 | -------------------------------------------------------------------------------- /remotestorage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | remoteStorage Hello World 8 | 9 | 10 | 11 | 12 |

remoteStorage Hello World

13 | 14 |
15 |

Loading...

16 |
17 | 18 | 32 | 33 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Or, go back to the homepage. 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /remotestorage/main.js: -------------------------------------------------------------------------------- 1 | ../common/main.js -------------------------------------------------------------------------------- /remotestorage/remotestorage-module.js: -------------------------------------------------------------------------------- 1 | var Todos = { 2 | name: 'todos', 3 | builder: function(privateClient) { 4 | privateClient.declareType('todo', { 5 | type: 'object', 6 | properties: { 7 | description: { type: 'string' }, 8 | done: { type: 'boolean' } 9 | }, 10 | required: ['description'] 11 | }); 12 | 13 | return { 14 | exports: { 15 | 16 | init: function() { 17 | privateClient.cache(''); 18 | }, 19 | 20 | on: privateClient.on, 21 | 22 | async addTask (description) { 23 | const url = Date.now().toString(36).toLowerCase(); 24 | const item = { 25 | url, 26 | description, 27 | }; 28 | 29 | await privateClient.storeObject('todo', url, item); 30 | 31 | return item; 32 | }, 33 | 34 | async updateTask (url, done) { 35 | // set `maxAge` to `false` to read from cache first 36 | const item = await privateClient.getObject(url, false); 37 | 38 | await privateClient.storeObject('todo', item.url, Object.assign(item, { 39 | done, 40 | })); 41 | 42 | return item; 43 | }, 44 | 45 | deleteTask: privateClient.remove.bind(privateClient), 46 | 47 | listTasks: function() { 48 | return privateClient.getAll(''); 49 | } 50 | } 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /remotestorage/remotestorage.js: -------------------------------------------------------------------------------- 1 | const remoteStorage = new RemoteStorage({ 2 | modules: [ 3 | Todos, 4 | ], 5 | }); 6 | 7 | async function init() { 8 | remoteStorage.access.claim('todos', 'rw'); 9 | 10 | // // mount the optional Connect widget 11 | // var widget = new Widget(remoteStorage); 12 | // widget.attach(); 13 | 14 | remoteStorage.todos.init(); 15 | 16 | // // handle change events 17 | // remoteStorage.todos.on('change', function(event) { 18 | // if(event.newValue && (! event.oldValue)) { 19 | // console.log('Change from '+event.origin+' (add)', event); 20 | // } 21 | // else if((! event.newValue) && event.oldValue) { 22 | // console.log('Change from '+event.origin+' (remove)', event); 23 | // } 24 | // else if(event.newValue && event.oldValue) { 25 | // console.log('Change from '+event.origin+' (change)', event); 26 | // } 27 | // }); 28 | 29 | // wrap ready event handler in promise 30 | return new Promise(function (res) { 31 | remoteStorage.on('ready', function() { 32 | return res(); 33 | }); 34 | }); 35 | } 36 | 37 | async function restoreSession() { 38 | // wait for library ready event 39 | await init(); 40 | 41 | try { 42 | if (!remoteStorage.remote.connected) 43 | return false; 44 | 45 | return { 46 | name: remoteStorage.remote.userAddress, 47 | url: remoteStorage.remote.userAddress, 48 | }; 49 | } catch (error) { 50 | alert(error.message); 51 | 52 | return false; 53 | } 54 | } 55 | 56 | function getLoginUrl() { 57 | const url = prompt('Introduce your remoteStorage address'); 58 | 59 | if (!url) 60 | return null; 61 | 62 | return url; 63 | } 64 | 65 | function performLogin(storageAddress) { 66 | remoteStorage.connect(storageAddress); 67 | } 68 | 69 | async function performLogout() { 70 | return remoteStorage.disconnect(); 71 | } 72 | 73 | async function performTaskCreation(description) { 74 | return await remoteStorage.todos.addTask(description); 75 | } 76 | 77 | async function performTaskUpdate(taskUrl, done) { 78 | await remoteStorage.todos.updateTask(...arguments); 79 | } 80 | 81 | async function performTaskDeletion(taskUrl) { 82 | await remoteStorage.todos.deleteTask(taskUrl); 83 | } 84 | 85 | async function loadTasks() { 86 | return Object.values(await remoteStorage.todos.listTasks()); 87 | } 88 | -------------------------------------------------------------------------------- /remotestorage/vite.config.js: -------------------------------------------------------------------------------- 1 | ../vite.config.js -------------------------------------------------------------------------------- /solid/Glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | The following is a list of basic concepts that you need to understand how Solid works. You can also check out the following presentation that introduces some of them: [RDF in SOLID](https://youtu.be/FEPabu0_3z0?t=967). 4 | 5 | To learn more about Solid in general, visit https://solidproject.org. 6 | 7 | - **Solid POD:** This is your personal storage where Solid applications store files. 8 | - **Identity provider:** This is the service used to perform authentication, it is often served in the same url as your POD (but not always). 9 | - **WebId:** The url that identifies you as a person, for example `https://noeldemartin.solidcommunity.net/profile/card#me`. WebIds can also identify organizations and other entities, it's not limited to individuals. 10 | - **Solid document:** The data stored in your POD can either be a binary, like an image or video, or a document with semantic information (an RDF document). Although these are called documents, this doesn't mean that they are stored in a text file. Documents are represented by a url, and a Solid POD can persist them in any way (text files, database, etc.). As a developer, you don't care about the persistance format because you interact with those using the Solid protocol. 11 | - **Solid container:** A collection of documents and binary resources. [Learn more](https://www.w3.org/TR/ldp-primer/). 12 | - **Solid Profile:** This is the document that contains information about you. It is the document that contains your webId. For example `https://noeldemartin.solidcommunity.net/profile/card`. 13 | - **RDF:** Resource Definition Framework, the abstract data representation language for data in Solid. [Learn more](https://www.w3.org/TR/rdf11-concepts/). 14 | - **Turtle:** A specific RDF encoding format. [Learn more](https://www.w3.org/TR/turtle/). 15 | - **SPARQL:** Query language used to query RDF data. [Learn more](https://www.w3.org/TR/sparql11-query/). 16 | -------------------------------------------------------------------------------- /solid/README.md: -------------------------------------------------------------------------------- 1 | # Solid Hello World 2 | 3 | This folder contains some simple applications illustrating how to get started with [Solid](https://solidproject.org/) using different libraries. 4 | 5 | - [solid-file-client](./solid-file-client): This example uses a higher-level library called [solid-file-client](https://github.com/jeff-zucker/solid-file-client). It's pretty minimal library, but makes some basic operations easier. 6 | - [solid-rest-api](./solid-rest-api): This example is the most minimal, it only uses the authentication library and a Turtle parser. All the interaction with the POD is implemented using the native `fetch` function. 7 | 8 | ## Understanding the code 9 | 10 | If you're not familiar with the basics of Solid, we strongly suggest that you check out [the glossary](Glossary.md). You can find more documentation about each code sample in the source files, they contain inline comments specific to each implementation. 11 | 12 | The `index.html` and `main.js` files are not documented, but they should be fairly easy to understand if you're already familiar with HTML and JavaScript. The application doesn't have any custom CSS because it uses a classless CSS framework called [Simple.css](https://simplecss.org). 13 | 14 | ## Usage instructions 15 | 16 | If you want to play around with the application, you'll need to log into a [Solid POD](https://solidproject.org/users/get-a-pod). If you prefer to run one in your local environment, we suggest that you use the [Community Solid Server (CSS)](https://github.com/solid/community-server) with the filesystem configuration. This will use your filesystem to serve a Solid POD from the folder of your choice; `./solid-pod` in this case: 17 | 18 | ```sh 19 | npm install -g @solid/community-server 20 | community-solid-server -c @css:config/file-no-setup.json -p 4000 -f ./solid-pod 21 | ``` 22 | 23 | If you want to modify the code, you'll also need to serve the application in a url. You could just open the `index.html` file in a browser, but unfortunately that will not work because the authentication flow performs a redirect and that won't work with a website being served with the `file://` protocol. 24 | 25 | Any tool to run a local server will work, for example you could use [ViteJS](https://vitejs.dev/): 26 | 27 | ```sh 28 | npm install -g vite 29 | vite . 30 | ``` 31 | 32 | ## FAQs 33 | 34 | ### Why is this example using Turtle, and not something more developer-friendly like [JSON-LD](https://json-ld.org/)? 35 | 36 | The app could have been built using other RDF formats, but Turtle is the most common in practice and you're likely to find it anyways when looking at other resources. This app is simple enough that the Turtle it's using is easy to understand, so it can also serve as a light introduction to Turtle itself. 37 | 38 | ### Why is this app writing Turtle by hand? Aren't there libraries to do that? 39 | 40 | Yes! We don't actually recommend using this approach in a real application, but it is a good way to understand how Solid works. In the same way that you wouldn't normally write SQL statements by hand, but it's useful to learn SQL before using libraries. 41 | 42 | Also, one of the best ways to debug a Solid application, regardless of what libraries it's using, is opening the network tab and looking at the request payloads. If you want to understand that, you'll need to understand Turtle and how the requests are typically made. 43 | 44 | If you want to use a more advanced library, check out this list of [Solid Developer Tools & Libraries](https://solidproject.org/developers/tools). 45 | 46 | ### This example is too simple, can you make one using more libraries? 47 | 48 | There are already some existing examples using more libraries: 49 | 50 | - [Solid To-Do App Tutorial](https://www.virginiabalseiro.com/blog/tutorial) ([React](https://reactjs.org/) + [solid-client](https://docs.inrupt.com/developer-tools/javascript/client-libraries/tutorial/read-write-data/)): A tutorial of how to build a To-Do app using [Inrupt](https://inrupt.com/)'s libraries. 51 | - [Ramen](https://github.com/noeldemartin/ramen) ([Vue](https://vuejs.org/) + [soukai-solid](https://github.com/noeldemartin/soukai-solid)): A simple application that adds a recipe for Ramen to your POD. This application can also serve as an example to use the type index. 52 | - [Hello Solid](https://wkokgit.github.io/hellosolid/) ([JQuery](https://jquery.com/) + [rdflib](https://github.com/linkeddata/rdflib.js)/[LDFlex](https://github.com/LDflex/LDflex)): A Solid Client application to explain the basics of Solid. Keep in mind that this library uses the deprecated [solid-auth-client](https://github.com/solid/solid-auth-client) for authentication, and will not work with newer Solid PODs. 53 | 54 | You can also check out [this list](https://timea.solidcommunity.net/HelloWorld/index.html). 55 | -------------------------------------------------------------------------------- /solid/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Solid Hello World Examples 8 | 9 | 10 | 11 | 12 |

Solid Hello World Examples

13 | 14 |

Here you can find more examples following the same simple structure, but using some advanced libraries:

15 | 16 | 20 | 21 |

If you would like to see even more advanced examples, you can find them in the FAQs.

22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /solid/solid-file-client/README.md: -------------------------------------------------------------------------------- 1 | # Solid Hello World (solid-file-client) 2 | 3 | This is a simple application illustrating how to get started with [Solid](https://solidproject.org/) using the [solid-file-client](https://github.com/jeff-zucker/solid-file-client) library. 4 | 5 | It only has two dependencies: [an authentication library](https://github.com/inrupt/solid-client-authn-js) and [solid-file-client](https://github.com/jeff-zucker/solid-file-client). Everything else is plain HTML, CSS and JavaScript. All the functionality related to Solid is contained in a single file; `solid.js`. 6 | 7 | ## Understanding the code & Usage instructions 8 | 9 | This example follows the same structure as other examples in this repository, so you can follow these [general instructions](../). 10 | -------------------------------------------------------------------------------- /solid/solid-file-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Solid Hello World (solid-file-client) 8 | 9 | 10 | 11 | 12 |

Solid Hello World
(solid-file-client)

13 | 14 |
15 |

Loading...

16 |
17 | 18 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Or, go back to the homepage. 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /solid/solid-file-client/main.js: -------------------------------------------------------------------------------- 1 | ../../common/main.js -------------------------------------------------------------------------------- /solid/solid-file-client/solid.js: -------------------------------------------------------------------------------- 1 | // You can find the basic Solid concepts explained in the Glossary.md file, inline comments talk about 2 | // the specifics of how this application is implemented. 3 | 4 | let user, tasksContainerUrl; 5 | const solidFileClient = new SolidFileClient(solidClientAuthentication); 6 | 7 | solidFileClient.rdf.setPrefix('schemaorg', 'https://schema.org/'); 8 | 9 | async function restoreSession() { 10 | // This function uses Inrupt's authentication library to restore a previous session. If you were 11 | // already logged into the application last time that you used it, this will trigger a redirect that 12 | // takes you back to the application. This usually happens without user interaction, but if you hadn't 13 | // logged in for a while, your identity provider may ask for your credentials again. 14 | // 15 | // After a successful login, this will also read the profile from your POD. 16 | // 17 | // @see https://docs.inrupt.com/developer-tools/javascript/client-libraries/tutorial/authenticate-browser/ 18 | 19 | try { 20 | await solidClientAuthentication.handleIncomingRedirect({ restorePreviousSession: true }); 21 | 22 | const session = solidClientAuthentication.getDefaultSession(); 23 | 24 | if (!session.info.isLoggedIn) 25 | return false; 26 | 27 | user = await fetchUserProfile(session.info.webId); 28 | 29 | return user; 30 | } catch (error) { 31 | alert(error.message); 32 | 33 | return false; 34 | } 35 | } 36 | 37 | function getLoginUrl() { 38 | // Asking for a login url in Solid is kind of tricky. In a real application, you should be 39 | // asking for a user's webId, and reading the user's profile you would be able to obtain 40 | // the url of their identity provider. However, most users may not know what their webId is, 41 | // and they introduce the url of their issue provider directly. In order to simplify this 42 | // example, we just use the base domain of the url they introduced, and this should work 43 | // most of the time. 44 | const url = prompt('Introduce your Solid login url'); 45 | 46 | if (!url) 47 | return null; 48 | 49 | const loginUrl = new URL(url); 50 | loginUrl.hash = ''; 51 | loginUrl.pathname = ''; 52 | 53 | return loginUrl.href; 54 | } 55 | 56 | function performLogin(loginUrl) { 57 | solidClientAuthentication.login({ 58 | oidcIssuer: loginUrl, 59 | redirectUrl: window.location.href, 60 | clientName: 'Hello World', 61 | }); 62 | } 63 | 64 | async function performLogout() { 65 | await solidClientAuthentication.logout(); 66 | } 67 | 68 | async function performTaskCreation(description) { 69 | // Data discovery mechanisms are still being defined in Solid, but so far it is clear that 70 | // applications should not hard-code the url of their containers like we are doing in this 71 | // example. 72 | // 73 | // In a real application, you should use one of these two alternatives: 74 | // 75 | // - The Type index. This is the one that most applications are using in practice today: 76 | // https://github.com/solid/solid/blob/main/proposals/data-discovery.md#type-index-registry 77 | // 78 | // - SAI, or Solid App Interoperability. This one is still being defined: 79 | // https://solid.github.io/data-interoperability-panel/specification/ 80 | 81 | if (!tasksContainerUrl) 82 | tasksContainerUrl = await createSolidContainer(`${user.storageUrl}tasks/`); 83 | 84 | const documentUrl = await createSolidDocument(tasksContainerUrl, ` 85 | @prefix schema: . 86 | 87 | <#it> 88 | a schema:Action ; 89 | schema:actionStatus schema:PotentialActionStatus ; 90 | schema:description "${escapeText(description)}" . 91 | `); 92 | const taskUrl = `${documentUrl}#it`; 93 | 94 | return { url: taskUrl, description }; 95 | } 96 | 97 | async function performTaskUpdate(taskUrl, done) { 98 | const documentUrl = getSolidDocumentUrl(taskUrl); 99 | 100 | await updateSolidDocument(documentUrl, ` 101 | DELETE DATA { 102 | <#it> 103 | 104 | . 105 | } ; 106 | INSERT DATA { 107 | <#it> 108 | 109 | . 110 | } 111 | `); 112 | } 113 | 114 | async function performTaskDeletion(taskUrl) { 115 | const documentUrl = getSolidDocumentUrl(taskUrl); 116 | 117 | await deleteSolidDocument(documentUrl); 118 | } 119 | 120 | async function loadTasks() { 121 | // In a real application, you shouldn't hard-code the path to the container like we're doing here. 122 | // Read more about this in the comments on the performTaskCreation function. 123 | 124 | const containerUrl = `${user.storageUrl}tasks/`; 125 | const containmentQuads = await readSolidDocument(containerUrl, null, { ldp: 'contains' }); 126 | 127 | if (!containmentQuads) 128 | return []; 129 | 130 | tasksContainerUrl = containerUrl; 131 | 132 | const tasks = []; 133 | for (const containmentQuad of containmentQuads) { 134 | const [typeQuad] = await readSolidDocument(containmentQuad.object.value, null, { rdf: 'type' }, { schemaorg: 'Action' }); 135 | 136 | if (!typeQuad) { 137 | // Not a Task, we can ignore this document. 138 | 139 | continue; 140 | } 141 | 142 | const taskUrl = typeQuad.subject.value; 143 | const [descriptionQuad] = await readSolidDocument(containmentQuad.object.value, `<${taskUrl}>`, { schemaorg: 'description' }); 144 | const [statusQuad] = await readSolidDocument(containmentQuad.object.value, `<${taskUrl}>`, { schemaorg: 'actionStatus' }); 145 | 146 | tasks.push({ 147 | url: taskUrl, 148 | description: descriptionQuad?.object.value || '-', 149 | done: statusQuad?.object.value === 'https://schema.org/CompletedActionStatus', 150 | }); 151 | } 152 | 153 | return tasks; 154 | } 155 | 156 | async function readSolidDocument(url, source, predicate, object, graph) { 157 | try { 158 | // solidFileClient.rdf.query returns an array of statements with matching terms. 159 | // (load and cache url content) 160 | return await solidFileClient.rdf.query(url, source, predicate, object, graph); 161 | } catch (error) { 162 | return null; 163 | } 164 | } 165 | 166 | async function createSolidDocument(url, contents) { 167 | const response = await solidFileClient.post(url, { 168 | headers: { 'Content-Type': 'text/turtle' }, 169 | body: contents, 170 | }); 171 | 172 | if (!isSuccessfulStatusCode(response.status)) 173 | throw new Error(`Failed creating document at ${url}, returned status ${response.status}`); 174 | 175 | const location = response.headers.get('Location'); 176 | 177 | return new URL(location, url).href; 178 | } 179 | 180 | async function updateSolidDocument(url, update) { 181 | const response = await solidFileClient.patchFile(url, update, 'application/sparql-update'); 182 | 183 | if (!isSuccessfulStatusCode(response.status)) 184 | throw new Error(`Failed updating document at ${url}, returned status ${response.status}`); 185 | } 186 | 187 | async function deleteSolidDocument(url) { 188 | const response = await solidFileClient.deleteFile(url); 189 | 190 | if (!isSuccessfulStatusCode(response.status)) 191 | throw new Error(`Failed deleting document at ${url}, returned status ${response.status}`); 192 | } 193 | 194 | async function createSolidContainer(url) { 195 | const response = await solidFileClient.createFolder(url); 196 | 197 | if (!isSuccessfulStatusCode(response.status)) 198 | throw new Error(`Failed creating container at ${url}, returned status ${response.status}`); 199 | 200 | return url; 201 | } 202 | 203 | function isSuccessfulStatusCode(statusCode) { 204 | return Math.floor(statusCode / 100) === 2; 205 | } 206 | 207 | function getSolidDocumentUrl(resourceUrl) { 208 | const url = new URL(resourceUrl); 209 | 210 | url.hash = ''; 211 | 212 | return url.href; 213 | } 214 | 215 | async function fetchUserProfile(webId) { 216 | const [nameQuad] = await readSolidDocument(webId, null, { foaf: 'name' }); 217 | const [storageQuad] = await readSolidDocument(webId, null, { space: 'storage' }); 218 | 219 | return { 220 | url: webId, 221 | name: nameQuad?.object.value || 'Anonymous', 222 | 223 | // WebIds may declare more than one storage url, so in a real application you should 224 | // ask which one to use if that happens. In this app, in order to keep it simple, we'll 225 | // just use the first one. If none is declared in the profile, we'll search for it. 226 | storageUrl: storageQuad?.object.value || await findUserStorage(webId), 227 | }; 228 | } 229 | 230 | // See https://solidproject.org/TR/protocol#storage. 231 | async function findUserStorage(url) { 232 | url = url.replace(/#.*$/, ''); 233 | url = url.endsWith('/') ? url + '../' : url + '/../'; 234 | url = new URL(url); 235 | 236 | const response = await solidFileClient.head(url.href); 237 | 238 | if (response.headers.get('Link')?.includes('; rel="type"')) 239 | return url.href; 240 | 241 | // Fallback for providers that don't advertise storage properly. 242 | if (url.pathname === '/') 243 | return url.href; 244 | 245 | return findUserStorage(url.href); 246 | } 247 | 248 | function escapeText(text) { 249 | return text.replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0'); 250 | } 251 | -------------------------------------------------------------------------------- /solid/solid-file-client/vite.config.js: -------------------------------------------------------------------------------- 1 | ../../vite.config.js -------------------------------------------------------------------------------- /solid/solid-rest-api/README.md: -------------------------------------------------------------------------------- 1 | # Solid Hello World (REST API) 2 | 3 | This is a simple application illustrating how to get started with [Solid](https://solidproject.org/). 4 | 5 | It only has two dependencies: [an authentication library](https://github.com/inrupt/solid-client-authn-js) and [an RDF parsing library](https://github.com/rdfjs/N3.js). Everything else is plain HTML, CSS and JavaScript. All the functionality related with Solid is contained in a single file; `solid.js`. 6 | 7 | ## Understanding the code & Usage instructions 8 | 9 | This example follows the same structure as other examples in this repository, so you can follow these [general instructions](../). 10 | -------------------------------------------------------------------------------- /solid/solid-rest-api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Solid Hello World (REST API) 8 | 9 | 10 | 11 | 12 |

Solid Hello World
(REST API)

13 | 14 |
15 |

Loading...

16 |
17 | 18 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Or, go back to the homepage. 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /solid/solid-rest-api/main.js: -------------------------------------------------------------------------------- 1 | ../../common/main.js -------------------------------------------------------------------------------- /solid/solid-rest-api/solid.js: -------------------------------------------------------------------------------- 1 | // You can find the basic Solid concepts explained in the Glossary.md file, inline comments talk about 2 | // the specifics of how this application is implemented. 3 | 4 | let user, tasksContainerUrl; 5 | 6 | async function restoreSession() { 7 | // This function uses Inrupt's authentication library to restore a previous session. If you were 8 | // already logged into the application last time that you used it, this will trigger a redirect that 9 | // takes you back to the application. This usually happens without user interaction, but if you hadn't 10 | // logged in for a while, your identity provider may ask for your credentials again. 11 | // 12 | // After a successful login, this will also read the profile from your POD. 13 | // 14 | // @see https://docs.inrupt.com/developer-tools/javascript/client-libraries/tutorial/authenticate-browser/ 15 | 16 | try { 17 | await solidClientAuthentication.handleIncomingRedirect({ restorePreviousSession: true }); 18 | 19 | const session = solidClientAuthentication.getDefaultSession(); 20 | 21 | if (!session.info.isLoggedIn) 22 | return false; 23 | 24 | user = await fetchUserProfile(session.info.webId); 25 | 26 | return user; 27 | } catch (error) { 28 | alert(error.message); 29 | 30 | return false; 31 | } 32 | } 33 | 34 | function getLoginUrl() { 35 | // Asking for a login url in Solid is kind of tricky. In a real application, you should be 36 | // asking for a user's webId, and reading the user's profile you would be able to obtain 37 | // the url of their identity provider. However, most users may not know what their webId is, 38 | // and they introduce the url of their issue provider directly. In order to simplify this 39 | // example, we just use the base domain of the url they introduced, and this should work 40 | // most of the time. 41 | const url = prompt('Introduce your Solid login url'); 42 | 43 | if (!url) 44 | return null; 45 | 46 | const loginUrl = new URL(url); 47 | loginUrl.hash = ''; 48 | loginUrl.pathname = ''; 49 | 50 | return loginUrl.href; 51 | } 52 | 53 | function performLogin(loginUrl) { 54 | solidClientAuthentication.login({ 55 | oidcIssuer: loginUrl, 56 | redirectUrl: window.location.href, 57 | clientName: 'Hello World', 58 | }); 59 | } 60 | 61 | async function performLogout() { 62 | await solidClientAuthentication.logout(); 63 | } 64 | 65 | async function performTaskCreation(description) { 66 | // Data discovery mechanisms are still being defined in Solid, but so far it is clear that 67 | // applications should not hard-code the url of their containers like we are doing in this 68 | // example. 69 | // 70 | // In a real application, you should use one of these two alternatives: 71 | // 72 | // - The Type index. This is the one that most applications are using in practice today: 73 | // https://github.com/solid/solid/blob/main/proposals/data-discovery.md#type-index-registry 74 | // 75 | // - SAI, or Solid App Interoperability. This one is still being defined: 76 | // https://solid.github.io/data-interoperability-panel/specification/ 77 | 78 | if (!tasksContainerUrl) 79 | tasksContainerUrl = await createSolidContainer(user.storageUrl, 'tasks'); 80 | 81 | const documentUrl = await createSolidDocument(tasksContainerUrl, ` 82 | @prefix schema: . 83 | 84 | <#it> 85 | a schema:Action ; 86 | schema:actionStatus schema:PotentialActionStatus ; 87 | schema:description "${escapeText(description)}" . 88 | `); 89 | const taskUrl = `${documentUrl}#it`; 90 | 91 | return { url: taskUrl, description }; 92 | } 93 | 94 | async function performTaskUpdate(taskUrl, done) { 95 | const documentUrl = getSolidDocumentUrl(taskUrl); 96 | 97 | await updateSolidDocument(documentUrl, ` 98 | DELETE DATA { 99 | <#it> 100 | 101 | . 102 | } ; 103 | INSERT DATA { 104 | <#it> 105 | 106 | . 107 | } 108 | `); 109 | } 110 | 111 | async function performTaskDeletion(taskUrl) { 112 | const documentUrl = getSolidDocumentUrl(taskUrl); 113 | 114 | await deleteSolidDocument(documentUrl); 115 | } 116 | 117 | async function loadTasks() { 118 | // In a real application, you shouldn't hard-code the path to the container like we're doing here. 119 | // Read more about this in the comments on the performTaskCreation function. 120 | 121 | const containerQuads = await readSolidDocument(`${user.storageUrl}tasks/`); 122 | 123 | if (!containerQuads) 124 | return []; 125 | 126 | tasksContainerUrl = `${user.storageUrl}tasks/`; 127 | 128 | const tasks = []; 129 | const containmentQuads = containerQuads.filter(quad => quad.predicate.value === 'http://www.w3.org/ns/ldp#contains'); 130 | 131 | for (const containmentQuad of containmentQuads) { 132 | const documentQuads = await readSolidDocument(containmentQuad.object.value); 133 | const typeQuad = documentQuads.find( 134 | quad => 135 | quad.predicate.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' && 136 | quad.object.value === 'https://schema.org/Action' 137 | ); 138 | 139 | if (!typeQuad) { 140 | // Not a Task, we can ignore this document. 141 | 142 | continue; 143 | } 144 | 145 | const taskUrl = typeQuad.subject.value; 146 | const descriptionQuad = documentQuads.find( 147 | quad => 148 | quad.subject.value === taskUrl && 149 | quad.predicate.value === 'https://schema.org/description' 150 | ); 151 | const statusQuad = documentQuads.find( 152 | quad => 153 | quad.subject.value === taskUrl && 154 | quad.predicate.value === 'https://schema.org/actionStatus' 155 | ); 156 | 157 | tasks.push({ 158 | url: taskUrl, 159 | description: descriptionQuad?.object.value || '-', 160 | done: statusQuad?.object.value === 'https://schema.org/CompletedActionStatus', 161 | }); 162 | } 163 | 164 | return tasks; 165 | } 166 | 167 | async function readSolidDocument(url) { 168 | try { 169 | const response = await solidClientAuthentication.fetch(url, { headers: { Accept: 'text/turtle' } }); 170 | 171 | if (!isSuccessfulStatusCode(response.status)) 172 | return null; 173 | 174 | const data = await response.text(); 175 | const parser = new N3.Parser({ baseIRI: url }); 176 | 177 | return parser.parse(data); 178 | } catch (error) { 179 | return null; 180 | } 181 | } 182 | 183 | async function createSolidDocument(url, contents) { 184 | const response = await solidClientAuthentication.fetch(url, { 185 | method: 'POST', 186 | headers: { 'Content-Type': 'text/turtle' }, 187 | body: contents, 188 | }); 189 | 190 | if (!isSuccessfulStatusCode(response.status)) 191 | throw new Error(`Failed creating document at ${url}, returned status ${response.status}`); 192 | 193 | const location = response.headers.get('Location'); 194 | 195 | return new URL(location, url).href; 196 | } 197 | 198 | async function updateSolidDocument(url, update) { 199 | const response = await solidClientAuthentication.fetch(url, { 200 | method: 'PATCH', 201 | headers: { 'Content-Type': 'application/sparql-update' }, 202 | body: update, 203 | }); 204 | 205 | if (!isSuccessfulStatusCode(response.status)) 206 | throw new Error(`Failed updating document at ${url}, returned status ${response.status}`); 207 | } 208 | 209 | async function deleteSolidDocument(url) { 210 | const response = await solidClientAuthentication.fetch(url, { method: 'DELETE' }); 211 | 212 | if (!isSuccessfulStatusCode(response.status)) 213 | throw new Error(`Failed deleting document at ${url}, returned status ${response.status}`); 214 | } 215 | 216 | async function createSolidContainer(url, name) { 217 | const response = await solidClientAuthentication.fetch(url, { 218 | method: 'POST', 219 | headers: { 220 | 'Content-Type': 'text/turtle', 221 | 'Link': '; rel="type"', 222 | 'Slug': name, 223 | }, 224 | }); 225 | 226 | if (!isSuccessfulStatusCode(response.status)) 227 | throw new Error(`Failed creating container at ${url}, returned status ${response.status}`); 228 | 229 | const location = response.headers.get('Location'); 230 | 231 | return new URL(location, url).href; 232 | } 233 | 234 | function isSuccessfulStatusCode(statusCode) { 235 | return Math.floor(statusCode / 100) === 2; 236 | } 237 | 238 | function getSolidDocumentUrl(resourceUrl) { 239 | const url = new URL(resourceUrl); 240 | 241 | url.hash = ''; 242 | 243 | return url.href; 244 | } 245 | 246 | async function fetchUserProfile(webId) { 247 | const profileQuads = await readSolidDocument(webId); 248 | const nameQuad = profileQuads.find(quad => quad.predicate.value === 'http://xmlns.com/foaf/0.1/name'); 249 | const storageQuad = profileQuads.find(quad => quad.predicate.value === 'http://www.w3.org/ns/pim/space#storage'); 250 | 251 | return { 252 | url: webId, 253 | name: nameQuad?.object.value || 'Anonymous', 254 | 255 | // WebIds may declare more than one storage url, so in a real application you should 256 | // ask which one to use if that happens. In this app, in order to keep it simple, we'll 257 | // just use the first one. If none is declared in the profile, we'll search for it. 258 | storageUrl: storageQuad?.object.value || await findUserStorage(webId), 259 | }; 260 | } 261 | 262 | async function findUserStorage(url) { 263 | url = url.replace(/#.*$/, ''); 264 | url = url.endsWith('/') ? url + '../' : url + '/../'; 265 | url = new URL(url); 266 | 267 | const response = await solidClientAuthentication.fetch(url.href); 268 | 269 | if (response.headers.get('Link')?.includes('; rel="type"')) 270 | return url.href; 271 | 272 | if (url.pathname === '/') 273 | return url.href; 274 | 275 | return findUserStorage(url.href); 276 | } 277 | 278 | function escapeText(text) { 279 | return text.replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0'); 280 | } 281 | -------------------------------------------------------------------------------- /solid/solid-rest-api/vite.config.js: -------------------------------------------------------------------------------- 1 | ../../vite.config.js -------------------------------------------------------------------------------- /solid/vite.config.js: -------------------------------------------------------------------------------- 1 | ../vite.config.js -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file allows Vite to serve files from upper directories. You can ignore this 3 | * file if you are not using Vite or you are using the app from a hosted version. 4 | * 5 | * @see https://vitejs.dev/config/#server-fs-strict 6 | */ 7 | module.exports = { 8 | server: { 9 | fs: { strict: false }, 10 | }, 11 | }; 12 | --------------------------------------------------------------------------------