Pre status refactor

main
Ailin Luca 2 years ago
parent 00b1c6ef6f
commit 8eceab9f3d
  1. 22
      package-lock.json
  2. 3
      packages/demo-react/package.json
  3. 31
      packages/demo-react/src/.ldo/post.context.ts
  4. 155
      packages/demo-react/src/.ldo/post.schema.ts
  5. 19
      packages/demo-react/src/.ldo/post.shapeTypes.ts
  6. 45
      packages/demo-react/src/.ldo/post.typings.ts
  7. 4
      packages/demo-react/src/.ldo/solidProfile.context.ts
  8. 2
      packages/demo-react/src/.ldo/solidProfile.schema.ts
  9. 4
      packages/demo-react/src/.ldo/solidProfile.shapeTypes.ts
  10. 2
      packages/demo-react/src/.ldo/solidProfile.typings.ts
  11. 23
      packages/demo-react/src/.shapes/post.shex
  12. 6
      packages/demo-react/src/Header.tsx
  13. 2
      packages/demo-react/src/dashboard/Dashboard.tsx
  14. 95
      packages/demo-react/src/dashboard/UploadButton.tsx
  15. 2
      packages/ldo/src/methods.ts
  16. 30
      packages/solid-react/src/SolidLdoProvider.tsx
  17. 3
      packages/solid-react/src/index.ts
  18. 91
      packages/solid-react/src/useLdoMethods.ts
  19. 57
      packages/solid-react/src/useSubject.ts
  20. 62
      packages/solid/src/SolidLdoDataset.ts
  21. 41
      packages/solid/src/requester/LeafRequester.ts
  22. 14
      packages/solid/src/requester/Requester.ts
  23. 12
      packages/solid/src/requester/requestResults/CommitChangesSuccess.ts
  24. 12
      packages/solid/src/requester/requestResults/DataResult.ts
  25. 1
      packages/solid/src/requester/requests/createDataResource.ts
  26. 8
      packages/solid/src/requester/requests/getAccessRules.ts
  27. 42
      packages/solid/src/requester/requests/updateDataResource.ts
  28. 51
      packages/solid/src/resource/Container.ts
  29. 14
      packages/solid/src/resource/Leaf.ts
  30. 38
      packages/solid/src/resource/Resource.ts
  31. 2
      packages/solid/src/util/uriTypes.ts
  32. 51
      packages/subscribable-dataset/src/ProxyTransactionalDataset.ts
  33. 1
      packages/subscribable-dataset/src/index.ts
  34. 53
      packages/subscribable-dataset/src/mergeDatasetChanges.ts

22
package-lock.json generated

