diff --git a/packages/solid/src/SolidLdoDataset.ts b/packages/solid/src/SolidLdoDataset.ts index c655f00..010f986 100644 --- a/packages/solid/src/SolidLdoDataset.ts +++ b/packages/solid/src/SolidLdoDataset.ts @@ -1,28 +1,12 @@ -import type { LdoBase, LdoTransactionDataset, ShapeType } from "@ldo/ldo"; -import { LdoDataset, startTransaction } from "@ldo/ldo"; -import type { DatasetChanges, GraphNode, SubjectNode } from "@ldo/rdf-utils"; +import { LdoDataset } from "@ldo/ldo"; 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 type { Resource } from "./resource/Resource"; import { SolidLdoTransactionDataset } from "./SolidLdoTransactionDataset"; +import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset"; /** * A SolidLdoDataset has all the functionality of an LdoDataset with the added @@ -58,14 +42,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, + datasetFactory: DatasetFactory, + transactionDatasetFactory: ITransactionDatasetFactory, initialDataset?: Dataset, ) { - super(datasetFactory, initialDataset); + super(datasetFactory, transactionDatasetFactory, initialDataset); this.context = context; } @@ -96,125 +82,9 @@ export class SolidLdoDataset extends LdoDataset { public startTransaction(): SolidLdoTransactionDataset { return new SolidLdoTransactionDataset( this, + this.context, this.datasetFactory, this.transactionDatasetFactory, ); } - - // /** - // * 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) ]; - // }, - // ), - // ); - - // // 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", - // ), - // }; - // } - - // /** - // * Shorthand for solidLdoDataset - // * .usingType(shapeType) - // * .write(...resources.map((r) => r.uri)) - // * .fromSubject(subject); - // * @param shapeType - The shapetype to represent the data - // * @param subject - A subject URI - // * @param resources - The resources changes to should written to - // */ - // createData( - // shapeType: ShapeType, - // subject: string | SubjectNode, - // resource: Resource, - // ...additionalResources: Resource[] - // ): Type { - // const resources = [resource, ...additionalResources]; - // const linkedDataObject = this.usingType(shapeType) - // .write(...resources.map((r) => r.uri)) - // .fromSubject(subject); - // startTransaction(linkedDataObject); - // return linkedDataObject; - // } } diff --git a/packages/solid/src/SolidLdoTransactionDataset.ts b/packages/solid/src/SolidLdoTransactionDataset.ts index 4e31b9c..9c2665d 100644 --- a/packages/solid/src/SolidLdoTransactionDataset.ts +++ b/packages/solid/src/SolidLdoTransactionDataset.ts @@ -1,8 +1,147 @@ 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 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"; export class SolidLdoTransactionDataset extends LdoTransactionDataset - implements ISolidLdoDataset { - + 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 + 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); + 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/methods.ts b/packages/solid/src/methods.ts index 5c4333d..ccaf5a1 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. @@ -75,16 +68,17 @@ export function changeData( * 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/test/Integration.test.ts b/packages/solid/test/Integration.test.ts index 7ee73a1..0e74e8c 100644 --- a/packages/solid/test/Integration.test.ts +++ b/packages/solid/test/Integration.test.ts @@ -20,9 +20,7 @@ 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, @@ -983,28 +981,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, @@ -1017,41 +1015,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, @@ -1064,21 +1038,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< @@ -1092,7 +1062,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< @@ -1103,17 +1076,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< @@ -1124,17 +1095,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 @@ -1158,10 +1127,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"); @@ -1445,30 +1419,6 @@ describe("Integration", () => { * =========================================================================== */ describe("methods", () => { - it("creates a data object for a specific subject", () => { - const resource = solidLdoDataset.getResource( - "https://example.com/resource.ttl", - ); - const post = solidLdoDataset.createData( - PostShShapeType, - "https://example.com/subject", - resource, - ); - post.type = { "@id": "CreativeWork" }; - expect(post.type["@id"]).toBe("CreativeWork"); - commitData(post); - 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"), - ), - ), - ).toBe(true); - }); - it("uses changeData to start a transaction", () => { const resource = solidLdoDataset.getResource( "https://example.com/resource.ttl",