diff --git a/Readme.md b/Readme.md index 84c4085..d084623 100644 --- a/Readme.md +++ b/Readme.md @@ -5,9 +5,6 @@ This is a monorepo that contains all libraries associated with Linked Data Objec ## 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. - ## Libraries The LDO monorepo contains the following - [@ldo/cli](./packages/cli/) diff --git a/documentation/images/Step2.png b/documentation/images/Step2.png deleted file mode 100644 index 5efcc6a..0000000 Binary files a/documentation/images/Step2.png and /dev/null differ diff --git a/documentation/images/Step4Login.png b/documentation/images/Step4Login.png deleted file mode 100644 index 18750d2..0000000 Binary files a/documentation/images/Step4Login.png and /dev/null differ diff --git a/documentation/images/Step4Logout.png b/documentation/images/Step4Logout.png deleted file mode 100644 index 5c91904..0000000 Binary files a/documentation/images/Step4Logout.png and /dev/null differ diff --git a/documentation/images/Step8.png b/documentation/images/Step8.png deleted file mode 100644 index 33f19b3..0000000 Binary files a/documentation/images/Step8.png and /dev/null differ diff --git a/documentation/solid-react-tutorial.md b/documentation/solid-react-tutorial.md deleted file mode 100644 index aef4f58..0000000 --- a/documentation/solid-react-tutorial.md +++ /dev/null @@ -1,787 +0,0 @@ -# Using LDO to build a Solid Application for React - -Solid separates the application from the storage, allowing users to put their data wherever they choose. Core to achieving this is application interoparability, the ability to use multiple apps on the same dataset. In order to make applications interoperable, Solid is standardized around RDF (Resource Description Framework), a standard for representing data. While RDF is extremely flexible, it is sometimes cumbersome to work with, that's where LDO (Linked Data Objects) comes in. - -In this tutorial, we'll build a web application for Solid using React and LDO. LDO's react library, "@ldo/solid-react" is designed to make it easy to manipulate data on a Solid Pod. - -We'll be making a simple micro-blogging website that allows you to write notes and upload photos. - -// TODO insert image - -This tutorial assumes that you are already familiar with React and the overall concepts associated with Solid. - -## 1. Getting Started - -First, we'll initialize the project. LDO is designed to work with TypeScript, so we want to initialize a typescript react project. - -```bash -npx create-react-app my-solid-app --template typescript -cd my-solid-app -``` - -## 2. Setting up a basic app infrastructure - -Before we can use LDO and connect to a Solid Pod, let's get the boilerplace React infrastructure out of the way. We'll set up a single page that renders your blog timeline and lets you make new posts, and we'll do this with 5 components: - -**App.tsx**: Base of the application. -```tsx -import React, { FunctionComponent } from 'react'; -import { Header } from './Header'; -import { Blog } from './Blog'; - -export const App: FunctionComponent = () => { - return ( -
-
- -
- ); -} -``` - -**Header.tsx**: A header component that will help the user log in. -```tsx -import { FunctionComponent } from "react"; - -export const Header: FunctionComponent = () => { - return ( -
-

Header

-
-
- ); -}; -``` - -**Blog.tsx**: The main place for the blog timeline. We'll use this component to list all posts you've made. -```tsx -import { FunctionComponent } from "react"; -import { MakePost } from "./MakePost"; -import { Post } from "./Post"; - -export const Blog: FunctionComponent = () => { - return ( -
- -
- -
- ); -}; -``` - -**MakePost.tsx**: A form to submit new posts. We've already wired it up with form elements to create a text body and upload an image for the post. We just need to fill out the `onSubmit` function. -```tsx -import { FormEvent, FunctionComponent, useCallback, useState } from "react"; - -export const MakePost: FunctionComponent = () => { - const [message, setMessage] = useState(""); - const [selectedFile, setSelectedFile] = useState(); - - const onSubmit = useCallback( - (e: FormEvent) => { - e.preventDefault(); - - // TODO upload functionality - }, - [message, selectedFile] - ); - - return ( -
- setMessage(e.target.value)} - /> - setSelectedFile(e.target.files?.[0])} - /> - -
- ); -}; -``` - -**Post.tsx**: A component to render a single post. -```tsx -import { FunctionComponent } from "react"; - -export const Post: FunctionComponent = () => { - return ( -
-

Post

-
- ); -}; -``` - -When everything's done, run `npm run start` and your application should look like this: - -![What the site should like after step 2 is complete.](./images/Step2.png) - -## Step 3: Installing LDO for Solid and React - -With the main infrastructure set up, let's install LDO's Solid/React library. - -```bash -npm install @ldo/solid-react -``` - -This library will give us many useful hooks and components for building a Solid application, but it can't be used unless we wrap the application in a provider. Because we're building a React application in the web browser, we'll wrap the application using the `BrowserSolidLdoProvider`. - -**App.tsx** -```tsx -// ... -import { BrowserSolidLdoProvider } from '@ldo/solid-react'; - -export const App: FunctionComponent = () => { - return ( -
- -
- - -
- ); -} -``` - -## 4. Implementing Login/Logout in the header - -Setting up login for a Solid application is easy when you're using ldo's Solid React library. With the `useSolidAuth()` hook, you can get information and methods to setup login. - -In the component below, we use the `session` object for information about the current session including `session.isLoggedIn`, a boolean indicating if the user is currently logged in, and `session.webId` to get the current user's webId. These will automatically update and rerender the component if anything changes about the current session. - -Next we use the `login(issuer: string)` method to initiate a login. Because a Solid Pod could be anywhere onthe web, we first ask the user to enter their Solid issuer then provide that to the login function. - -Finally, the `logout()` function lets you easily trigger a log out. - -**Header.tsx** -```tsx -import { useSolidAuth } from "@ldo/solid-react"; -import { FunctionComponent } from "react"; - -export const Header: FunctionComponent = () => { - const { session, login, logout } = useSolidAuth(); - - return ( -
- {session.isLoggedIn ? ( - // Is the session is logged in -

- You are logged in with the webId {session.webId}.{" "} - -

- ) : ( - // If the session is not logged in -

- You are not Logged In{" "} - -

- )} -
-
- ); -}; -``` - -Because `useSolidAuth` is a hook, you can use it anywhere in the application, even components that don't contain buttons for "login" and "logout." For example, we could use the `session` object in `Blog.tsx` to display a message if the user is not logged in. - -```tsx -import { FunctionComponent } from "react"; -import { MakePost } from "./MakePost"; -import { Post } from "./Post"; -import { useSolidAuth } from "@ldo/solid-react"; - -export const Blog: FunctionComponent = () => { - const { session } = useSolidAuth(); - if (!session.isLoggedIn) return

No blog available. Log in first.

; - - return ( -
- // .. -
- ); -}; - -``` - -Once you've implemented these changes, the application should look like this when logged out: - -![Your site when logged out after completing step 4.](./images/Step4Logout.png) - -And this when logged in: - -![Your site when logged in after completing step 4.](./images/Step4Login.png) - -## 5. Setting up a shape -In step 6, we're going to use information from a user's Solid WebId profile. But, before we can do that, we want to set up a shape for the Solid Profile. - -LDO uses ShEx "Shapes" as schemas to describe how data looks in an application. We can get started by using the `init` command line tool to get the project ready to use shapes. - -```bash -npx @ldo/cli init -``` - -This command will install required libraries and creates two folders: the `.shapes` folder and the `.ldo` folder. - -If you look in the `.shapes` folder, you'll find a default file called `foafProfile.shex`. This is a ShEx shape that defines a very simplified profile object. - -If you look in the `.ldo` folder, you'll files generated from the shape. For example, `foafProfile.typings.ts` contains the Typescript typings associated with the shape, `foafProfile.context.ts` conatians a JSON-LD context for the shape, and `foafProfile.shapeTypes.ts` contains a shape type, a special object that groups all the information for a shape together. We'll be using ShapeTypes later in this tutorial. - -For our project, we want to use a Solid Profile, so let's delete the "foafProfile" ShEx shape and make a new file for our Solid profile. - -```bash -rm ./src/.shapes/foafProfile.shex -touch ./src/.shapes/solidProfile.shex -``` - -Now, let's create a shape for the Solid Profile. The code for a Solid profile is listed below, but you can learn more about creating ShEx shapes of your own on the [ShEx website](https://shex.io) - -```shex -PREFIX srs: -PREFIX foaf: -PREFIX rdfs: -PREFIX schem: -PREFIX vcard: -PREFIX xsd: -PREFIX acl: -PREFIX cert: -PREFIX ldp: -PREFIX sp: -PREFIX solid: - -srs:SolidProfileShape EXTRA a { - a [ schem:Person ] - // rdfs:comment "Defines the node as a Person (from Schema.org)" ; - a [ foaf:Person ] - // rdfs:comment "Defines the node as a Person (from foaf)" ; - vcard:fn xsd:string ? - // rdfs:comment "The formatted name of a person. Example: John Smith" ; - foaf:name xsd:string ? - // rdfs:comment "An alternate way to define a person's name." ; - vcard:hasAddress @srs:AddressShape * - // rdfs:comment "The person's street address." ; - vcard:hasEmail @srs:EmailShape * - // rdfs:comment "The person's email." ; - vcard:hasPhoto IRI ? - // rdfs:comment "A link to the person's photo" ; - foaf:img xsd:string ? - // rdfs:comment "Photo link but in string form" ; - vcard:hasTelephone @srs:PhoneNumberShape * - // rdfs:comment "Person's telephone number" ; - vcard:phone xsd:string ? - // rdfs:comment "An alternative way to define a person's telephone number using a string" ; - vcard:organization-name xsd:string ? - // rdfs:comment "The name of the organization with which the person is affiliated" ; - vcard:role xsd:string ? - // rdfs:comment "The name of the person's role in their organization" ; - acl:trustedApp @srs:TrustedAppShape * - // rdfs:comment "A list of app origins that are trusted by this user" ; - cert:key @srs:RSAPublicKeyShape * - // rdfs:comment "A list of RSA public keys that are associated with private keys the user holds." ; - ldp:inbox IRI - // rdfs:comment "The user's LDP inbox to which apps can post notifications" ; - sp:preferencesFile IRI ? - // rdfs:comment "The user's preferences" ; - sp:storage IRI * - // rdfs:comment "The location of a Solid storage server related to this WebId" ; - solid:account IRI ? - // rdfs:comment "The user's account" ; - solid:privateTypeIndex IRI * - // rdfs:comment "A registry of all types used on the user's Pod (for private access only)" ; - solid:publicTypeIndex IRI * - // rdfs:comment "A registry of all types used on the user's Pod (for public access)" ; - foaf:knows IRI * - // rdfs:comment "A list of WebIds for all the people this user knows." ; -} - -srs:AddressShape { - vcard:country-name xsd:string ? - // rdfs:comment "The name of the user's country of residence" ; - vcard:locality xsd:string ? - // rdfs:comment "The name of the user's locality (City, Town etc.) of residence" ; - vcard:postal-code xsd:string ? - // rdfs:comment "The user's postal code" ; - vcard:region xsd:string ? - // rdfs:comment "The name of the user's region (State, Province etc.) of residence" ; - vcard:street-address xsd:string ? - // rdfs:comment "The user's street address" ; -} - -srs:EmailShape EXTRA a { - a [ - vcard:Dom - vcard:Home - vcard:ISDN - vcard:Internet - vcard:Intl - vcard:Label - vcard:Parcel - vcard:Postal - vcard:Pref - vcard:Work - vcard:X400 - ] ? - // rdfs:comment "The type of email." ; - vcard:value IRI - // rdfs:comment "The value of an email as a mailto link (Example )" ; -} - -srs:PhoneNumberShape EXTRA a { - a [ - vcard:Dom - vcard:Home - vcard:ISDN - vcard:Internet - vcard:Intl - vcard:Label - vcard:Parcel - vcard:Postal - vcard:Pref - vcard:Work - vcard:X400 - ] ? - // rdfs:comment "They type of Phone Number" ; - vcard:value IRI - // rdfs:comment "The value of a phone number as a tel link (Example )" ; -} - -srs:TrustedAppShape { - acl:mode [acl:Append acl:Control acl:Read acl:Write] + - // rdfs:comment "The level of access provided to this origin" ; - acl:origin IRI - // rdfs:comment "The app origin the user trusts" -} - -srs:RSAPublicKeyShape { - cert:modulus xsd:string - // rdfs:comment "RSA Modulus" ; - cert:exponent xsd:integer - // rdfs:comment "RSA Exponent" ; -} -``` - -Finally, we can run the command below to build the Solid Profile shape. - -```bash -npm run build:ldo -``` - -You'll notice that the `.ldo` folder contains information about a _solid_ profile. - -## 6. Fetching and using information - -Let's go back to the header we built. Yeah it's cool, but if your profile includes a name, wouldn't it be better if it said, "You are logged in as Jackson Morgan" rather than "You are logged in with the webId https://solidweb.me/jackson3/profile/card#me?" - -Well, we can fix that by retrieving the user's profile document and using the data from it. - -We can use the `useResource` and `useSubject` hooks to do this. - -```tsx -import { FunctionComponent } from "react"; -import { useResource, useSolidAuth, useSubject } from "@ldo/solid-react"; -import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes"; - -export const Header: FunctionComponent = () => { - const { session, login, logout } = useSolidAuth(); - const webIdResource = useResource(session.webId); - const profile = useSubject(SolidProfileShapeShapeType, session.webId); - - const loggedInName = webIdResource?.isReading() - ? "LOADING..." - : profile?.fn - ? profile.fn - : session.webId; - - return ( -
- {session.isLoggedIn ? ( - // Is the session is logged in -

- You are logged as {loggedInName}.{" "} - -

- ) : ( -// ... -``` - -The `useResource(uri: string)` will load a provided URI into your application. You can use methods like `.isReading()` to get the current status of the resource. When anything updates with the resource, a rerender will be triggered on your component. - -RDF data is automatically loaded into a central dataset inside your application. To access that dataset, we can use `useSubject(uri: string)`. `useSubject` takes in a ShapeType and an uri. It returns a JSON representation of that URI given the ShapeType. In the above example, we've provided the autogenerated `SolidProfileShapeShapeType` as well as the webId. This essentially says to LDO, "The URI I've provided is a Solid Profile. Please give me JSON representing this as a Solid Profile." - -Once we have the subject, all we have to do is treat it like JSON. To get the "formalName" for a profile, just call `profile.fn`. - -## 7. Getting the main container - -Let's move on to building the blog section of our site. One of the biggest questions when building Solid applications is "Where should I save new data?" While that question may have an different answer in the future, today apps traditionally create a new folder to save new data to. - -But, to create a new folder, we want to know what the root folder of the application is. At the time of login, the only URL we know is the WebId, so we want to find the root folder for that WebId. It's not always to root domain. For example, on some pod servers a WebId follows this format `https://example.pod/myusername/profile/card#me` and the root file is `https://example.pod/myusername/`. So, how do we know which container is the real root container? Well, we can use the `getRootContainer` method. - -Let's add the following hook to **Blog.tsx** - -```tsx -// ... -import { useLdo, useResource, useSolidAuth } from "@ldo/solid-react"; -import { ConatinerUri } from "@ldo/solid"; - -export const Blog: FunctionComponent = () => { - const { session } = useSolidAuth(); - - const { getResource } = useLdo(); - const [mainContainerUri, setMainContainerUri] = useState< - ContainerUri | undefined - >(); - - useEffect(() => { - if (session.webId) { - // Get the WebId resource - const webIdResource = getResource(session.webId); - // Get the root container associated with that WebId - webIdResource.getRootContainer().then((rootContainerResult) => { - // Check if there is an error - if (rootContainerResult.isError) return; - // Get a child of the root resource called "my-solid-app/" - const mainContainer = rootContainerResult.child("my-solid-app/"); - setMainContainerUri(mainContainer.uri); - // Create the main container if it doesn't exist yet - mainContainer.createIfAbsent(); - }); - } - }, [getResource, session.webId]); - - //... -``` - -Let's walk through what's happening here. First, we can use the `useLdo` hook to get a number of useful functions for interacting with data (and we'll use more in the future). In this case, we're getting the `getResource` function. This serves roughly the same purpose as the `useResource` hook, but in function form rather than hook form. Keep in mind that resources retrieved from the `getResource` function won't trigger rerenders on update, so it's best used when you need a resource for purposes other than the render. - -Using the `getResource` function, we get a resource representing the webId. Every resource has the `getRootContainer` method which returns a promise with either the root container, or an error. Everything returned by LDO methods has the `isError` parameter, so you can easily check if it's an error. - -We'll then save the URI of the main application container so we can use it in step 8. - -Any container resource has the `child` method which gets a representation of the any child, and with that representation we can call the `createIfAbsent` method to create the create out application's main container. - -## 8. Rendering Container Children - -Before we continue, let's talk a bit about the folder structure for this application. We just got our "main folder", the folder we'll save everything to. Inside that folder, we'll put our individual blog posts. These will be folders themselves with potentially two files: a post data file (index.ttl) and some image file. Overall, our folder layout will look like this: - -``` -rootContainer/ -├─ my-solid-app/ -│ ├─ post1/ -│ │ ├─ index.ttl -│ │ ├─ some_image.png/ -│ ├─ post2/ -│ │ ├─ index.ttl -│ │ ├─ another_image.jpg/ -``` - -We've already created the `my-solid-app/` container, so let's add a bit of functionality to create the post folders. Let's modify **MakePost.tsx**. - -```tsx -export const MakePost: FunctionComponent<{ mainContainer: Container }> = ({ - mainContainer, -}) => { - const [message, setMessage] = useState(""); - const [selectedFile, setSelectedFile] = useState(); - - const onSubmit = useCallback( - async (e: FormEvent) => { - e.preventDefault(); - - // Create the container for the post - const postContainerResult = await mainContainer.createChildAndOverwrite( - `${v4()}/` - ); - // Check if there was an error - if (postContainerResult.isError) { - alert(postContainerResult.message); - return; - } - const postContainer = postContainerResult.resource; - }, - [message, selectedFile, mainContainer] - ); - //... -``` - -Firstly, note that we've added a prop to include a main container. We use this in the `onSubmit` function to call the `createChildAndOverwrite` method. We can generate a name for the sub-folder any way we want, but in this example, we used UUID to generate a random name for the folder. - -Finally, we check to see if the result is an error, and if it isn't we extract our new Post container from the result. - -Now that we have the ability to create a container, let's view it. - -We'll modify **Post.tsx** to include the uri of the post: - -```tsx -import { ContainerUri } from "@ldo/solid"; - -export const Post: FunctionComponent<{ postUri: ContainerUri }> = ({ - postUri, -}) => { - return ( -
-

ContainerURI is: {postUri}

-
- ); -}; -``` - -And, in **Blog.tsx** we'll use the `useResource` hook on the main container keep track of the status of the main container. - -```tsx -export const Blog: FunctionComponent = () => { - const { session } = useSolidAuth(); - - const { getResource } = useLdo(); - const [mainContainerUri, setMainContainerUri] = useState< - ContainerUri | undefined - >(); - const mainContainer = useResource(mainContainerUri); - - // ... - - return ( -
- -
- {mainContainer - // Get all the children of the main container - .children() - // Filter out children that aren't containers themselves - .filter((child): child is Container => child.type === "container") - // Render a "Post" for each child - .map((child) => ( - - -
-
- ))} -
- ); -}; -``` - -In the render, we can use the `children()` method on the main container to get all the child elements of our container. As discussed earlier, the only children of this container should be containers themselves, so we'll filter out all non-containers. And finally, we render the post for each child. - -Once this step is done, you should be able to press the "Post" button to create posts (or at least the container for the post. We'll make the rest of the post in future steps). It should look like this. - -![Your site when after completing step 8.](./images/Step8.png) - -## 9. Uploading unstructured data - -Pods aren't just for storing containers, of course. They can also about storing raw data like images and videos. Let's add the ability to upload an image to our application. - -```tsx - const onSubmit = useCallback( - async (e: FormEvent) => { - // ... - // Upload Image - let uploadedImage: Leaf | undefined; - if (selectedFile) { - const result = await postContainer.uploadChildAndOverwrite( - selectedFile.name as LeafUri, - selectedFile, - selectedFile.type - ); - if (result.isError) { - alert(result.message); - await postContainer.delete(); - return; - } - uploadedImage = result.resource; - } - }, - [message, selectedFile, mainContainer] - ); -``` - -We added the above section to the onSubmit function in **MakePost.tsx**. In this part of code, we use the selected file created in Step 2 as well as the post container's `uploadChildAndOverwrite` method to upload the file. This method takes in three parameters: - * The name of the file. - * The file itself (or any Blob) - * The file's mime-type - -Finally, we check if there's an error, and if there isn't, we assign the result to a variable, `uploadedImage`. We'll use this in step 10. - -After implementing this step, your application should now be able to upload photos to your Pod. - -## 10. Addeding structured data. - -Unstructured data is good, but the real lifeblood of Solid comes from its structured data. In this step, we'll create a Post document that contains the Post's text body, a link to the image, and it's time of posting. - -Before we can do that, like we did with the profile, we want to have a ShEx shape for a social media posting. Create a new file called **./.shapes.post.shex** and paste the following ShEx shape. - -**./.shapes/post.shex** -```shex -PREFIX rdf: -PREFIX rdfs: -PREFIX xsd: -PREFIX ex: -BASE - -ex:PostSh { - a [ ] ; - xsd:string? - // rdfs:label '''articleBody''' - // rdfs:comment '''The actual body of the article. ''' ; - xsd:date - // rdfs:label '''uploadDate''' - // rdfs:comment '''Date when this media object was uploaded to this site.''' ; - IRI ? - // rdfs:label '''image''' - // rdfs:comment '''A media object that encodes this CreativeWork. This property is a synonym for encoding.''' ; - IRI - // rdfs:label '''publisher''' - // rdfs:comment '''The publisher of the creative work.''' ; -} -// rdfs:label '''SocialMediaPost''' -// rdfs:comment '''A post to a social media platform, including blog posts, tweets, Facebook posts, etc.''' -``` - -Now we can build the shapes again by running: - -```bash -npm run build:ldo -``` - -With the new shape in order, let's add some code to **MakePost.tsx** to create the structured data we need. - -```tsx -import { PostShShapeType } from "./.ldo/post.shapeTypes"; - -export const MakePost: FunctionComponent<{ mainContainer: Container }> = ({ - mainContainer, -}) => { - // ... - const { createData, commitData } = useLdo(); - - const onSubmit = useCallback( - async (e: FormEvent) => { - // ... - - // Create Post - const indexResource = postContainer.child("index.ttl"); - // Create new data of type "Post" where the subject is the index - // resource's uri, and write any changes to the indexResource. - const post = createData( - PostShShapeType, - indexResource.uri, - indexResource - ); - // Set the article body - post.articleBody = message; - if (uploadedImage) { - // Link the URI to the - post.image = { "@id": uploadedImage.uri }; - } - // Say that the type is a "SocialMediaPosting" - post.type = { "@id": "SocialMediaPosting" }; - // Add an upload date - post.uploadDate = new Date().toISOString(); - // The commitData function handles sending the data to the Pod. - const result = await commitData(post); - if (result.isError) { - alert(result.message); - } - }, - [mainContainer, selectedFile, createData, message, commitData] - ); - // ... -``` - -Structured data is a little different than unstructured data. Data can potentially exist in multiple resources. That isn't the case here, but we're still going to treat index.ttl, the resource, separately from the data we put on the resource. - -When we want to create data we can use the `createData` function (which we can get through the `useLdo` hook). `createData` takes three arguments: -* The ShapeType of the data. In this case, we're asserting that this data is a "Post." -* The uri for the data's "subject." In this case, it's the same as the index.ttl resource. -* A list of resources that this data will be written to. All triples that are created when you modify this data will be saved to this list of resources. - -From there, we can just modify the data as if it were normal JSON. Note that in some cases we set a field to `{ "@id": uri }`. This means that the field should point to the given URI. - -Finally the `commitData()` method takes the modified data and syncs it with the Solid Pods. - -When all is saved, the data on the Pod should look something like this: - -```turtle - "Hello this is a post"; - ; - a ; - "2023-09-26T19:01:17.263Z"^^. - -``` - -## 11. Displaying the Post -Finally, let's bring it all together and modify **Post.tsx** to display the uploaded data. - -**Post.tsx** -```tsx -import { FunctionComponent, useCallback, useMemo } from "react"; -import { ContainerUri, LeafUri } from "@ldo/solid"; -import { useLdo, useResource, useSubject } from "@ldo/solid-react"; -import { PostShShapeType } from "./.ldo/post.shapeTypes"; - -export const Post: FunctionComponent<{ postUri: ContainerUri }> = ({ - postUri, -}) => { - const postIndexUri = `${postUri}index.ttl`; - const postResource = useResource(postIndexUri); - const post = useSubject(PostShShapeType, postIndexUri); - const { getResource } = useLdo(); - const imageResource = useResource( - post?.image?.["@id"] as LeafUri | undefined - ); - - // Convert the blob into a URL to be used in the img tag - const blobUrl = useMemo(() => { - if (imageResource && imageResource.isBinary()) { - return URL.createObjectURL(imageResource.getBlob()!); - } - return undefined; - }, [imageResource]); - - const deletePost = useCallback(async () => { - const postContainer = getResource(postUri); - await postContainer.delete(); - }, [postUri, getResource]); - - if (postResource.status.isError) { - return

postResource.status.message

; - } - - return ( -
-

{post.articleBody}

- {blobUrl && ( - {post.articleBody} - )} - -
- ); -}; -``` - -Here, we've employed a few concepts that we're already familiar with as well as a few new tricks. Of course, we're using "useResource" and "useSubject" to get data about the Post. Then we can render that information by treating it just like JSON. For example `