@ -33909,9 +33909,13 @@
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
@ -35611,7 +35615,8 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"solid-authn-react-native": "^2.0.3"
"solid-authn-react-native": "^2.0.3",
"uuid": "^9.0.1"
},
"devDependencies": {
"@craco/craco": "^7.1.0",
@ -46610,7 +46615,8 @@
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"solid-authn-react-native": "^2.0.3",
"tsconfig-paths-webpack-plugin": "^4.1.0"
"tsconfig-paths-webpack-plugin": "^4.1.0",
"uuid": "*"
},
"dependencies": {
"@craco/craco": {
@ -69280,9 +69286,9 @@
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
},
"v8-compile-cache": {
"version": "2.3.0",

@ -8,7 +8,8 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"solid-authn-react-native": "^2.0.3"
"solid-authn-react-native": "^2.0.3",
"uuid": "^9.0.1"
},
"scripts": {
"start": "craco start",

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

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

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

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

@ -1,4 +1,4 @@
import type { ContextDefinition } from "jsonld";
import { ContextDefinition } from "jsonld";
/**
* =============================================================================
@ -8,7 +8,6 @@ import type { ContextDefinition } from "jsonld";
export const solidProfileContext: ContextDefinition = {
type: {
"@id": "@type",
"@container": "@set",
},
Person: "http://schema.org/Person",
Person2: "http://xmlns.com/foaf/0.1/Person",
@ -64,7 +63,6 @@ export const solidProfileContext: ContextDefinition = {
value: {
"@id": "http://www.w3.org/2006/vcard/ns#value",
"@type": "@id",
"@container": "@set",
},
hasPhoto: {
"@id": "http://www.w3.org/2006/vcard/ns#hasPhoto",

@ -1,4 +1,4 @@
import type { Schema } from "shexj";
import { Schema } from "shexj";
/**
* =============================================================================

@ -1,7 +1,7 @@
import type { ShapeType } from "@ldo/ldo";
import { ShapeType } from "@ldo/ldo";
import { solidProfileSchema } from "./solidProfile.schema";
import { solidProfileContext } from "./solidProfile.context";
import type {
import {
SolidProfileShape,
AddressShape,
EmailShape,

@ -1,4 +1,4 @@
import type { ContextDefinition } from "jsonld";
import { ContextDefinition } from "jsonld";
/**
* =============================================================================

@ -0,0 +1,23 @@
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX ex: <https://example.com/>
BASE <http://schema.org/>
ex:PostSh {
a [<SocialMediaPosting> <CreativeWork> <Thing>] ;
<articleBody> xsd:string?
// rdfs:label '''articleBody'''
// rdfs:comment '''The actual body of the article. ''' ;
<uploadDate> xsd:date
// rdfs:label '''uploadDate'''
// rdfs:comment '''Date when this media object was uploaded to this site.''' ;
<image> IRI ?
// rdfs:label '''image'''
// rdfs:comment '''A media object that encodes this CreativeWork. This property is a synonym for encoding.''' ;
<publisher> 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.'''

@ -1,7 +1,8 @@
import { useState } from "react";
import type { FunctionComponent } from "react";
import React from "react";
import { useResource, useSolidAuth } from "@ldo/solid-react";
import { useResource, useSolidAuth, useSubject } from "@ldo/solid-react";
import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes";
const DEFAULT_ISSUER = "https://solidweb.me";
@ -9,12 +10,13 @@ export const LoggedInHeader: FunctionComponent<{ webId: string }> = ({
webId,
}) => {
const webIdResource = useResource(webId);
const profile = useSubject(SolidProfileShapeShapeType, webId);
const { logout } = useSolidAuth();
return (
<>
<span>
Logged in as {webId}. Welcome{" "}
{webIdResource.isReading() ? "LOADING NAME" : "Cool Dude"}
{webIdResource.isReading() ? "LOADING NAME" : profile.fn}
</span>
<button onClick={logout}>Log Out</button>
</>

@ -16,7 +16,7 @@ export const Dashboard: FunctionComponent<BuildMainContainerChildProps> = ({
return (
<div>
<div>
<UploadButton />
<UploadButton mainContainer={mainContainer} />
</div>
<hr />
{mainContainer.children().map((child) => (

@ -1,10 +1,91 @@
import React, { useCallback } from "react";
import type { FunctionComponent } from "react";
import React, { useCallback, useState, useRef } from "react";
import type { FunctionComponent, FormEvent } from "react";
import type { Container, Leaf } from "@ldo/solid";
import { v4 } from "uuid";
import { useLdo, useSolidAuth } from "@ldo/solid-react";
import { PostShShapeType } from "../.ldo/post.shapeTypes";
export const UploadButton: FunctionComponent = () => {
const upload = useCallback(() => {
const _message = prompt("Type a message for your post");
}, []);
export const UploadButton: FunctionComponent<{ mainContainer: Container }> = ({
mainContainer,
}) => {
const [message, setMessage] = useState("");
const [selectedFile, setSelectedFile] = useState<File | undefined>();
const fileInputRef = useRef<HTMLInputElement | null>(null);
const { createData, commitData } = useLdo();
const { session } = useSolidAuth();
const onSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
return <button onClick={upload}>Make a New Post</button>;
// Create the container file
const mediaContainer = await mainContainer.createChildAndOverwrite(
`${v4()}/`,
);
if (mediaContainer.type === "error") {
alert(mediaContainer.message);
return;
}
// Upload Image
let uploadedImage: Leaf | undefined;
if (selectedFile) {
const result = await mediaContainer.uploadChildAndOverwrite(
selectedFile.name,
selectedFile,
selectedFile.type,
);
if (result.type === "error") {
alert(result.message);
await mediaContainer.delete();
return;
}
uploadedImage = result;
}
// Create Post
const indexResource = mediaContainer.child("index.ttl");
const post = createData(
PostShShapeType,
indexResource.uri,
indexResource,
);
post.articleBody = message;
if (uploadedImage) {
post.image = { "@id": uploadedImage.uri };
}
if (session.webId) {
post.publisher = { "@id": session.webId };
}
post.type = { "@id": "SocialMediaPosting" };
post.uploadDate = new Date().toISOString();
const result = await commitData(post);
if (result.type === "error") {
alert(result.message);
}
// Clear the UI after Upload
setMessage("");
setSelectedFile(undefined);
if (fileInputRef.current) fileInputRef.current.value = "";
},
[message, selectedFile, session.webId],
);
return (
<form onSubmit={onSubmit}>
<input
type="text"
placeholder="Make a Post"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<input
type="file"
accept="image/*"
ref={fileInputRef}
onChange={(e) => setSelectedFile(e.target.files?.[0])}
/>
<input type="submit" value="Post" />
</form>
);
};

@ -57,7 +57,7 @@ export function commitTransaction(ldo: LdoBase): void {
});
}
export function transactionChanges(ldo: LdoBase): DatasetChanges {
export function transactionChanges(ldo: LdoBase): DatasetChanges<Quad> {
const [dataset] = getTransactionalDatasetFromLdo(ldo);
return dataset.getChanges();
}

@ -8,25 +8,16 @@ import {
import { useSolidAuth } from "./SolidAuthContext";
import type { SolidLdoDataset } from "@ldo/solid";
import { createSolidLdoDataset } from "@ldo/solid";
import type { LdoBase, ShapeType } from "@ldo/ldo";
import type { SubjectNode } from "@ldo/rdf-utils";
import type { UseLdoMethods } from "./useLdoMethods";
import { createUseLdoMethods } from "./useLdoMethods";
export const SolidLdoReactContext =
// This will be set in the provider
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
createContext<UseLdoResult>(undefined);
createContext<UseLdoMethods>(undefined);
export interface UseLdoResult {
dataset: SolidLdoDataset;
getResource: SolidLdoDataset["getResource"];
getSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type | Error;
}
export function useLdo(): UseLdoResult {
export function useLdo(): UseLdoMethods {
return useContext(SolidLdoReactContext);
}
@ -50,17 +41,8 @@ export const SolidLdoProvider: FunctionComponent<SolidLdoProviderProps> = ({
solidLdoDataset.context.fetch = fetch;
}, [fetch]);
const value: UseLdoResult = useMemo(
() => ({
dataset: solidLdoDataset,
getResource: solidLdoDataset.getResource.bind(solidLdoDataset),
getSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type | Error {
return solidLdoDataset.usingType(shapeType).fromSubject(subject);
},
}),
const value: UseLdoMethods = useMemo(
() => createUseLdoMethods(solidLdoDataset),
[solidLdoDataset],
);

@ -3,5 +3,6 @@ export * from "./SolidAuthContext";
export { useLdo } from "./SolidLdoProvider";
// documentHooks
// hooks
export * from "./useResource";
export * from "./useSubject";

@ -0,0 +1,91 @@
import type { LdoBase, ShapeType } from "@ldo/ldo";
import { transactionChanges } from "@ldo/ldo";
import { write } from "@ldo/ldo";
import { startTransaction } from "@ldo/ldo";
import type { SubjectNode } from "@ldo/rdf-utils";
import type { Resource, SolidLdoDataset } from "@ldo/solid";
export interface UseLdoMethods {
dataset: SolidLdoDataset;
getResource: SolidLdoDataset["getResource"];
getSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type | Error;
createData<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
...resources: Resource[]
): Type;
changeData<Type extends LdoBase>(input: Type, ...resources: Resource[]): Type;
commitData(input: LdoBase): ReturnType<SolidLdoDataset["commitChangesToPod"]>;
}
export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods {
return {
dataset: dataset,
/**
* Gets a resource
*/
getResource: dataset.getResource.bind(dataset),
/**
* Returns a Linked Data Object for a subject
* @param shapeType The shape type for the data
* @param subject Subject Node
* @returns A Linked Data Object
*/
getSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type | Error {
return dataset.usingType(shapeType).fromSubject(subject);
},
/**
* Begins tracking changes to eventually commit for a new subject
* @param shapeType The shape type that defines the created data
* @param subject The RDF subject for a Linked Data Object
* @param resources Any number of resources to which this data should be written
* @returns A Linked Data Object to modify and commit
*/
createData<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
...resources: Resource[]
): Type {
const linkedDataObject = dataset
.usingType(shapeType)
.write(...resources.map((r) => r.uri))
.fromSubject(subject);
startTransaction(linkedDataObject);
return linkedDataObject;
},
/**
* Begins tracking changes to eventually commit
* @param input A linked data object to track changes on
* @param resources
*/
changeData<Type extends LdoBase>(
input: Type,
...resources: Resource[]
): Type {
// Clone the input and set a graph
const [transactionLdo] = write(...resources.map((r) => r.uri)).usingCopy(
input,
);
// Start a transaction with the input
startTransaction(transactionLdo);
// Return
return transactionLdo;
},
/**
* Commits the transaction to the global dataset, syncing all subscribing
* components and Solid Pods
*/
commitData(
input: LdoBase,
): ReturnType<SolidLdoDataset["commitChangesToPod"]> {
const changes = transactionChanges(input);
return dataset.commitChangesToPod(changes);
},
};
}

@ -0,0 +1,57 @@
import type { SubjectNode } from "@ldo/rdf-utils";
import {
ContextUtil,
JsonldDatasetProxyBuilder,
} from "@ldo/jsonld-dataset-proxy";
import type { ShapeType } from "@ldo/ldo";
import { LdoBuilder } from "@ldo/ldo";
import type { LdoBase } from "@ldo/ldo";
import { useLdo } from "./SolidLdoProvider";
import { useCallback, useEffect, useMemo, useState } from "react";
import { TrackingProxyContext } from "./util/TrackingProxyContext";
import { defaultGraph } from "@rdfjs/data-model";
export function useSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type {
const { dataset } = useLdo();
const [forceUpdateCounter, setForceUpdateCounter] = useState(0);
const forceUpdate = useCallback(
() => setForceUpdateCounter((val) => val + 1),
[],
);
// The main linked data object
const linkedDataObject = useMemo(() => {
// Remove all current subscriptions
dataset.removeListenerFromAllEvents(forceUpdate);
// Rebuild the LdoBuilder from scratch to inject TrackingProxyContext
const contextUtil = new ContextUtil(shapeType.context);
const proxyContext = new TrackingProxyContext(
{
dataset,
contextUtil,
writeGraphs: [defaultGraph()],
languageOrdering: ["none", "en", "other"],
},
forceUpdate,
);
const builder = new LdoBuilder(
new JsonldDatasetProxyBuilder(proxyContext),
shapeType,
);
return builder.fromSubject(subject);
}, [shapeType, subject, dataset, forceUpdateCounter, forceUpdate]);
useEffect(() => {
// Unregister force update listener upon unmount
return () => {
dataset.removeListenerFromAllEvents(forceUpdate);
};
}, [shapeType, subject]);
return linkedDataObject;
}