{post.articleBody}

`. - -Notice that we can get the URL for the image with `post.image["@id"]`, but we're not using that directly in the img tag (eg ``). Why? Many resources on a Solid Pod (these included) are not open to the public. If we put the image URL directly in the the img tag, the request to get that image would be unauthorized. Instead, we perform an authenticated fetch on the image the same way we do with anything else: using `useResource`. Once we have the resource, we can convert the resource to a blob url with `URL.createObjectURL(imageResource.getBlob())`. - -We've also added a delete button. Deleting containers and resources is just as simple as running `resource.delete()`. - -## Conclusion - -And with that, you have a fully functional Solid application. LDO's React/Solid integration keeps track of state and makes sure everything is run efficiently so you can focus on developing your application. \ No newline at end of file diff --git a/lerna.json b/lerna.json index c532954..5e58c95 100644 --- a/lerna.json +++ b/lerna.json @@ -1,4 +1,4 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.0.1-alpha.18" + "version": "0.0.1-alpha.19" } diff --git a/package-lock.json b/package-lock.json index a1b9123..4df69c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22793,7 +22793,7 @@ }, "packages/cli": { "name": "@ldo/cli", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "license": "MIT", "dependencies": { "@ldo/schema-converter-shex": "^0.0.1-alpha.17", @@ -23015,10 +23015,10 @@ }, "packages/demo-react": { "name": "@ldo/demo-react", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "dependencies": { "@inrupt/solid-client-authn-browser": "^2.0.0", - "@ldo/solid-react": "^0.0.1-alpha.18", + "@ldo/solid-react": "^0.0.1-alpha.19", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", @@ -23027,7 +23027,7 @@ }, "devDependencies": { "@craco/craco": "^7.1.0", - "@ldo/cli": "^0.0.1-alpha.18", + "@ldo/cli": "^0.0.1-alpha.19", "@types/jsonld": "^1.5.9", "@types/react": "^18.2.21", "@types/shexj": "^2.1.4", @@ -23206,11 +23206,11 @@ }, "packages/jsonld-dataset-proxy": { "name": "@ldo/jsonld-dataset-proxy", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "license": "MIT", "dependencies": { "@ldo/rdf-utils": "^0.0.1-alpha.17", - "@ldo/subscribable-dataset": "^0.0.1-alpha.18", + "@ldo/subscribable-dataset": "^0.0.1-alpha.19", "@rdfjs/data-model": "^1.2.0", "@rdfjs/dataset": "^1.1.0", "jsonld2graphobject": "^0.0.4" @@ -23286,12 +23286,12 @@ }, "packages/ldo": { "name": "@ldo/ldo", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "license": "MIT", "dependencies": { "@ldo/dataset": "^0.0.1-alpha.17", - "@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.18", - "@ldo/subscribable-dataset": "^0.0.1-alpha.18", + "@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.19", + "@ldo/subscribable-dataset": "^0.0.1-alpha.19", "@rdfjs/data-model": "^1.2.0", "buffer": "^6.0.3", "readable-stream": "^4.3.0" @@ -24570,18 +24570,18 @@ }, "packages/solid": { "name": "@ldo/solid", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "license": "MIT", "dependencies": { "@ldo/dataset": "^0.0.1-alpha.17", - "@ldo/ldo": "^0.0.1-alpha.18", + "@ldo/ldo": "^0.0.1-alpha.19", "@ldo/rdf-utils": "^0.0.1-alpha.17", "cross-fetch": "^3.1.6", "http-link-header": "^1.1.1" }, "devDependencies": { "@inrupt/solid-client-authn-core": "^1.17.1", - "@ldo/cli": "^0.0.1-alpha.18", + "@ldo/cli": "^0.0.1-alpha.19", "@rdfjs/data-model": "^1.2.0", "@rdfjs/types": "^1.0.1", "@solid/community-server": "^6.0.2", @@ -24598,15 +24598,15 @@ }, "packages/solid-react": { "name": "@ldo/solid-react", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "license": "MIT", "dependencies": { "@inrupt/solid-client": "^2.0.0", "@ldo/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", + "@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.19", + "@ldo/ldo": "^0.0.1-alpha.19", + "@ldo/solid": "^0.0.1-alpha.19", + "@ldo/subscribable-dataset": "^0.0.1-alpha.19", "@rdfjs/data-model": "^1.2.0", "cross-fetch": "^3.1.6" }, @@ -26804,7 +26804,7 @@ }, "packages/subscribable-dataset": { "name": "@ldo/subscribable-dataset", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "license": "MIT", "dependencies": { "@ldo/dataset": "^0.0.1-alpha.17", diff --git a/packages/cli/package.json b/packages/cli/package.json index f6dafb8..c200b19 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@ldo/cli", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "description": "A Command Line Interface for Linked Data Objects", "main": "./dist/index.js", "bin": { diff --git a/packages/demo-react/package.json b/packages/demo-react/package.json index 2c2756b..3eaec63 100644 --- a/packages/demo-react/package.json +++ b/packages/demo-react/package.json @@ -1,9 +1,9 @@ { "name": "@ldo/demo-react", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "dependencies": { "@inrupt/solid-client-authn-browser": "^2.0.0", - "@ldo/solid-react": "^0.0.1-alpha.18", + "@ldo/solid-react": "^0.0.1-alpha.19", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", @@ -37,7 +37,7 @@ }, "devDependencies": { "@craco/craco": "^7.1.0", - "@ldo/cli": "^0.0.1-alpha.18", + "@ldo/cli": "^0.0.1-alpha.19", "@types/jsonld": "^1.5.9", "@types/react": "^18.2.21", "@types/shexj": "^2.1.4", diff --git a/packages/jsonld-dataset-proxy/package.json b/packages/jsonld-dataset-proxy/package.json index 091cb29..5c4088e 100644 --- a/packages/jsonld-dataset-proxy/package.json +++ b/packages/jsonld-dataset-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@ldo/jsonld-dataset-proxy", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "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.18", + "@ldo/subscribable-dataset": "^0.0.1-alpha.19", "@rdfjs/data-model": "^1.2.0", "@rdfjs/dataset": "^1.1.0", "jsonld2graphobject": "^0.0.4" diff --git a/packages/jsonld-dataset-proxy/src/arrayProxy/modifyArray.ts b/packages/jsonld-dataset-proxy/src/arrayProxy/modifyArray.ts index 54cd5df..b1ae921 100644 --- a/packages/jsonld-dataset-proxy/src/arrayProxy/modifyArray.ts +++ b/packages/jsonld-dataset-proxy/src/arrayProxy/modifyArray.ts @@ -1,7 +1,10 @@ import { defaultGraph } from "@rdfjs/data-model"; import type { Quad } from "@rdfjs/types"; import type { ObjectNode } from "@ldo/rdf-utils"; -import { ProxyTransactionalDataset } from "@ldo/subscribable-dataset"; +import { + TransactionDataset, + createTransactionDatasetFactory, +} from "@ldo/subscribable-dataset"; import { createDatasetFactory } from "@ldo/dataset"; import type { ProxyContext } from "../ProxyContext"; import { addObjectToDataset } from "../util/addObjectToDataset"; @@ -31,9 +34,10 @@ export function checkArrayModification( ); } // Create a test dataset to see if the inputted data is valid - const testDataset = new ProxyTransactionalDataset( + const testDataset = new TransactionDataset( proxyContext.dataset, createDatasetFactory(), + createTransactionDatasetFactory(), ); addObjectToDataset( objectToAdd as RawObject, diff --git a/packages/ldo/package.json b/packages/ldo/package.json index e08f3c1..47823f8 100644 --- a/packages/ldo/package.json +++ b/packages/ldo/package.json @@ -1,6 +1,6 @@ { "name": "@ldo/ldo", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "description": "", "main": "dist/index.js", "scripts": { @@ -39,8 +39,8 @@ }, "dependencies": { "@ldo/dataset": "^0.0.1-alpha.17", - "@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.18", - "@ldo/subscribable-dataset": "^0.0.1-alpha.18", + "@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.19", + "@ldo/subscribable-dataset": "^0.0.1-alpha.19", "@rdfjs/data-model": "^1.2.0", "buffer": "^6.0.3", "readable-stream": "^4.3.0" diff --git a/packages/ldo/src/LdoDataset.ts b/packages/ldo/src/LdoDataset.ts index f8e73e0..bdd736e 100644 --- a/packages/ldo/src/LdoDataset.ts +++ b/packages/ldo/src/LdoDataset.ts @@ -1,9 +1,11 @@ import type { Quad } from "@rdfjs/types"; import jsonldDatasetProxy from "@ldo/jsonld-dataset-proxy"; -import { WrapperSubscribableDataset } from "@ldo/subscribable-dataset"; +import { SubscribableDataset } from "@ldo/subscribable-dataset"; import { LdoBuilder } from "./LdoBuilder"; import type { ShapeType } from "./ShapeType"; import type { LdoBase } from "./index"; +import { LdoTransactionDataset } from "./LdoTransactionDataset"; +import type { ILdoDataset } from "./types"; /** * @category Getting an LdoDataset @@ -22,7 +24,10 @@ import type { LdoBase } from "./index"; * const ldoBuilder = ldoDataset.usingType(FoafProfileShapeType); * ``` */ -export class LdoDataset extends WrapperSubscribableDataset { +export class LdoDataset + extends SubscribableDataset + implements ILdoDataset +{ /** * Creates an LdoBuilder for a given shapeType * @@ -35,4 +40,12 @@ export class LdoDataset extends WrapperSubscribableDataset { const proxyBuilder = jsonldDatasetProxy(this, shapeType.context); return new LdoBuilder(proxyBuilder, shapeType); } + + public startTransaction(): LdoTransactionDataset { + return new LdoTransactionDataset( + this, + this.datasetFactory, + this.transactionDatasetFactory, + ); + } } diff --git a/packages/ldo/src/LdoDatasetFactory.ts b/packages/ldo/src/LdoDatasetFactory.ts index 9b6e335..94343a3 100644 --- a/packages/ldo/src/LdoDatasetFactory.ts +++ b/packages/ldo/src/LdoDatasetFactory.ts @@ -1,4 +1,6 @@ -import type { DatasetFactory, Dataset, Quad } from "@rdfjs/types"; +import type { Dataset, Quad } from "@rdfjs/types"; +import type { ISubscribableDatasetFactory } from "@ldo/subscribable-dataset"; +import { SubscribableDatasetFactory } from "@ldo/subscribable-dataset"; import { LdoDataset } from "./LdoDataset"; /** @@ -9,37 +11,28 @@ import { LdoDataset } from "./LdoDataset"; * * @example * ```typescript - * import { createLdoDatasetFactory } from "ldo"; + * import { createLdoDatasetFactory } from "@ldo/ldo"; + * import { createExtendedDatasetFactory } from "@ldo/dataset"; + * import { createTransactionDatasetFactory } from "@ldo/subscribable-dataset"; * - * const datasetFactory = // some RDF/JS Dataset Factory - * const ldoDatasetFactory = new LdoDatasetFactory(datasetFactory); + * const datasetFactory = createExtendedDatasetFactory(); + * const transactionDatasetFactory = createTransactionDatasetFactroy(); + * const ldoDatasetFactory = new LdoDatasetFactory( + * datasetFactory, + * transactionDatasetFactory + * ); * const ldoDataset = ldoDatasetFactory.dataset(initialDataset); * ``` */ -export class LdoDatasetFactory implements DatasetFactory { - private datasetFactory: DatasetFactory; - - /** - * @constructor - * @param datasetFactory - A generic dataset factory this factory will wrap - */ - constructor(datasetFactory: DatasetFactory) { - this.datasetFactory = datasetFactory; - } - - /** - * Creates an LdoDataset - * @param quads - A list of quads to initialize the dataset - * @returns an LdoDataset - */ - dataset(quads?: Dataset | Quad[]): LdoDataset { +export class LdoDatasetFactory + extends SubscribableDatasetFactory + implements ISubscribableDatasetFactory +{ + dataset(quads?: Dataset | Quad[] | undefined): LdoDataset { return new LdoDataset( this.datasetFactory, - quads - ? Array.isArray(quads) - ? this.datasetFactory.dataset(quads) - : quads - : undefined, + this.transactionDatasetFactory, + this.datasetFactory.dataset(quads), ); } } diff --git a/packages/ldo/src/LdoTransactionDataset.ts b/packages/ldo/src/LdoTransactionDataset.ts new file mode 100644 index 0000000..762a068 --- /dev/null +++ b/packages/ldo/src/LdoTransactionDataset.ts @@ -0,0 +1,19 @@ +import { TransactionDataset } from "@ldo/subscribable-dataset"; +import type { Quad } from "@rdfjs/types"; +import type { ILdoDataset } from "./types"; +import { LdoBuilder } from "./LdoBuilder"; +import type { ShapeType } from "./ShapeType"; +import type { LdoBase } from "./util"; +import jsonldDatasetProxy from "@ldo/jsonld-dataset-proxy"; + +export class LdoTransactionDataset + extends TransactionDataset + implements ILdoDataset +{ + usingType( + shapeType: ShapeType, + ): LdoBuilder { + const proxyBuilder = jsonldDatasetProxy(this, shapeType.context); + return new LdoBuilder(proxyBuilder, shapeType); + } +} diff --git a/packages/ldo/src/createLdoDataset.ts b/packages/ldo/src/createLdoDataset.ts index cca5b04..9ec6037 100644 --- a/packages/ldo/src/createLdoDataset.ts +++ b/packages/ldo/src/createLdoDataset.ts @@ -1,6 +1,7 @@ import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types"; import { createDataset } from "@ldo/dataset"; import { LdoDatasetFactory } from "./LdoDatasetFactory"; +import { createTransactionDatasetFactory } from "@ldo/subscribable-dataset"; import type { LdoDataset } from "./LdoDataset"; /** @@ -22,7 +23,10 @@ export function createLdoDatasetFactory() { return createDataset(quads); }, }; - return new LdoDatasetFactory(datasetFactory); + return new LdoDatasetFactory( + datasetFactory, + createTransactionDatasetFactory(), + ); } /** diff --git a/packages/ldo/src/index.ts b/packages/ldo/src/index.ts index a612422..77f5250 100644 --- a/packages/ldo/src/index.ts +++ b/packages/ldo/src/index.ts @@ -2,6 +2,7 @@ export * from "./parseRdf"; export * from "./ShapeType"; export * from "./methods"; export * from "./LdoDataset"; +export * from "./LdoTransactionDataset"; export * from "./LdoBuilder"; export * from "./createLdoDataset"; import type { LdoBase as LdoBaseImport } from "./util"; diff --git a/packages/ldo/src/types.ts b/packages/ldo/src/types.ts new file mode 100644 index 0000000..6f751f3 --- /dev/null +++ b/packages/ldo/src/types.ts @@ -0,0 +1,9 @@ +import type { ISubscribableDataset } from "@ldo/subscribable-dataset"; +import type { LdoBuilder } from "./LdoBuilder"; +import type { ShapeType } from "./ShapeType"; +import type { LdoBase } from "./util"; +import type { Quad } from "@rdfjs/types"; + +export interface ILdoDataset extends ISubscribableDataset { + usingType(shapeType: ShapeType): LdoBuilder; +} diff --git a/packages/ldo/src/util.ts b/packages/ldo/src/util.ts index fa7ad0a..322a3b7 100644 --- a/packages/ldo/src/util.ts +++ b/packages/ldo/src/util.ts @@ -8,14 +8,15 @@ import { } from "@ldo/jsonld-dataset-proxy"; import type { AnyNode } from "@ldo/rdf-utils"; import type { - SubscribableDataset, - TransactionalDataset, + ISubscribableDataset, + ITransactionDataset, } from "@ldo/subscribable-dataset"; /** * @category Types * `LdoBase` is an interface defining that a Linked Data Object is a JavaScript Object Literal. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type LdoBase = Record; /** @@ -42,21 +43,21 @@ export function normalizeNodeNames( export function canDatasetStartTransaction( dataset: Dataset, -): dataset is SubscribableDataset { +): dataset is ISubscribableDataset { return ( - typeof (dataset as SubscribableDataset).startTransaction === "function" + typeof (dataset as ISubscribableDataset).startTransaction === "function" ); } export function isTransactionalDataset( dataset: Dataset, -): dataset is TransactionalDataset { - return typeof (dataset as TransactionalDataset).commit === "function"; +): dataset is ITransactionDataset { + return typeof (dataset as ITransactionDataset).commit === "function"; } export function getTransactionalDatasetFromLdo( ldo: LdoBase, -): [TransactionalDataset, SubjectProxy | ArrayProxy] { +): [ITransactionDataset, SubjectProxy | ArrayProxy] { const proxy = getProxyFromObject(ldo); const dataset = proxy[_getUnderlyingDataset]; if ( diff --git a/packages/ldo/test/TransactionLdoDataset.test.ts b/packages/ldo/test/TransactionLdoDataset.test.ts new file mode 100644 index 0000000..482b530 --- /dev/null +++ b/packages/ldo/test/TransactionLdoDataset.test.ts @@ -0,0 +1,21 @@ +import { createLdoDataset } from "../src/createLdoDataset"; +import { ProfileShapeType } from "./profileData"; + +describe("TransactionLdoDataset", () => { + it("Uses transactions with an LdoBuilder", () => { + const ldoDataset = createLdoDataset(); + const transaction = ldoDataset.startTransaction(); + const profile = transaction + .usingType(ProfileShapeType) + .fromSubject("https://example.com/Person1"); + profile.fn = "John Doe"; + expect(transaction.getChanges().added?.toString()).toBe( + ' "John Doe" .\n', + ); + expect(ldoDataset.toString()).toBe(""); + transaction.commit(); + expect(ldoDataset.toString()).toBe( + ' "John Doe" .\n', + ); + }); +}); diff --git a/packages/ldo/test/methods.test.ts b/packages/ldo/test/methods.test.ts index c89bc3c..a63d2fd 100644 --- a/packages/ldo/test/methods.test.ts +++ b/packages/ldo/test/methods.test.ts @@ -9,7 +9,6 @@ import { import { createDataset } from "@ldo/dataset"; import type { SolidProfileShape } from "./profileData"; import { ProfileShapeType } from "./profileData"; -import type { LdoDataset } from "../src"; import { commitTransaction, createLdoDataset, @@ -25,9 +24,10 @@ import { setLanguagePreferences, languagesOf, } from "../src"; +import type { ILdoDataset } from "../src/types"; describe("methods", () => { - let dataset: LdoDataset; + let dataset: ILdoDataset; let profile: SolidProfileShape; beforeEach(() => { dataset = createLdoDataset(); diff --git a/packages/solid-react/package.json b/packages/solid-react/package.json index c2f24e8..e3fee83 100644 --- a/packages/solid-react/package.json +++ b/packages/solid-react/package.json @@ -1,6 +1,6 @@ { "name": "@ldo/solid-react", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "description": "A React library for LDO and Solid", "main": "dist/index.js", "scripts": { @@ -38,10 +38,10 @@ "dependencies": { "@inrupt/solid-client": "^2.0.0", "@ldo/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", + "@ldo/jsonld-dataset-proxy": "^0.0.1-alpha.19", + "@ldo/ldo": "^0.0.1-alpha.19", + "@ldo/solid": "^0.0.1-alpha.19", + "@ldo/subscribable-dataset": "^0.0.1-alpha.19", "@rdfjs/data-model": "^1.2.0", "cross-fetch": "^3.1.6" }, diff --git a/packages/solid-react/src/useLdoMethods.ts b/packages/solid-react/src/useLdoMethods.ts index d6add4e..fc95869 100644 --- a/packages/solid-react/src/useLdoMethods.ts +++ b/packages/solid-react/src/useLdoMethods.ts @@ -1,6 +1,10 @@ import type { LdoBase, ShapeType } from "@ldo/ldo"; import type { SubjectNode } from "@ldo/rdf-utils"; -import type { Resource, SolidLdoDataset } from "@ldo/solid"; +import type { + Resource, + SolidLdoDataset, + SolidLdoTransactionDataset, +} from "@ldo/solid"; import { changeData, commitData } from "@ldo/solid"; export interface UseLdoMethods { @@ -21,7 +25,9 @@ export interface UseLdoMethods { resource: Resource, ...additionalResources: Resource[] ): Type; - commitData(input: LdoBase): ReturnType; + commitData( + input: LdoBase, + ): ReturnType; } export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods { diff --git a/packages/solid/package.json b/packages/solid/package.json index a7042bb..68fa0bf 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "@ldo/solid", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "description": "A library for LDO and Solid", "main": "dist/index.js", "scripts": { @@ -26,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.18", + "@ldo/cli": "^0.0.1-alpha.19", "@rdfjs/data-model": "^1.2.0", "@rdfjs/types": "^1.0.1", "@solid/community-server": "^6.0.2", @@ -41,7 +41,7 @@ }, "dependencies": { "@ldo/dataset": "^0.0.1-alpha.17", - "@ldo/ldo": "^0.0.1-alpha.18", + "@ldo/ldo": "^0.0.1-alpha.19", "@ldo/rdf-utils": "^0.0.1-alpha.17", "cross-fetch": "^3.1.6", "http-link-header": "^1.1.1" diff --git a/packages/solid/src/SolidLdoDataset.ts b/packages/solid/src/SolidLdoDataset.ts index 53c2e02..e3b6a34 100644 --- a/packages/solid/src/SolidLdoDataset.ts +++ b/packages/solid/src/SolidLdoDataset.ts @@ -1,26 +1,14 @@ 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, - UpdateResultError, -} from "./requester/requests/updateDataResource"; -import { AggregateError } from "./requester/results/error/ErrorResult"; -import { InvalidUriError } from "./requester/results/error/InvalidUriError"; -import type { AggregateSuccess } from "./requester/results/success/SuccessResult"; -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"; import type { ResourceGetterOptions } from "./ResourceStore"; import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; -import { splitChangesByGraph } from "./util/splitChangesByGraph"; import type { ContainerUri, LeafUri } from "./util/uriTypes"; -import { isContainerUri } from "./util/uriTypes"; +import { SolidLdoTransactionDataset } from "./SolidLdoTransactionDataset"; +import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset"; +import type { SubjectNode } from "@ldo/rdf-utils"; import type { Resource } from "./resource/Resource"; /** @@ -57,14 +45,16 @@ export class SolidLdoDataset extends LdoDataset { /** * @param context - SolidLdoDatasetContext * @param datasetFactory - An optional dataset factory + * @param transactionDatasetFactory - A factory for creating transaction datasets * @param initialDataset - A set of triples to initialize this dataset */ constructor( context: SolidLdoDatasetContext, datasetFactory: DatasetFactory, + transactionDatasetFactory: ITransactionDatasetFactory, initialDataset?: Dataset, ) { - super(datasetFactory, initialDataset); + super(datasetFactory, transactionDatasetFactory, initialDataset); this.context = context; } @@ -92,98 +82,13 @@ 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, - ): Promise< - | AggregateSuccess< - ResourceResult - > - | AggregateError - > { - // Optimistically add changes to the datastore - // this.bulk(changes); - const changesByGraph = splitChangesByGraph(changes); - - // Iterate through all changes by graph in - const results: [ - GraphNode, - DatasetChanges, - UpdateResult | InvalidUriError | UpdateDefaultGraphSuccess, - ][] = await Promise.all( - Array.from(changesByGraph.entries()).map( - async ([graph, datasetChanges]) => { - if (graph.termType === "DefaultGraph") { - // Undefined means that this is the default graph - this.bulk(datasetChanges); - return [ - graph, - datasetChanges, - { - type: "updateDefaultGraphSuccess", - isError: false, - } as UpdateDefaultGraphSuccess, - ]; - } - if (isContainerUri(graph.value)) { - return [ - graph, - datasetChanges, - new InvalidUriError( - graph.value, - `Container URIs are not allowed for custom data.`, - ), - ]; - } - const resource = this.getResource(graph.value as LeafUri); - return [graph, datasetChanges, await resource.update(datasetChanges)]; - }, - ), + public startTransaction(): SolidLdoTransactionDataset { + return new SolidLdoTransactionDataset( + this, + this.context, + this.datasetFactory, + this.transactionDatasetFactory, ); - - // If one has errored, return error - const errors = results.filter((result) => result[2].isError); - - if (errors.length > 0) { - return new AggregateError( - errors.map( - (result) => result[2] as UpdateResultError | InvalidUriError, - ), - ); - } - return { - isError: false, - type: "aggregateSuccess", - results: results - .map((result) => result[2]) - .filter( - (result): result is ResourceResult => - result.type === "updateSuccess" || - result.type === "updateDefaultGraphSuccess", - ), - }; } /** diff --git a/packages/solid/src/SolidLdoTransactionDataset.ts b/packages/solid/src/SolidLdoTransactionDataset.ts new file mode 100644 index 0000000..cbacbb3 --- /dev/null +++ b/packages/solid/src/SolidLdoTransactionDataset.ts @@ -0,0 +1,179 @@ +import { LdoTransactionDataset } from "@ldo/ldo"; +import type { ISolidLdoDataset } from "./types"; +import type { ResourceGetterOptions } from "./ResourceStore"; +import type { Container } from "./resource/Container"; +import type { Leaf } from "./resource/Leaf"; +import { + isContainerUri, + type ContainerUri, + type LeafUri, +} from "./util/uriTypes"; +import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; +import type { DatasetFactory, Quad } from "@rdfjs/types"; +import { + updateDatasetInBulk, + type ITransactionDatasetFactory, +} from "@ldo/subscribable-dataset"; +import type { SolidLdoDataset } from "./SolidLdoDataset"; +import type { AggregateSuccess } from "./requester/results/success/SuccessResult"; +import type { ResourceResult } from "./resource/resourceResult/ResourceResult"; +import type { + UpdateDefaultGraphSuccess, + UpdateSuccess, +} from "./requester/results/success/UpdateSuccess"; +import { AggregateError } from "./requester/results/error/ErrorResult"; +import type { + UpdateResult, + UpdateResultError, +} from "./requester/requests/updateDataResource"; +import { InvalidUriError } from "./requester/results/error/InvalidUriError"; +import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils"; +import { splitChangesByGraph } from "./util/splitChangesByGraph"; + +/** + * A SolidLdoTransactionDataset has all the functionality of a SolidLdoDataset + * and represents a transaction to the parent SolidLdoDataset. + * + * It is recommended to use the `startTransaction` method on a SolidLdoDataset + * to initialize this class + * + * @example + * ```typescript + * import { createSolidLdoDataset } from "@ldo/solid"; + * import { ProfileShapeType } from "./.ldo/profile.shapeTypes.ts" + * + * // ... + * + * const solidLdoDataset = createSolidLdoDataset(); + * + * const profileDocument = solidLdoDataset + * .getResource("https://example.com/profile"); + * await profileDocument.read(); + * + * const transaction = solidLdoDataset.startTransaction(); + * + * const profile = transaction + * .using(ProfileShapeType) + * .fromSubject("https://example.com/profile#me"); + * profile.name = "Some Name"; + * await transaction.commitToPod(); + * ``` + */ +export class SolidLdoTransactionDataset + extends LdoTransactionDataset + implements ISolidLdoDataset +{ + /** + * @internal + */ + public context: SolidLdoDatasetContext; + + /** + * @param context - SolidLdoDatasetContext + * @param datasetFactory - An optional dataset factory + * @param transactionDatasetFactory - A factory for creating transaction datasets + * @param initialDataset - A set of triples to initialize this dataset + */ + constructor( + parentDataset: SolidLdoDataset, + context: SolidLdoDatasetContext, + datasetFactory: DatasetFactory, + transactionDatasetFactory: ITransactionDatasetFactory, + ) { + super(parentDataset, datasetFactory, transactionDatasetFactory); + this.context = context; + } + + /** + * Retireves a representation (either a LeafResource or a ContainerResource) + * of a Solid Resource at the given URI. This resource represents the + * current state of the resource: whether it is currently fetched or in the + * process of fetching as well as some information about it. + * + * @param uri - the URI of the resource + * @param options - Special options for getting the resource + * + * @returns a Leaf or Container Resource + * + * @example + * ```typescript + * const profileDocument = solidLdoDataset + * .getResource("https://example.com/profile"); + * ``` + */ + getResource(uri: ContainerUri, options?: ResourceGetterOptions): Container; + getResource(uri: LeafUri, options?: ResourceGetterOptions): Leaf; + getResource(uri: string, options?: ResourceGetterOptions): Leaf | Container; + getResource(uri: string, options?: ResourceGetterOptions): Leaf | Container { + return this.context.resourceStore.get(uri, options); + } + + async commitToPod(): Promise< + | AggregateSuccess< + ResourceResult + > + | AggregateError + > { + const changes = this.getChanges(); + const changesByGraph = splitChangesByGraph(changes); + + // Iterate through all changes by graph in + const results: [ + GraphNode, + DatasetChanges, + UpdateResult | InvalidUriError | UpdateDefaultGraphSuccess, + ][] = await Promise.all( + Array.from(changesByGraph.entries()).map( + async ([graph, datasetChanges]) => { + if (graph.termType === "DefaultGraph") { + // Undefined means that this is the default graph + updateDatasetInBulk(this.parentDataset, datasetChanges); + return [ + graph, + datasetChanges, + { + type: "updateDefaultGraphSuccess", + isError: false, + } as UpdateDefaultGraphSuccess, + ]; + } + if (isContainerUri(graph.value)) { + return [ + graph, + datasetChanges, + new InvalidUriError( + graph.value, + `Container URIs are not allowed for custom data.`, + ), + ]; + } + const resource = this.getResource(graph.value as LeafUri); + const updateResult = await resource.update(datasetChanges); + return [graph, datasetChanges, updateResult]; + }, + ), + ); + + // If one has errored, return error + const errors = results.filter((result) => result[2].isError); + + if (errors.length > 0) { + return new AggregateError( + errors.map( + (result) => result[2] as UpdateResultError | InvalidUriError, + ), + ); + } + return { + isError: false, + type: "aggregateSuccess", + results: results + .map((result) => result[2]) + .filter( + (result): result is ResourceResult => + result.type === "updateSuccess" || + result.type === "updateDefaultGraphSuccess", + ), + }; + } +} diff --git a/packages/solid/src/createSolidLdoDataset.ts b/packages/solid/src/createSolidLdoDataset.ts index 3a58a84..dfafe85 100644 --- a/packages/solid/src/createSolidLdoDataset.ts +++ b/packages/solid/src/createSolidLdoDataset.ts @@ -5,6 +5,7 @@ import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; import { createDataset, createDatasetFactory } from "@ldo/dataset"; import { ResourceStore } from "./ResourceStore"; import { guaranteeFetch } from "./util/guaranteeFetch"; +import { createTransactionDatasetFactory } from "@ldo/subscribable-dataset"; /** * Options for createSolidDataset @@ -56,6 +57,7 @@ export function createSolidLdoDataset( const solidLdoDataset = new SolidLdoDataset( context, finalDatasetFactory, + createTransactionDatasetFactory(), finalDataset, ); const resourceStore = new ResourceStore(context); diff --git a/packages/solid/src/index.ts b/packages/solid/src/index.ts index 5711dbc..05605b1 100644 --- a/packages/solid/src/index.ts +++ b/packages/solid/src/index.ts @@ -1,6 +1,7 @@ export * from "./createSolidLdoDataset"; export * from "./SolidLdoDataset"; export * from "./SolidLdoDatasetContext"; +export * from "./SolidLdoTransactionDataset"; export * from "./resource/Resource"; export * from "./resource/Container"; diff --git a/packages/solid/src/methods.ts b/packages/solid/src/methods.ts index 5c4333d..b1945ba 100644 --- a/packages/solid/src/methods.ts +++ b/packages/solid/src/methods.ts @@ -1,16 +1,9 @@ -import { - startTransaction, - type LdoBase, - write, - transactionChanges, - getDataset, -} from "@ldo/ldo"; -import type { DatasetChanges } from "@ldo/rdf-utils"; +import { startTransaction, type LdoBase, write, getDataset } from "@ldo/ldo"; 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"; +import type { SolidLdoTransactionDataset } from "./SolidLdoTransactionDataset"; /** * Begins tracking changes to eventually commit. @@ -34,7 +27,7 @@ import type { SubscribableDataset } from "@ldo/subscribable-dataset"; * * const cProfile = changeData(profile, resource); * cProfile.name = "My New Name"; - * await commitData(cProfile); + * const result = await commitData(cProfile); * ``` */ export function changeData( @@ -72,19 +65,20 @@ export function changeData( * * const cProfile = changeData(profile, resource); * cProfile.name = "My New Name"; - * await commitData(cProfile); + * const result = await commitData(cProfile); * ``` */ -export function commitData( +export async function commitData( input: LdoBase, -): ReturnType { - const changes = transactionChanges(input); +): ReturnType { + const transactionDataset = getDataset(input) as SolidLdoTransactionDataset; + const result = await transactionDataset.commitToPod(); + if (result.isError) return result; // Take the LdoProxy out of commit mode. This uses hidden methods of JSONLD-DATASET-PROXY const proxy = getProxyFromObject(input); proxy[_proxyContext] = proxy[_proxyContext].duplicate({ dataset: proxy[_proxyContext].state .parentDataset as SubscribableDataset, }); - const dataset = getDataset(input) as SolidLdoDataset; - return dataset.commitChangesToPod(changes as DatasetChanges); + return result; } diff --git a/packages/solid/src/requester/requests/requestOptions.ts b/packages/solid/src/requester/requests/requestOptions.ts index 3232911..376c043 100644 --- a/packages/solid/src/requester/requests/requestOptions.ts +++ b/packages/solid/src/requester/requests/requestOptions.ts @@ -1,4 +1,4 @@ -import type { BulkEditableDataset } from "@ldo/subscribable-dataset"; +import type { IBulkEditableDataset } from "@ldo/subscribable-dataset"; import type { Quad } from "@rdfjs/types"; /** @@ -18,5 +18,5 @@ export interface DatasetRequestOptions extends BasicRequestOptions { /** * A dataset to be modified with any new information obtained from a request */ - dataset?: BulkEditableDataset; + dataset?: IBulkEditableDataset; } diff --git a/packages/solid/src/types.ts b/packages/solid/src/types.ts new file mode 100644 index 0000000..e87f63c --- /dev/null +++ b/packages/solid/src/types.ts @@ -0,0 +1,14 @@ +import type { ResourceGetterOptions } from "./ResourceStore"; +import type { Container } from "./resource/Container"; +import type { Leaf } from "./resource/Leaf"; +import type { ContainerUri, LeafUri } from "./util/uriTypes"; + +/** + * A SolidLdoDataset provides methods for getting Solid resources. + */ +export interface ISolidLdoDataset { + getResource(uri: ContainerUri, options?: ResourceGetterOptions): Container; + getResource(uri: LeafUri, options?: ResourceGetterOptions): Leaf; + getResource(uri: string, options?: ResourceGetterOptions): Leaf | Container; + getResource(uri: string, options?: ResourceGetterOptions): Leaf | Container; +} diff --git a/packages/solid/test/Integration.test.ts b/packages/solid/test/Integration.test.ts index 1b30732..b005000 100644 --- a/packages/solid/test/Integration.test.ts +++ b/packages/solid/test/Integration.test.ts @@ -21,9 +21,6 @@ import { defaultGraph, } from "@rdfjs/data-model"; import type { CreateSuccess } from "../src/requester/results/success/CreateSuccess"; -import type { DatasetChanges } from "@ldo/rdf-utils"; -import { createDataset } from "@ldo/dataset"; -import type { Quad } from "@rdfjs/types"; import type { AggregateSuccess } from "../src/requester/results/success/SuccessResult"; import type { UpdateDefaultGraphSuccess, @@ -994,28 +991,28 @@ describe("Integration", () => { * Update */ describe("updateDataResource", () => { - const changes: DatasetChanges = { - added: createDataset([ - createQuad( - namedNode("http://example.org/#green-goblin"), - namedNode("http://xmlns.com/foaf/0.1/name"), - literal("Norman Osborn"), - namedNode(SAMPLE_DATA_URI), - ), - ]), - removed: createDataset([ - createQuad( - namedNode("http://example.org/#green-goblin"), - namedNode("http://xmlns.com/foaf/0.1/name"), - literal("Green Goblin"), - namedNode(SAMPLE_DATA_URI), - ), - ]), - }; + const normanQuad = createQuad( + namedNode("http://example.org/#green-goblin"), + namedNode("http://xmlns.com/foaf/0.1/name"), + literal("Norman Osborn"), + namedNode(SAMPLE_DATA_URI), + ); + + const goblinQuad = createQuad( + namedNode("http://example.org/#green-goblin"), + namedNode("http://xmlns.com/foaf/0.1/name"), + literal("Green Goblin"), + namedNode(SAMPLE_DATA_URI), + ); it("applies changes to a Pod", async () => { const result = await testRequestLoads( - () => solidLdoDataset.commitChangesToPod(changes), + () => { + const transaction = solidLdoDataset.startTransaction(); + transaction.add(normanQuad); + transaction.delete(goblinQuad); + return transaction.commitToPod(); + }, solidLdoDataset.getResource(SAMPLE_DATA_URI), { isLoading: true, @@ -1028,41 +1025,17 @@ describe("Integration", () => { >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type === "updateSuccess").toBe(true); - expect( - solidLdoDataset.has( - createQuad( - namedNode("http://example.org/#green-goblin"), - namedNode("http://xmlns.com/foaf/0.1/name"), - literal("Norman Osborn"), - namedNode(SAMPLE_DATA_URI), - ), - ), - ).toBe(true); - expect( - solidLdoDataset.has( - createQuad( - namedNode("http://example.org/#green-goblin"), - namedNode("http://xmlns.com/foaf/0.1/name"), - literal("Green Goblin"), - namedNode(SAMPLE_DATA_URI), - ), - ), - ).toBe(false); + expect(solidLdoDataset.has(normanQuad)).toBe(true); + expect(solidLdoDataset.has(goblinQuad)).toBe(false); }); it("applies only remove changes to the Pod", async () => { - const changes: DatasetChanges = { - removed: createDataset([ - createQuad( - namedNode("http://example.org/#green-goblin"), - namedNode("http://xmlns.com/foaf/0.1/name"), - literal("Green Goblin"), - namedNode(SAMPLE_DATA_URI), - ), - ]), - }; const result = await testRequestLoads( - () => solidLdoDataset.commitChangesToPod(changes), + () => { + const transaction = solidLdoDataset.startTransaction(); + transaction.delete(goblinQuad); + return transaction.commitToPod(); + }, solidLdoDataset.getResource(SAMPLE_DATA_URI), { isLoading: true, @@ -1075,21 +1048,17 @@ describe("Integration", () => { >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type === "updateSuccess").toBe(true); - expect( - solidLdoDataset.has( - createQuad( - namedNode("http://example.org/#green-goblin"), - namedNode("http://xmlns.com/foaf/0.1/name"), - literal("Green Goblin"), - namedNode(SAMPLE_DATA_URI), - ), - ), - ).toBe(false); + expect(solidLdoDataset.has(goblinQuad)).toBe(false); }); it("handles an HTTP error", async () => { fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); - const result = await solidLdoDataset.commitChangesToPod(changes); + + const transaction = solidLdoDataset.startTransaction(); + transaction.add(normanQuad); + transaction.delete(goblinQuad); + const result = await transaction.commitToPod(); + expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< @@ -1103,7 +1072,10 @@ describe("Integration", () => { fetchMock.mockImplementationOnce(() => { throw new Error("Some Error"); }); - const result = await solidLdoDataset.commitChangesToPod(changes); + const transaction = solidLdoDataset.startTransaction(); + transaction.add(normanQuad); + transaction.delete(goblinQuad); + const result = await transaction.commitToPod(); expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< @@ -1114,17 +1086,15 @@ describe("Integration", () => { }); it("errors when trying to update a container", async () => { - const changes: DatasetChanges = { - added: createDataset([ - createQuad( - namedNode("http://example.org/#green-goblin"), - namedNode("http://xmlns.com/foaf/0.1/name"), - literal("Norman Osborn"), - namedNode(SAMPLE_CONTAINER_URI), - ), - ]), - }; - const result = await solidLdoDataset.commitChangesToPod(changes); + const badContainerQuad = createQuad( + namedNode("http://example.org/#green-goblin"), + namedNode("http://xmlns.com/foaf/0.1/name"), + literal("Norman Osborn"), + namedNode(SAMPLE_CONTAINER_URI), + ); + const transaction = solidLdoDataset.startTransaction(); + transaction.add(badContainerQuad); + const result = await transaction.commitToPod(); expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< @@ -1135,17 +1105,15 @@ describe("Integration", () => { }); it("writes to the default graph without fetching", async () => { - const changes: DatasetChanges = { - added: createDataset([ - createQuad( - namedNode("http://example.org/#green-goblin"), - namedNode("http://xmlns.com/foaf/0.1/name"), - literal("Norman Osborn"), - defaultGraph(), - ), - ]), - }; - const result = await solidLdoDataset.commitChangesToPod(changes); + const defaultGraphQuad = createQuad( + namedNode("http://example.org/#green-goblin"), + namedNode("http://xmlns.com/foaf/0.1/name"), + literal("Norman Osborn"), + defaultGraph(), + ); + const transaction = solidLdoDataset.startTransaction(); + transaction.add(defaultGraphQuad); + const result = await transaction.commitToPod(); expect(result.type).toBe("aggregateSuccess"); const aggregateSuccess = result as AggregateSuccess< ResourceSuccess @@ -1169,10 +1137,15 @@ describe("Integration", () => { it("batches data update changes", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); + const transaction1 = solidLdoDataset.startTransaction(); + transaction1.delete(goblinQuad); + const transaction2 = solidLdoDataset.startTransaction(); + transaction2.add(normanQuad); + const [, updateResult1, updateResult2] = await Promise.all([ resource.read(), - solidLdoDataset.commitChangesToPod({ removed: changes.removed }), - solidLdoDataset.commitChangesToPod({ added: changes.added }), + transaction1.commitToPod(), + transaction2.commitToPod(), ]); expect(updateResult1.type).toBe("aggregateSuccess"); expect(updateResult2.type).toBe("aggregateSuccess"); @@ -1456,10 +1429,8 @@ describe("Integration", () => { * =========================================================================== */ describe("methods", () => { - it("creates a data object for a specific subject", () => { - const resource = solidLdoDataset.getResource( - "https://example.com/resource.ttl", - ); + it("creates a data object for a specific subject", async () => { + const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const post = solidLdoDataset.createData( PostShShapeType, "https://example.com/subject", @@ -1467,29 +1438,46 @@ describe("Integration", () => { ); post.type = { "@id": "CreativeWork" }; expect(post.type["@id"]).toBe("CreativeWork"); - commitData(post); + const result = await commitData(post); + expect(result.type).toBe("aggregateSuccess"); expect( solidLdoDataset.has( createQuad( namedNode("https://example.com/subject"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), namedNode("http://schema.org/CreativeWork"), - namedNode("https://example.com/resource.ttl"), + namedNode(SAMPLE_DATA_URI), ), ), ).toBe(true); }); - it("uses changeData to start a transaction", () => { - const resource = solidLdoDataset.getResource( - "https://example.com/resource.ttl", + it("handles an error when committing data", async () => { + fetchMock.mockResolvedValueOnce( + new Response(SAMPLE_DATA_URI, { + status: 500, + }), ); + const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); + const post = solidLdoDataset.createData( + PostShShapeType, + "https://example.com/subject", + resource, + ); + post.type = { "@id": "CreativeWork" }; + expect(post.type["@id"]).toBe("CreativeWork"); + const result = await commitData(post); + expect(result.isError).toBe(true); + }); + + it("uses changeData to start a transaction", async () => { + const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); solidLdoDataset.add( createQuad( namedNode("https://example.com/subject"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), namedNode("http://schema.org/CreativeWork"), - namedNode("https://example.com/resource.ttl"), + namedNode(SAMPLE_DATA_URI), ), ); const post = solidLdoDataset @@ -1498,14 +1486,15 @@ describe("Integration", () => { const cPost = changeData(post, resource); cPost.type = { "@id": "SocialMediaPosting" }; expect(cPost.type["@id"]).toBe("SocialMediaPosting"); - commitData(cPost); + const result = await commitData(cPost); + expect(result.isError).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode("https://example.com/subject"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), namedNode("http://schema.org/SocialMediaPosting"), - namedNode("https://example.com/resource.ttl"), + namedNode(SAMPLE_DATA_URI), ), ), ).toBe(true); diff --git a/packages/subscribable-dataset/package.json b/packages/subscribable-dataset/package.json index fc2bd7b..c9145e4 100644 --- a/packages/subscribable-dataset/package.json +++ b/packages/subscribable-dataset/package.json @@ -1,6 +1,6 @@ { "name": "@ldo/subscribable-dataset", - "version": "0.0.1-alpha.18", + "version": "0.0.1-alpha.19", "description": "An RDFJS dataset implementation that can be subscribed to for updates", "main": "dist/index.js", "scripts": { diff --git a/packages/subscribable-dataset/src/WrapperSubscribableDataset.ts b/packages/subscribable-dataset/src/SubscribableDataset.ts similarity index 61% rename from packages/subscribable-dataset/src/WrapperSubscribableDataset.ts rename to packages/subscribable-dataset/src/SubscribableDataset.ts index a2d0ee5..c2f99dd 100644 --- a/packages/subscribable-dataset/src/WrapperSubscribableDataset.ts +++ b/packages/subscribable-dataset/src/SubscribableDataset.ts @@ -8,40 +8,36 @@ import type { ObjectNode, GraphNode, } from "@ldo/rdf-utils"; -import type { - Dataset, - BaseQuad, - Stream, - Term, - DatasetFactory, -} from "@rdfjs/types"; +import type { Dataset, BaseQuad, Term, DatasetFactory } from "@rdfjs/types"; import type { nodeEventListener, - SubscribableDataset, - TransactionalDataset, + ISubscribableDataset, + ITransactionDataset, + ITransactionDatasetFactory, } from "./types"; -import { ProxyTransactionalDataset } from "./ProxyTransactionalDataset"; +import { ExtendedDataset } from "@ldo/dataset"; /** * A wrapper for a dataset that allows subscriptions to be made on nodes to * be triggered whenever a quad containing that added or removed. */ -export class WrapperSubscribableDataset< - InAndOutQuad extends BaseQuad = BaseQuad, -> implements SubscribableDataset +export class SubscribableDataset + extends ExtendedDataset + implements ISubscribableDataset { /** - * The underlying dataset factory + * DatasetFactory for creating new datasets */ - private datasetFactory: DatasetFactory; + protected datasetFactory: DatasetFactory; + /** - * The underlying dataset + * The underlying event emitter */ - private dataset: Dataset; + protected eventEmitter: EventEmitter; /** - * The underlying event emitter + * The underlying dataset factory for creating transaction datasets */ - private eventEmitter: EventEmitter; + protected transactionDatasetFactory: ITransactionDatasetFactory; /** * Helps find all the events for a given listener */ @@ -54,11 +50,13 @@ export class WrapperSubscribableDataset< */ constructor( datasetFactory: DatasetFactory, + transactionDatasetFactory: ITransactionDatasetFactory, initialDataset?: Dataset, ) { - this.datasetFactory = datasetFactory; - this.dataset = initialDataset || this.datasetFactory.dataset(); + super(initialDataset || datasetFactory.dataset(), datasetFactory); + this.transactionDatasetFactory = transactionDatasetFactory; this.eventEmitter = new EventEmitter(); + this.datasetFactory = datasetFactory; } /** @@ -67,6 +65,18 @@ export class WrapperSubscribableDataset< * ================================================================== */ + /** + * A helper method that mimics what the super of addAll would be + */ + private superAddAll( + quads: Dataset | InAndOutQuad[], + ): this { + for (const quad of quads) { + super.add(quad); + } + return this; + } + /** * Imports the quads into this dataset. * This method differs from Dataset.union in that it adds all quads to the current instance, rather than combining quads and the current instance to create a new instance. @@ -76,7 +86,7 @@ export class WrapperSubscribableDataset< public addAll( quads: Dataset | InAndOutQuad[], ): this { - this.dataset.addAll(quads); + this.superAddAll(quads); this.triggerSubscriptionForQuads({ added: this.datasetFactory.dataset(quads), }); @@ -89,26 +99,17 @@ export class WrapperSubscribableDataset< */ public bulk(changed: DatasetChanges): this { if (changed.added) { - this.dataset.addAll(changed.added); + this.superAddAll(changed.added); } if (changed.removed) { changed.removed.forEach((quad) => { - this.dataset.delete(quad); + super.delete(quad); }); } this.triggerSubscriptionForQuads(changed); return this; } - /** - * Returns true if the current instance is a superset of the given dataset; differently put: if the given dataset is a subset of, is contained in the current dataset. - * Blank Nodes will be normalized. - * @param other - */ - public contains(other: Dataset): boolean { - return this.dataset.contains(other); - } - /** * This method removes the quads in the current instance that match the given arguments. The logic described in Quad Matching is applied for each quad in this dataset to select the quads which will be deleted. * @param subject @@ -123,191 +124,14 @@ export class WrapperSubscribableDataset< object?: Term, graph?: Term, ): this { - const matching = this.dataset.match(subject, predicate, object, graph); + const matching = super.match(subject, predicate, object, graph); for (const quad of matching) { - this.dataset.delete(quad); + super.delete(quad); } this.triggerSubscriptionForQuads({ removed: matching }); return this; } - /** - * Returns a new dataset that contains alls quads from the current dataset, not included in the given dataset. - * @param other - */ - public difference( - other: Dataset, - ): Dataset { - return this.dataset.difference(other); - } - - /** - * Returns true if the current instance contains the same graph structure as the given dataset. - * @param other - */ - public equals(other: Dataset): boolean { - return this.dataset.equals(other); - } - - /** - * Universal quantification method, tests whether every quad in the dataset passes the test implemented by the provided iteratee. - * This method immediately returns boolean false once a quad that does not pass the test is found. - * This method always returns boolean true on an empty dataset. - * Note: This method is aligned with Array.prototype.every() in ECMAScript-262. - * @param iteratee - */ - public every( - iteratee: (quad: InAndOutQuad, dataset: this) => boolean, - ): boolean { - return this.dataset.every((quad) => iteratee(quad, this)); - } - - /** - * Creates a new dataset with all the quads that pass the test implemented by the provided iteratee. - * Note: This method is aligned with Array.prototype.filter() in ECMAScript-262. - * @param iteratee - */ - public filter( - iteratee: (quad: InAndOutQuad, dataset: this) => boolean, - ): Dataset { - return this.dataset.filter((quad) => iteratee(quad, this)); - } - - /** - * Executes the provided iteratee once on each quad in the dataset. - * Note: This method is aligned with Array.prototype.forEach() in ECMAScript-262. - * @param iteratee - */ - public forEach(iteratee: (quad: InAndOutQuad, dataset: this) => void): void { - return this.dataset.forEach((quad) => iteratee(quad, this)); - } - - /** - * Imports all quads from the given stream into the dataset. - * The stream events end and error are wrapped in a Promise. - * @param stream - */ - public async import(stream: Stream): Promise { - await this.dataset.import(stream); - return this; - } - - /** - * Returns a new dataset containing alls quads from the current dataset that are also included in the given dataset. - * @param other - */ - // Typescript disabled because rdf-js has incorrect typings - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - public intersection( - other: Dataset, - ): Dataset { - return this.dataset.intersection(other); - } - - /** - * Returns a new dataset containing all quads returned by applying iteratee to each quad in the current dataset. - * @param iteratee - */ - public map( - iteratee: (quad: InAndOutQuad, dataset: this) => InAndOutQuad, - ): Dataset { - return this.dataset.map((quad) => iteratee(quad, this)); - } - - /** - * This method calls the iteratee on each quad of the DatasetCore. The first time the iteratee is called, the accumulator value is the initialValue or, if not given, equals to the first quad of the Dataset. The return value of the iteratee is used as accumulator value for the next calls. - * This method returns the return value of the last iteratee call. - * Note: This method is aligned with Array.prototype.reduce() in ECMAScript-262. - * @param iteratee - * @param initialValue - */ - public reduce( - iteratee: (accumulator: A, quad: InAndOutQuad, dataset: this) => A, - initialValue?: A, - ): A { - return this.dataset.reduce( - (acc, quad) => iteratee(acc, quad, this), - initialValue, - ); - } - - /** - * Existential quantification method, tests whether some quads in the dataset pass the test implemented by the provided iteratee. - * Note: This method is aligned with Array.prototype.some() in ECMAScript-262. - * @param iteratee - * @returns boolean true once a quad that passes the test is found. - */ - public some( - iteratee: (quad: InAndOutQuad, dataset: this) => boolean, - ): boolean { - return this.dataset.some((quad) => iteratee(quad, this)); - } - - /** - * Returns the set of quads within the dataset as a host language native sequence, for example an Array in ECMAScript-262. - * Note: Since a DatasetCore is an unordered set, the order of the quads within the returned sequence is arbitrary. - */ - public toArray(): InAndOutQuad[] { - return this.dataset.toArray(); - } - - /** - * Returns an N-Quads string representation of the dataset, preprocessed with RDF Dataset Normalization algorithm. - */ - public toCanonical(): string { - return this.dataset.toCanonical(); - } - - /** - * Returns a stream that contains all quads of the dataset. - */ - public toStream(): Stream { - return this.dataset.toStream(); - } - - /** - * Returns an N-Quads string representation of the dataset. - * No prior normalization is required, therefore the results for the same quads may vary depending on the Dataset implementation. - */ - public toString(): string { - return this.dataset.toString(); - } - - /** - * Returns a new Dataset that is a concatenation of this dataset and the quads given as an argument. - * @param other - */ - public union( - quads: Dataset, - ): Dataset { - return this.dataset.union(quads); - } - - /** - * This method returns a new dataset that is comprised of all quads in the current instance matching the given arguments. The logic described in Quad Matching is applied for each quad in this dataset to check if it should be included in the output dataset. - * @param subject - * @param predicate - * @param object - * @param graph - * @returns a Dataset with matching triples - */ - public match( - subject?: Term | null, - predicate?: Term | null, - object?: Term | null, - graph?: Term | null, - ): Dataset { - return this.dataset.match(subject, predicate, object, graph); - } - - /** - * A non-negative integer that specifies the number of quads in the set. - */ - public get size(): number { - return this.dataset.size; - } - /** * Adds the specified quad to the dataset. * Existing quads, as defined in Quad.equals, will be ignored. @@ -315,7 +139,7 @@ export class WrapperSubscribableDataset< * @returns the dataset instance it was called on. */ public add(quad: InAndOutQuad): this { - this.dataset.add(quad); + super.add(quad); this.triggerSubscriptionForQuads({ added: this.datasetFactory.dataset([quad]), }); @@ -328,28 +152,13 @@ export class WrapperSubscribableDataset< * @param quad */ public delete(quad: InAndOutQuad): this { - this.dataset.delete(quad); + super.delete(quad); this.triggerSubscriptionForQuads({ removed: this.datasetFactory.dataset([quad]), }); return this; } - /** - * Determines whether a dataset includes a certain quad, returning true or false as appropriate. - * @param quad - */ - public has(quad: InAndOutQuad): boolean { - return this.dataset.has(quad); - } - - /** - * Returns an iterator - */ - public [Symbol.iterator](): Iterator { - return this.dataset[Symbol.iterator](); - } - /** * ================================================================== * EVENTEMITTER METHODS @@ -609,7 +418,7 @@ export class WrapperSubscribableDataset< /** * Returns a transactional dataset that will update this dataset when its transaction is committed. */ - public startTransaction(): TransactionalDataset { - return new ProxyTransactionalDataset(this, this.datasetFactory); + public startTransaction(): ITransactionDataset { + return this.transactionDatasetFactory.transactionDataset(this); } } diff --git a/packages/subscribable-dataset/src/SubscribableDatasetFactory.ts b/packages/subscribable-dataset/src/SubscribableDatasetFactory.ts new file mode 100644 index 0000000..3fbbcfe --- /dev/null +++ b/packages/subscribable-dataset/src/SubscribableDatasetFactory.ts @@ -0,0 +1,31 @@ +import type { DatasetFactory, BaseQuad, Dataset } from "@rdfjs/types"; +import type { ITransactionDatasetFactory } from "./types"; +import { SubscribableDataset } from "./SubscribableDataset"; + +/** + * A DatasetFactory that returns a SubscribableDataset given a generic DatasetFactory. + */ +export class SubscribableDatasetFactory< + InAndOutQuad extends BaseQuad = BaseQuad, +> implements DatasetFactory +{ + protected datasetFactory: DatasetFactory; + protected transactionDatasetFactory: ITransactionDatasetFactory; + constructor( + datasetFactory: DatasetFactory, + transactionDatasetFactory: ITransactionDatasetFactory, + ) { + this.datasetFactory = datasetFactory; + this.transactionDatasetFactory = transactionDatasetFactory; + } + + dataset( + quads?: Dataset | InAndOutQuad[], + ): SubscribableDataset { + return new SubscribableDataset( + this.datasetFactory, + this.transactionDatasetFactory, + quads ? this.datasetFactory.dataset(quads) : undefined, + ); + } +} diff --git a/packages/subscribable-dataset/src/ProxyTransactionalDataset.ts b/packages/subscribable-dataset/src/TransactionDataset.ts similarity index 84% rename from packages/subscribable-dataset/src/ProxyTransactionalDataset.ts rename to packages/subscribable-dataset/src/TransactionDataset.ts index 6e7a66d..9e8ef3c 100644 --- a/packages/subscribable-dataset/src/ProxyTransactionalDataset.ts +++ b/packages/subscribable-dataset/src/TransactionDataset.ts @@ -1,27 +1,23 @@ import type { Dataset, BaseQuad, Term, DatasetFactory } from "@rdfjs/types"; import type { DatasetChanges } from "@ldo/rdf-utils"; -import type { BulkEditableDataset, TransactionalDataset } from "./types"; -import { ExtendedDataset } from "@ldo/dataset"; +import type { ITransactionDataset, ITransactionDatasetFactory } from "./types"; import { mergeDatasetChanges } from "./mergeDatasetChanges"; +import { SubscribableDataset } from "./SubscribableDataset"; +import { updateDatasetInBulk } from "./util"; /** * Proxy Transactional Dataset is a transactional dataset that does not duplicate * the parent dataset, it will dynamically determine the correct return value for * methods in real time when the method is called. */ -export class ProxyTransactionalDataset - extends ExtendedDataset - implements TransactionalDataset +export class TransactionDataset + extends SubscribableDataset + implements ITransactionDataset { /** * The parent dataset that will be updated upon commit */ - private parentDataset: Dataset; - - /** - * A factory for creating new datasets to be added to the update method - */ - private datasetFactory: DatasetFactory; + public readonly parentDataset: Dataset; /** * The changes made that are ready to commit @@ -44,10 +40,10 @@ export class ProxyTransactionalDataset constructor( parentDataset: Dataset, datasetFactory: DatasetFactory, + transactionDatasetFactory: ITransactionDatasetFactory, ) { - super(datasetFactory.dataset(), datasetFactory); + super(datasetFactory, transactionDatasetFactory, datasetFactory.dataset()); this.parentDataset = parentDataset; - this.datasetFactory = datasetFactory; this.datasetChanges = {}; } @@ -251,21 +247,7 @@ export class ProxyTransactionalDataset * Helper method to update the parent dataset or any other provided dataset */ private updateParentDataset(datasetChanges: DatasetChanges) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((this.parentDataset as any).bulk) { - (this.parentDataset as BulkEditableDataset).bulk( - datasetChanges, - ); - } else { - if (datasetChanges.added) { - this.parentDataset.addAll(datasetChanges.added); - } - if (datasetChanges.removed) { - datasetChanges.removed.forEach((curQuad) => { - this.parentDataset.delete(curQuad); - }); - } - } + return updateDatasetInBulk(this.parentDataset, datasetChanges); } /** @@ -296,17 +278,6 @@ export class ProxyTransactionalDataset this.committedDatasetChanges = undefined; } - /** - * Starts a new transaction with this transactional dataset as the parent - * @returns - */ - public startTransaction(): TransactionalDataset { - // This is caused by the typings being incorrect for the intersect method - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new ProxyTransactionalDataset(this, this.datasetFactory); - } - public getChanges(): DatasetChanges { return this.datasetChanges; } diff --git a/packages/subscribable-dataset/src/TransactionDatasetFactory.ts b/packages/subscribable-dataset/src/TransactionDatasetFactory.ts new file mode 100644 index 0000000..86ebfac --- /dev/null +++ b/packages/subscribable-dataset/src/TransactionDatasetFactory.ts @@ -0,0 +1,27 @@ +import type { BaseQuad, DatasetFactory } from "@rdfjs/types"; +import type { ISubscribableDataset, ITransactionDatasetFactory } from "./types"; +import { TransactionDataset } from "./TransactionDataset"; + +export class TransactionDatasetFactory + implements ITransactionDatasetFactory +{ + private datasetFactory: DatasetFactory; + private transactionDatasetFactory: ITransactionDatasetFactory; + constructor( + datasetFactory: DatasetFactory, + transactionDatasetFactory?: ITransactionDatasetFactory, + ) { + this.datasetFactory = datasetFactory; + this.transactionDatasetFactory = transactionDatasetFactory || this; + } + + transactionDataset( + parentDataset: ISubscribableDataset, + ): TransactionDataset { + return new TransactionDataset( + parentDataset, + this.datasetFactory, + this.transactionDatasetFactory, + ); + } +} diff --git a/packages/subscribable-dataset/src/WrapperSubscribableDatasetFactory.ts b/packages/subscribable-dataset/src/WrapperSubscribableDatasetFactory.ts deleted file mode 100644 index b3d1757..0000000 --- a/packages/subscribable-dataset/src/WrapperSubscribableDatasetFactory.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { DatasetFactory, BaseQuad, Dataset } from "@rdfjs/types"; -import { WrapperSubscribableDataset } from "./WrapperSubscribableDataset"; - -/** - * A DatasetFactory that returns a WrapperSubscribableDataset given a generic DatasetFactory. - */ -export class WrapperSubscribableDatasetFactory< - InAndOutQuad extends BaseQuad = BaseQuad, -> implements DatasetFactory -{ - private datasetFactory: DatasetFactory; - constructor(datasetFactory: DatasetFactory) { - this.datasetFactory = datasetFactory; - } - - dataset( - quads?: Dataset | InAndOutQuad[], - ): WrapperSubscribableDataset { - // Typings are wrong - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new WrapperSubscribableDataset( - this.datasetFactory, - quads ? this.datasetFactory.dataset(quads) : undefined, - ); - } -} diff --git a/packages/subscribable-dataset/src/createSubscribableDataset.ts b/packages/subscribable-dataset/src/createSubscribableDataset.ts new file mode 100644 index 0000000..186a7e7 --- /dev/null +++ b/packages/subscribable-dataset/src/createSubscribableDataset.ts @@ -0,0 +1,46 @@ +import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types"; +import { createDataset } from "@ldo/dataset"; +import { SubscribableDatasetFactory } from "./SubscribableDatasetFactory"; +import type { + ISubscribableDataset, + ISubscribableDatasetFactory, + ITransactionDatasetFactory, +} from "./types"; +import { TransactionDatasetFactory } from "./TransactionDatasetFactory"; + +const datasetFactory: DatasetFactory = { + dataset: (quads?: Dataset | Quad[]): Dataset => { + return createDataset(quads); + }, +}; + +/** + * Creates a factory that generates TransactionDatasets + * @returns TransactionDatasetFactory + */ +export function createTransactionDatasetFactory(): ITransactionDatasetFactory { + return new TransactionDatasetFactory(datasetFactory); +} + +/** + * Creates a dataset factory that generates a SubscribableDataset + * @returns DatasetFactory for SubscribableDataset + */ +export function createSubscribableDatasetFactory(): ISubscribableDatasetFactory { + return new SubscribableDatasetFactory( + datasetFactory, + createTransactionDatasetFactory(), + ); +} + +/** + * Creates a SubscribableDataset + * @param quads: A dataset or array of Quads to initialize the dataset. + * @returns Dataset + */ +export function createSubscribableDataset( + quads?: Dataset | Quad[], +): ISubscribableDataset { + const subscribableDatasetFactory = createSubscribableDatasetFactory(); + return subscribableDatasetFactory.dataset(quads); +} diff --git a/packages/subscribable-dataset/src/createWrapperSubscribableDatasetFromSerializedInput.ts b/packages/subscribable-dataset/src/createSubscribableDatasetFromSerializedInput.ts similarity index 67% rename from packages/subscribable-dataset/src/createWrapperSubscribableDatasetFromSerializedInput.ts rename to packages/subscribable-dataset/src/createSubscribableDatasetFromSerializedInput.ts index dd3c511..bed692a 100644 --- a/packages/subscribable-dataset/src/createWrapperSubscribableDatasetFromSerializedInput.ts +++ b/packages/subscribable-dataset/src/createSubscribableDatasetFromSerializedInput.ts @@ -1,8 +1,8 @@ import type { Quad } from "@rdfjs/types"; import type { ParserOptions } from "@ldo/rdf-utils"; import { createDatasetFromSerializedInput } from "@ldo/dataset"; -import { createWrapperSubscribableDatasetFactory } from "./createWrapperSubscribableDataset"; -import type { WrapperSubscribableDataset } from "./WrapperSubscribableDataset"; +import { createSubscribableDatasetFactory } from "./createSubscribableDataset"; +import type { ISubscribableDataset } from "./types"; /** * Creates a SubscribableDataset with a string input that could be JSON-LD, Turtle, N-Triples, TriG, RDF*, or N3. @@ -18,9 +18,9 @@ import type { WrapperSubscribableDataset } from "./WrapperSubscribableDataset"; export async function createWrapperSubscribableDatasetFromSerializedInput( data: string, options?: ParserOptions, -): Promise> { - const datasetFactory = createWrapperSubscribableDatasetFactory(); - return createDatasetFromSerializedInput>( +): Promise> { + const datasetFactory = createSubscribableDatasetFactory(); + return createDatasetFromSerializedInput>( datasetFactory, data, options, diff --git a/packages/subscribable-dataset/src/createWrapperSubscribableDataset.ts b/packages/subscribable-dataset/src/createWrapperSubscribableDataset.ts deleted file mode 100644 index df7af0d..0000000 --- a/packages/subscribable-dataset/src/createWrapperSubscribableDataset.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types"; -import type { WrapperSubscribableDataset } from "./WrapperSubscribableDataset"; -import { createDataset } from "@ldo/dataset"; -import { WrapperSubscribableDatasetFactory } from "./WrapperSubscribableDatasetFactory"; - -/** - * Creates a dataset factory that generates a SubscribableDataset - * @returns DatasetFactory for SubscribableDataset - */ -export function createWrapperSubscribableDatasetFactory(): WrapperSubscribableDatasetFactory { - const datasetFactory: DatasetFactory = { - dataset: (quads?: Dataset | Quad[]): Dataset => { - return createDataset(quads); - }, - }; - return new WrapperSubscribableDatasetFactory(datasetFactory); -} - -/** - * Creates a SubscribableDataset - * @param quads: A dataset or array of Quads to initialize the dataset. - * @returns Dataset - */ -export function createWrapperSubscribableDataset( - quads?: Dataset | Quad[], -): WrapperSubscribableDataset { - const wrapperSubscribableDatasetFactory = - createWrapperSubscribableDatasetFactory(); - return wrapperSubscribableDatasetFactory.dataset(quads); -} diff --git a/packages/subscribable-dataset/src/index.ts b/packages/subscribable-dataset/src/index.ts index d4ecf2f..a48f3f1 100644 --- a/packages/subscribable-dataset/src/index.ts +++ b/packages/subscribable-dataset/src/index.ts @@ -1,10 +1,9 @@ -export { - createWrapperSubscribableDataset as createSubscribableDataset, - createWrapperSubscribableDatasetFactory as createSubscribableDatasetFactory, -} from "./createWrapperSubscribableDataset"; -export { createWrapperSubscribableDatasetFromSerializedInput as serializedToSubscribableDataset } from "./createWrapperSubscribableDatasetFromSerializedInput"; -export * from "./ProxyTransactionalDataset"; -export * from "./WrapperSubscribableDataset"; -export * from "./WrapperSubscribableDatasetFactory"; +export * from "./createSubscribableDataset"; +export { createWrapperSubscribableDatasetFromSerializedInput as serializedToSubscribableDataset } from "./createSubscribableDatasetFromSerializedInput"; +export * from "./TransactionDataset"; +export * from "./TransactionDatasetFactory"; +export * from "./SubscribableDataset"; +export * from "./SubscribableDatasetFactory"; export * from "./types"; export * from "./mergeDatasetChanges"; +export * from "./util"; diff --git a/packages/subscribable-dataset/src/types.ts b/packages/subscribable-dataset/src/types.ts index 4987642..055ed21 100644 --- a/packages/subscribable-dataset/src/types.ts +++ b/packages/subscribable-dataset/src/types.ts @@ -1,5 +1,5 @@ import type { DatasetChanges, QuadMatch } from "@ldo/rdf-utils"; -import type { Dataset, BaseQuad } from "@rdfjs/types"; +import type { Dataset, BaseQuad, DatasetFactory } from "@rdfjs/types"; /** * An event listeners for nodes @@ -11,28 +11,29 @@ export type nodeEventListener = ( /** * Adds the bulk method for add and remove */ -export interface BulkEditableDataset +export interface IBulkEditableDataset extends Dataset { bulk(changes: DatasetChanges): this; } /** - * A dataset that allows you to modify the dataset and + * Factory for creating SubscribableDatasets */ -export interface TransactionalDataset - extends BulkEditableDataset { - rollback(): void; - commit(): void; - getChanges(): DatasetChanges; -} +export type ISubscribableDatasetFactory< + InAndOutQuad extends BaseQuad = BaseQuad, +> = DatasetFactory< + InAndOutQuad, + InAndOutQuad, + ISubscribableDataset +>; /** * Dataset that allows developers to subscribe to a sepecific term and be alerted * if a quad is added or removed containing that term. It's methods follow the * EventEmitter interface except take in namedNodes as keys. */ -export interface SubscribableDataset - extends BulkEditableDataset { +export interface ISubscribableDataset + extends IBulkEditableDataset { /** * Alias for emitter.on(eventName, listener). * @param eventName @@ -140,5 +141,27 @@ export interface SubscribableDataset /** * Returns a transactional dataset that will update this dataset when its transaction is committed. */ - startTransaction(): TransactionalDataset; + startTransaction(): ITransactionDataset; +} + +/** + * Creates a TransactionDataset + */ +export interface ITransactionDatasetFactory< + InAndOutQuad extends BaseQuad = BaseQuad, +> { + transactionDataset( + parent: Dataset, + ): ITransactionDataset; +} + +/** + * A dataset that allows you to modify the dataset and + */ +export interface ITransactionDataset + extends ISubscribableDataset { + readonly parentDataset: Dataset; + rollback(): void; + commit(): void; + getChanges(): DatasetChanges; } diff --git a/packages/subscribable-dataset/src/util.ts b/packages/subscribable-dataset/src/util.ts new file mode 100644 index 0000000..cfb8667 --- /dev/null +++ b/packages/subscribable-dataset/src/util.ts @@ -0,0 +1,27 @@ +import type { DatasetChanges } from "@ldo/rdf-utils"; +import type { BaseQuad, Dataset } from "@rdfjs/types"; +import type { IBulkEditableDataset } from "./types"; + +/** + * Performs a bulk update for a dataset even if it doesn't have a bulk method. + * @param dataset - the input dataset + * @param datasetChanges - changes to be applied + */ +export function updateDatasetInBulk( + dataset: Dataset, + datasetChanges: DatasetChanges, +) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((dataset as any).bulk) { + (dataset as IBulkEditableDataset).bulk(datasetChanges); + } else { + if (datasetChanges.added) { + dataset.addAll(datasetChanges.added); + } + if (datasetChanges.removed) { + datasetChanges.removed.forEach((curQuad) => { + dataset.delete(curQuad); + }); + } + } +} diff --git a/packages/subscribable-dataset/test/WrapperSubscribableDataset.test.ts b/packages/subscribable-dataset/test/SubscribableDataset.test.ts similarity index 97% rename from packages/subscribable-dataset/test/WrapperSubscribableDataset.test.ts rename to packages/subscribable-dataset/test/SubscribableDataset.test.ts index c8e91b4..84a707d 100644 --- a/packages/subscribable-dataset/test/WrapperSubscribableDataset.test.ts +++ b/packages/subscribable-dataset/test/SubscribableDataset.test.ts @@ -1,5 +1,5 @@ -import type { SubscribableDataset } from "../src"; -import { ProxyTransactionalDataset, createSubscribableDataset } from "../src"; +import type { ISubscribableDataset } from "../src"; +import { TransactionDataset, createSubscribableDataset } from "../src"; import { createDataset } from "@ldo/dataset"; import { namedNode, @@ -11,14 +11,14 @@ import { import type { Quad, BlankNode } from "@rdfjs/types"; import testDataset from "@ldo/dataset/test/dataset.testHelper"; -describe("WrapperSubscribableDataset", () => { +describe("SubscribableDataset", () => { // Regular dataset tests testDataset({ dataset: createSubscribableDataset, }); // Subscribable Dataset tests - let subscribableDatastet: SubscribableDataset; + let subscribableDatastet: ISubscribableDataset; const tomTypeQuad = quad( namedNode("http://example.org/cartoons#Tom"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), @@ -449,8 +449,7 @@ describe("WrapperSubscribableDataset", () => { it("Returns a transaction", () => { expect( - subscribableDatastet.startTransaction() instanceof - ProxyTransactionalDataset, + subscribableDatastet.startTransaction() instanceof TransactionDataset, ).toBe(true); }); }); diff --git a/packages/subscribable-dataset/test/ProxyTransactionalDataset.test.ts b/packages/subscribable-dataset/test/TransactionalDataset.test.ts similarity index 88% rename from packages/subscribable-dataset/test/ProxyTransactionalDataset.test.ts rename to packages/subscribable-dataset/test/TransactionalDataset.test.ts index bc380be..903feef 100644 --- a/packages/subscribable-dataset/test/ProxyTransactionalDataset.test.ts +++ b/packages/subscribable-dataset/test/TransactionalDataset.test.ts @@ -5,14 +5,18 @@ import type { Quad, DatasetCore, } from "@rdfjs/types"; -import type { BulkEditableDataset } from "../src"; -import { ExtendedDatasetFactory } from "@ldo/dataset"; -import { ProxyTransactionalDataset } from "../src"; +import type { ISubscribableDataset } from "../src"; +import { ExtendedDatasetFactory, createDataset } from "@ldo/dataset"; +import { + TransactionDataset, + createSubscribableDataset, + createTransactionDatasetFactory, +} from "../src"; import datasetCoreFactory from "@rdfjs/dataset"; -describe("ProxyTransactionalDataset", () => { - let parentDataset: Dataset; - let transactionalDataset: ProxyTransactionalDataset; +describe("TransactionDataset", () => { + let parentDataset: ISubscribableDataset; + let transactionalDataset: TransactionDataset; const tomTypeQuad = quad( namedNode("http://example.org/cartoons#Tom"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), @@ -41,12 +45,13 @@ describe("ProxyTransactionalDataset", () => { const extendedDatasetFactory = new ExtendedDatasetFactory(datasetFactory); const initializeWithExtendedDatasetParent = (quads?: Quad[]) => { - parentDataset = extendedDatasetFactory.dataset( + parentDataset = createSubscribableDataset( quads || [tomTypeQuad, tomNameQuad], ); - transactionalDataset = new ProxyTransactionalDataset( + transactionalDataset = new TransactionDataset( parentDataset, extendedDatasetFactory, + createTransactionDatasetFactory(), ); }; @@ -301,14 +306,15 @@ describe("ProxyTransactionalDataset", () => { // Disable for tests // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const mockParent: BulkEditableDataset = { + const mockParent: ISubscribableDataset = { bulk: jest.fn(), has: (curQuad) => parentDataset.has(curQuad), [Symbol.iterator]: () => parentDataset[Symbol.iterator](), }; - transactionalDataset = new ProxyTransactionalDataset( + transactionalDataset = new TransactionDataset( mockParent, extendedDatasetFactory, + createTransactionDatasetFactory(), ); transactionalDataset.add(lickyNameQuad); @@ -317,10 +323,27 @@ describe("ProxyTransactionalDataset", () => { expect(mockParent.bulk).toHaveBeenCalled(); }); + it("Uses bulk update on commit when the parent dataset is not bulk updatable", () => { + // Disable for tests + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const mockParent: Dataset = createDataset([tomTypeQuad]); + transactionalDataset = new TransactionDataset( + mockParent, + extendedDatasetFactory, + createTransactionDatasetFactory(), + ); + + transactionalDataset.add(lickyNameQuad); + transactionalDataset.delete(tomTypeQuad); + transactionalDataset.commit(); + expect(mockParent.has(lickyNameQuad)).toBe(true); + expect(mockParent.has(tomTypeQuad)).toBe(false); + }); + it("Returns a transactional dataset", () => { expect( - transactionalDataset.startTransaction() instanceof - ProxyTransactionalDataset, + transactionalDataset.startTransaction() instanceof TransactionDataset, ).toBe(true); }); diff --git a/packages/subscribable-dataset/test/createWrapperSubscribableDatasetFromSerializedInput.test.ts b/packages/subscribable-dataset/test/createSubscribableDatasetFromSerializedInput.test.ts similarity index 100% rename from packages/subscribable-dataset/test/createWrapperSubscribableDatasetFromSerializedInput.test.ts rename to packages/subscribable-dataset/test/createSubscribableDatasetFromSerializedInput.test.ts diff --git a/packages/subscribable-dataset/test/index.test.ts b/packages/subscribable-dataset/test/index.test.ts index 493e1ac..dd48974 100644 --- a/packages/subscribable-dataset/test/index.test.ts +++ b/packages/subscribable-dataset/test/index.test.ts @@ -2,17 +2,19 @@ import { createSubscribableDataset, createSubscribableDatasetFactory, serializedToSubscribableDataset, - ProxyTransactionalDataset, - WrapperSubscribableDataset, - WrapperSubscribableDatasetFactory, + SubscribableDataset, + SubscribableDatasetFactory, + TransactionDataset, + TransactionDatasetFactory, } from "../src"; describe("Exports", () => { it("Has all exports", () => { expect(createSubscribableDataset); - expect(ProxyTransactionalDataset); - expect(WrapperSubscribableDataset); - expect(WrapperSubscribableDatasetFactory); + expect(SubscribableDataset); + expect(TransactionDataset); + expect(SubscribableDatasetFactory); + expect(TransactionDatasetFactory); expect(serializedToSubscribableDataset); expect(createSubscribableDatasetFactory); });