@ -1,11 +1,17 @@
import { LdoDataset } from "@ldo/ldo";
import type { Dataset, DatasetFactory } from "@rdfjs/types";
import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils";
import type { Dataset, DatasetFactory, Quad } from "@rdfjs/types";
import { CommitChangesSuccess } from "./requester/requestResults/CommitChangesSuccess";
import { InvalidUriError } from "./requester/requestResults/DataResult";
import { AggregateError } from "./requester/requestResults/ErrorResult";
import type { UpdateResultError } from "./requester/requests/updateDataResource";
import type { Container } from "./resource/Container";
import type { Leaf } from "./resource/Leaf";
import type { Resource } from "./resource/Resource";
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";
export class SolidLdoDataset extends LdoDataset {
public context: SolidLdoDatasetContext;
@ -25,4 +31,56 @@ export class SolidLdoDataset extends LdoDataset {
getResource(uri: string, options?: ResourceGetterOptions): Leaf | Container {
return this.context.resourceStore.get(uri, options);
}
async commitChangesToPod(
changes: DatasetChanges<Quad>,
): Promise<
CommitChangesSuccess | AggregateError<UpdateResultError | InvalidUriError>
> {
const changesByGraph = splitChangesByGraph(changes);
const results: [
GraphNode,
DatasetChanges<Quad>,
UpdateResultError | InvalidUriError | Leaf | { type: "defaultGraph" },
][] = 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: "defaultGraph" }];
}
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)];
},
),
);
// If one has errored, return error
const errors = results.filter((result) => result[2].type === "error");
if (errors.length > 0) {
return new AggregateError(
"",
errors.map(
(result) => result[2] as UpdateResultError | InvalidUriError,
),
);
}
return new CommitChangesSuccess(
"",
results
.map((result) => result[2])
.filter((result): result is Leaf => result.type === "leaf"),
);
}
}

@ -1,3 +1,42 @@
import type { DatasetChanges } from "@ldo/rdf-utils";
import { mergeDatasetChanges } from "@ldo/subscribable-dataset";
import type { Quad } from "@rdfjs/types";
import { Requester } from "./Requester";
import type { UpdateResult } from "./requests/updateDataResource";
import { updateDataResource } from "./requests/updateDataResource";
export class LeafRequester extends Requester {}
export const UPDATE_KEY = "update";
export class LeafRequester extends Requester {
isUpdating(): boolean {
return this.requestBatcher.isLoading(UPDATE_KEY);
}
/**
* Update the data on this resource
* @param changes
*/
async updateDataResource(
changes: DatasetChanges<Quad>,
): Promise<UpdateResult> {
const result = await this.requestBatcher.queueProcess({
name: UPDATE_KEY,
args: [
{ uri: this.uri, fetch: this.context.fetch },
changes,
this.context.solidLdoDataset,
],
perform: updateDataResource,
modifyQueue: (queue, isLoading, [, changes]) => {
if (queue[queue.length - 1].name === UPDATE_KEY) {
// Merge Changes
const originalChanges = queue[queue.length - 1].args[1];
mergeDatasetChanges(originalChanges, changes);
return true;
}
return false;
},
});
return result;
}
}

@ -1,6 +1,5 @@
import { ANY_KEY, RequestBatcher } from "../util/RequestBatcher";
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import type { DatasetChanges } from "@ldo/rdf-utils";
import type {
CreateResult,
CreateResultWithoutOverwrite,
@ -15,12 +14,10 @@ import type {
import { uploadResource } from "./requests/uploadResource";
import type { DeleteResult } from "./requests/deleteResource";
import { deleteResource } from "./requests/deleteResource";
import type { UpdateResult } from "./requests/updateDataResource";
const READ_KEY = "read";
const CREATE_KEY = "createDataResource";
const UPLOAD_KEY = "upload";
const UPDATE_KEY = "updateDataREsource";
const DELETE_KEY = "delete";
export abstract class Requester {
@ -47,9 +44,6 @@ export abstract class Requester {
isReading(): boolean {
return this.requestBatcher.isLoading(READ_KEY);
}
isUpdating(): boolean {
return this.requestBatcher.isLoading(UPDATE_KEY);
}
isDeletinng(): boolean {
return this.requestBatcher.isLoading(DELETE_KEY);
}
@ -166,14 +160,6 @@ export abstract class Requester {
return result;
}
/**
* Update the data on this resource
* @param changes
*/
updateDataResource(_changes: DatasetChanges): Promise<UpdateResult> {
throw new Error("Not Implemented");
}
/**
* Delete this resource
*/

@ -0,0 +1,12 @@
import type { Container } from "../../resource/Container";
import type { Leaf } from "../../resource/Leaf";
import { RequesterResult } from "./RequesterResult";
export class CommitChangesSuccess extends RequesterResult {
readonly type = "commitChangesSuccess" as const;
readonly affectedResources: (Leaf | Container)[];
constructor(uri: string, affectedResources: (Leaf | Container)[]) {
super(uri);
this.affectedResources = affectedResources;
}
}

@ -12,4 +12,16 @@ export class DataResult extends RequesterResult {
export class TurtleFormattingError extends ErrorResult {
errorType = "turtleFormatting" as const;
constructor(uri: string, message?: string) {
super(uri, message || `Problem parsing turtle for ${uri}`);
}
}
export class InvalidUriError extends ErrorResult {
errorType = "invalidUri" as const;
constructor(uri: string, message?: string) {
super(uri, message || `${uri} is not a valid uri.`);
}
}

@ -58,7 +58,6 @@ export async function createDataResource(
}
// Create the document
const parentUri = getParentUri(uri)!;
console.log("This is the URI", uri);
const headers: HeadersInit = {
"content-type": "text/turtle",
slug: getSlug(uri),

@ -1,14 +1,12 @@
import { universalAccess } from "@inrupt/solid-client";
import type {
AccessRuleFetchError,
AccessRuleResult,
} from "../requestResults/AccessRule";
import type { SimpleRequestParams } from "./requestParams";
export async function getAccessRules({
uri,
fetch,
}: SimpleRequestParams): Promise<AccessRuleResult | AccessRuleFetchError> {
export async function getAccessRules(
_params: SimpleRequestParams,
): Promise<AccessRuleResult | AccessRuleFetchError> {
throw new Error("Not Implemented");
// const [publicAccess, agentAccess] = await Promise.all([
// universalAccess.getPublicAccess(uri, { fetch }),

@ -1,15 +1,45 @@
import type { DatasetChanges } from "@ldo/rdf-utils";
import type { DataResult } from "../requestResults/DataResult";
import { changesToSparqlUpdate } from "@ldo/rdf-utils";
import { DataResult } from "../requestResults/DataResult";
import type { HttpErrorResultType } from "../requestResults/HttpErrorResult";
import type { UnexpectedError } from "../requestResults/ErrorResult";
import type { RequestParams } from "./requestParams";
import { HttpErrorResult } from "../requestResults/HttpErrorResult";
import { UnexpectedError } from "../requestResults/ErrorResult";
import type { SimpleRequestParams } from "./requestParams";
import type { SubscribableDataset } from "@ldo/subscribable-dataset";
import type { Quad } from "@rdfjs/types";
export type UpdateResult = DataResult | UpdateResultError;
export type UpdateResultError = HttpErrorResultType | UnexpectedError;
export async function updateDataResource(
_params: RequestParams,
_datasetChanges: DatasetChanges,
{ uri, fetch }: SimpleRequestParams,
datasetChanges: DatasetChanges<Quad>,
mainDataset: SubscribableDataset<Quad>,
): Promise<UpdateResult> {
throw new Error("Not Implemented");
try {
// Put Changes in transactional dataset
const transaction = mainDataset.startTransaction();
transaction.addAll(datasetChanges.added || []);
datasetChanges.removed?.forEach((quad) => transaction.delete(quad));
// Commit data optimistically
transaction.commit();
// Make request
const sparqlUpdate = await changesToSparqlUpdate(datasetChanges);
const response = await fetch(uri, {
method: "PATCH",
body: sparqlUpdate,
headers: {
"Content-Type": "application/sparql-update",
},
});
const httpError = HttpErrorResult.checkResponse(uri, response);
if (httpError) {
// Handle error rollback
transaction.rollback();
return httpError;
}
return new DataResult(uri);
} catch (err) {
return UnexpectedError.fromThrown(uri, err);
}
}

@ -11,6 +11,10 @@ import type {
} from "../requester/requests/createDataResource";
import type { DeleteResultError } from "../requester/requests/deleteResource";
import type { ReadResultError } from "../requester/requests/readResource";
import type {
UploadResultError,
UploadResultWithoutOverwriteError,
} from "../requester/requests/uploadResource";
import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext";
import { getParentUri, ldpContains } from "../util/rdfUtils";
import type { ContainerUri, LeafUri } from "../util/uriTypes";
@ -89,6 +93,13 @@ export class Container extends Resource {
});
}
child(slug: ContainerUri): Container;
child(slug: LeafUri): Leaf;
child(slug: string): Leaf | Container;
child(slug: string): Leaf | Container {
return this.context.resourceStore.get(`${this.uri}${slug}`);
}
createChildAndOverwrite(
slug: ContainerUri,
): Promise<Container | CreateResultErrors>;
@ -97,8 +108,7 @@ export class Container extends Resource {
createChildAndOverwrite(
slug: string,
): Promise<Resource | CreateResultErrors> {
const resource = this.context.resourceStore.get(`${this.uri}${slug}`);
return resource.createAndOverwrite();
return this.child(slug).createAndOverwrite();
}
createChildIfAbsent(
@ -109,12 +119,41 @@ export class Container extends Resource {
): Promise<Leaf | CreateResultWithoutOverwriteErrors>;
createChildIfAbsent(
slug: string,
): Promise<Resource | CreateResultWithoutOverwriteErrors>;
): Promise<Container | Leaf | CreateResultWithoutOverwriteErrors>;
createChildIfAbsent(
slug: string,
): Promise<Resource | CreateResultWithoutOverwriteErrors> {
const resource = this.context.resourceStore.get(`${this.uri}${slug}`);
return resource.createIfAbsent();
): Promise<Container | Leaf | CreateResultWithoutOverwriteErrors> {
return this.child(slug).createIfAbsent();
}
async uploadChildAndOverwrite(
slug: string,
blob: Blob,
mimeType: string,
): Promise<Leaf | UploadResultError> {
const child = this.child(slug);
if (child.type === "leaf") {
return child.uploadAndOverwrite(blob, mimeType);
}
return new UnexpectedError(
child.uri,
new Error(`${slug} is not a leaf uri.`),
);
}
async uploadIfAbsent(
slug: string,
blob: Blob,
mimeType: string,
): Promise<Leaf | UploadResultWithoutOverwriteError> {
const child = this.child(slug);
if (child.type === "leaf") {
return child.uploadIfAbsent(blob, mimeType);
}
return new UnexpectedError(
child.uri,
new Error(`${slug} is not a leaf uri.`),
);
}
async clear(): Promise<

@ -1,6 +1,6 @@
import type { DatasetChanges } from "@ldo/rdf-utils";
import type { Quad } from "@rdfjs/types";
import { LeafRequester } from "../requester/LeafRequester";
import type { Requester } from "../requester/Requester";
import type { AbsentResult } from "../requester/requestResults/AbsentResult";
import type { BinaryResult } from "../requester/requestResults/BinaryResult";
import type { DataResult } from "../requester/requestResults/DataResult";
@ -20,7 +20,7 @@ import { Resource } from "./Resource";
export class Leaf extends Resource {
readonly uri: LeafUri;
protected requester: Requester;
protected requester: LeafRequester;
readonly type = "leaf" as const;
protected binaryData: { data: Blob; mimeType: string } | undefined;
@ -31,6 +31,10 @@ export class Leaf extends Resource {
this.requester = new LeafRequester(uri, context);
}
isUpdating(): boolean {
return this.requester.isUpdating();
}
protected parseResult<PossibleErrors extends ErrorResult>(
result: AbsentResult | BinaryResult | DataResult | PossibleErrors,
): this | PossibleErrors {
@ -84,8 +88,10 @@ export class Leaf extends Resource {
return this.parseResult(await this.requester.upload(blob, mimeType));
}
update(_changes: DatasetChanges): Promise<this | UpdateResultError> {
throw new Error("Method not implemented");
async update(
changes: DatasetChanges<Quad>,
): Promise<this | UpdateResultError> {
return this.parseResult(await this.requester.updateDataResource(changes));
}
// Delete Method

@ -21,6 +21,7 @@ import { getAccessRules } from "../requester/requests/getAccessRules";
import { setAccessRules } from "../requester/requests/setAccessRules";
import type TypedEmitter from "typed-emitter";
import EventEmitter from "events";
import { getParentUri } from "../util/rdfUtils";
export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
update: () => void;
@ -51,9 +52,6 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
isReading(): boolean {
return this.requester.isReading();
}
isUpdating(): boolean {
return this.requester.isUpdating();
}
isDeleting(): boolean {
return this.requester.isDeletinng();
}
@ -78,26 +76,24 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
protected parseResult<PossibleErrors extends ErrorResult>(
result: AbsentResult | BinaryResult | DataResult | PossibleErrors,
): this | PossibleErrors {
let toReturn: this | PossibleErrors;
switch (result.type) {
case "error":
toReturn = result;
break;
case "absent":
this.didInitialFetch = true;
this.absent = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias
toReturn = this;
break;
default:
this.didInitialFetch = true;
this.absent = false;
// eslint-disable-next-line @typescript-eslint/no-this-alias
toReturn = this;
break;
if (result.type === "error") {
return result;
}
if (result.type === "absent") {
this.didInitialFetch = true;
this.absent = true;
} else {
this.didInitialFetch = true;
this.absent = false;
}
this.emit("update");
return toReturn;
const parentUri = getParentUri(this.uri);
if (parentUri) {
const parentContainer = this.context.resourceStore.get(parentUri);
parentContainer.emit("update");
}
return this;
}
// Read Methods

@ -8,7 +8,7 @@ export function isContainerUri(uri: string): uri is ContainerUri {
}
export function isLeafUri(uri: string): uri is LeafUri {
return !isContainerUri;
return !isContainerUri(uri);
}
type NonPathnameEnding = "" | `?${string}` | `#${string}`;

@ -2,6 +2,7 @@ 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 { mergeDatasetChanges } from "./mergeDatasetChanges";
/**
* Proxy Transactional Dataset is a transactional dataset that does not duplicate
@ -240,48 +241,14 @@ export default class ProxyTransactionalDataset<
}): void {
this.checkIfTransactionCommitted();
// Add added
if (changes.added) {
if (this.datasetChanges.added) {
this.datasetChanges.added.addAll(changes.added);
} else {
this.datasetChanges.added = this.datasetFactory.dataset(changes.added);
}
// Delete from removed if present
const changesIntersection = this.datasetChanges.removed?.intersection(
this.datasetFactory.dataset(changes.added),
);
if (changesIntersection && changesIntersection.size > 0) {
this.datasetChanges.removed =
this.datasetChanges.removed?.difference(changesIntersection);
}
}
// Add removed
if (changes.removed) {
if (this.datasetChanges.removed) {
this.datasetChanges.removed.addAll(changes.removed);
} else {
this.datasetChanges.removed = this.datasetFactory.dataset(
changes.removed,
);
}
// Delete from added if present
const changesIntersection = this.datasetChanges.added?.intersection(
this.datasetFactory.dataset(changes.removed),
);
if (changesIntersection && changesIntersection.size > 0) {
this.datasetChanges.added =
this.datasetChanges.added?.difference(changesIntersection);
}
}
// Make undefined if size is zero
if (this.datasetChanges.added && this.datasetChanges.added.size === 0) {
this.datasetChanges.added = undefined;
}
if (this.datasetChanges.removed && this.datasetChanges.removed.size === 0) {
this.datasetChanges.removed = undefined;
}
mergeDatasetChanges(this.datasetChanges, {
added: changes.added
? this.datasetFactory.dataset(changes.added)
: undefined,
removed: changes.removed
? this.datasetFactory.dataset(changes.removed)
: undefined,
});
}
/**

@ -7,3 +7,4 @@ export { default as ProxyTransactionalDataset } from "./ProxyTransactionalDatase
export { default as WrapperSubscribableDataset } from "./WrapperSubscribableDataset";
export { default as WrapperSubscribableDatasetFactory } from "./WrapperSubscribableDatasetFactory";
export * from "./types";
export * from "./mergeDatasetChanges";

@ -0,0 +1,53 @@
import type { DatasetChanges } from "@ldo/rdf-utils";
import type { BaseQuad } from "@rdfjs/types";
/**
* Merges a new change into an original change
* @param originalChange
* @param newChange
*/
export function mergeDatasetChanges<InAndOutQuad extends BaseQuad>(
originalChange: DatasetChanges<InAndOutQuad>,
newChange: DatasetChanges<InAndOutQuad>,
): void {
// Add added
if (newChange.added) {
if (originalChange.added) {
originalChange.added.addAll(newChange.added);
} else {
originalChange.added = newChange.added;
}
// Delete from removed if present
const changesIntersection = originalChange.removed?.intersection(
newChange.added,
);
if (changesIntersection && changesIntersection.size > 0) {
originalChange.removed =
originalChange.removed?.difference(changesIntersection);
}
}
// Add removed
if (newChange.removed) {
if (originalChange.removed) {
originalChange.removed.addAll(newChange.removed);
} else {
originalChange.removed = newChange.removed;
}
// Delete from added if present
const changesIntersection = originalChange.added?.intersection(
newChange.removed,
);
if (changesIntersection && changesIntersection.size > 0) {
originalChange.added =
originalChange.added?.difference(changesIntersection);
}
}
// Make undefined if size is zero
if (originalChange.added && originalChange.added.size === 0) {
originalChange.added = undefined;
}
if (originalChange.removed && originalChange.removed.size === 0) {
originalChange.removed = undefined;
}
}
Loading…
Cancel
Save