diff --git a/.gitignore b/.gitignore index d966ac0..b963875 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ packages/*/dist .idea .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/package-lock.json b/package-lock.json index 42b2bb8..e834d41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36161,6 +36161,7 @@ "@rdfjs/types": "^1.0.1", "@solid/community-server": "^6.0.2", "@types/jest": "^29.0.3", + "dotenv": "^16.3.1", "jest-rdf": "^1.8.0", "ts-jest": "^29.0.2", "ts-node": "^10.9.1", @@ -46910,17 +46911,18 @@ "@ldo/solid": { "version": "file:packages/solid", "requires": { - "@inrupt/solid-client-authn-core": "*", + "@inrupt/solid-client-authn-core": "^1.17.1", "@ldo/cli": "^0.0.0", "@ldo/dataset": "^0.0.0", "@ldo/ldo": "^0.0.0", "@ldo/rdf-utils": "^0.0.0", "@rdfjs/data-model": "^1.2.0", "@rdfjs/types": "^1.0.1", - "@solid/community-server": "*", + "@solid/community-server": "^6.0.2", "@types/jest": "^29.0.3", "cross-fetch": "^3.1.6", - "jest-rdf": "*", + "dotenv": "*", + "jest-rdf": "^1.8.0", "ts-jest": "^29.0.2", "ts-mixer": "^6.0.3", "ts-node": "^10.9.1", diff --git a/packages/solid/jest.config.js b/packages/solid/jest.config.js index bad5f64..899d6b4 100644 --- a/packages/solid/jest.config.js +++ b/packages/solid/jest.config.js @@ -3,4 +3,5 @@ const sharedConfig = require("../../jest.config.js"); module.exports = { ...sharedConfig, rootDir: "./", + setupFiles: ["/test/setup-tests.ts"], }; diff --git a/packages/solid/package.json b/packages/solid/package.json index 7d212c5..3c5f9c6 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -30,6 +30,7 @@ "@rdfjs/types": "^1.0.1", "@solid/community-server": "^6.0.2", "@types/jest": "^29.0.3", + "dotenv": "^16.3.1", "jest-rdf": "^1.8.0", "ts-jest": "^29.0.2", "ts-node": "^10.9.1", diff --git a/packages/solid/src/SolidLdoDataset.ts b/packages/solid/src/SolidLdoDataset.ts index 16d8502..869387b 100644 --- a/packages/solid/src/SolidLdoDataset.ts +++ b/packages/solid/src/SolidLdoDataset.ts @@ -1,7 +1,6 @@ import { LdoDataset } from "@ldo/ldo"; import type { Dataset, DatasetFactory } from "@rdfjs/types"; import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; -import type { ResourceStatus } from "./resource/Resource"; export class SolidLdoDataset extends LdoDataset { public context: SolidLdoDatasetContext; @@ -15,7 +14,7 @@ export class SolidLdoDataset extends LdoDataset { this.context = context; } - getResourceStatus(): ResourceStatus { - throw new Error("Not Implemented"); - } + // getResourceStatus(): ResourceStatus { + // throw new Error("Not Implemented"); + // } } diff --git a/packages/solid/src/createSolidLdoDataset.ts b/packages/solid/src/createSolidLdoDataset.ts index 07eb41f..25d0712 100644 --- a/packages/solid/src/createSolidLdoDataset.ts +++ b/packages/solid/src/createSolidLdoDataset.ts @@ -1,15 +1,8 @@ import type { Dataset, DatasetFactory } from "@rdfjs/types"; import { SolidLdoDataset } from "./SolidLdoDataset"; -import { AccessRulesStore } from "./document/accessRules/AccessRulesStore"; -import { BinaryResourceStore } from "./document/resource/binaryResource/BinaryResourceStore"; -import { DataResourceStore } from "./document/resource/dataResource/DataResourceStore"; -import { ContainerResourceStore } from "./document/resource/dataResource/containerResource/ContainerResourceStore"; -import type { - DocumentEventEmitter, - SolidLdoDatasetContext, -} from "./SolidLdoDatasetContext"; + +import type { SolidLdoDatasetContext } from "./SolidLdoDatasetContext"; import crossFetch from "cross-fetch"; -import { EventEmitter } from "events"; import { createDataset, createDatasetFactory } from "@ldo/dataset"; export interface CreateSolidLdoDatasetOptions { @@ -29,22 +22,13 @@ export function createSolidLdoDataset( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const context: SolidLdoDatasetContext = { - documentEventEmitter: new EventEmitter() as DocumentEventEmitter, fetch: finalFetch, }; - const binaryResourceStore = new BinaryResourceStore(context); - const dataResourceStore = new DataResourceStore(context); - const containerResourceStore = new ContainerResourceStore(context); - const accessRulesStore = new AccessRulesStore(context); const solidLdoDataset = new SolidLdoDataset( context, finalDatasetFactory, finalDataset, ); - context.binaryResourceStore = binaryResourceStore; - context.dataResourceStore = dataResourceStore; - context.containerResourceStore = containerResourceStore; - context.accessRulesStore = accessRulesStore; context.solidLdoDataset = solidLdoDataset; return solidLdoDataset; diff --git a/packages/solid/src/requester/LeafRequester.ts b/packages/solid/src/requester/LeafRequester.ts index 492a392..f7d02db 100644 --- a/packages/solid/src/requester/LeafRequester.ts +++ b/packages/solid/src/requester/LeafRequester.ts @@ -1,11 +1,9 @@ -import type { LeafUri } from "../uriTypes"; +import type { LeafUri } from "../util/uriTypes"; import { RequestBatcher } from "../util/RequestBatcher"; import type { SolidLdoDatasetContext } from "../SolidLdoDatasetContext"; import { AbsentResult } from "./requesterResults/AbsentResult"; -import { - DataResult, - TurtleFormattingError, -} from "./requesterResults/DataResult"; +import type { TurtleFormattingError } from "./requesterResults/DataResult"; +import { DataResult } from "./requesterResults/DataResult"; import { BinaryResult } from "./requesterResults/BinaryResult"; import { HttpErrorResult, @@ -14,8 +12,18 @@ import { UnexpectedHttpError, } from "./requesterResults/HttpErrorResult"; import { UnexpectedError } from "./requesterResults/ErrorResult"; +import type { LdoDataset } from "@ldo/ldo"; import { parseRdf } from "@ldo/ldo"; -import { namedNode } from "@rdfjs/data-model"; +import { namedNode, quad as createQuad } from "@rdfjs/data-model"; +import { + addRawTurtleToDataset, + addResourceRdfToContainer, + deleteResourceRdfFromContainer, + getParentUri, + getSlug, +} from "../util/rdfUtils"; +import type { TransactionalDataset } from "@ldo/subscribable-dataset"; +import type { Quad } from "@rdfjs/types"; export type ReadResult = | AbsentResult @@ -27,6 +35,22 @@ export type ReadResult = | UnexpectedError | TurtleFormattingError; +export type CreateResult = + | DataResult + | BinaryResult + | ServerHttpError + | UnauthenticatedHttpError + | UnexpectedError + | UnexpectedHttpError; +export type CreateResultWithoutOverwrite = CreateResult | TurtleFormattingError; + +export type DeleteResult = + | AbsentResult + | ServerHttpError + | UnauthenticatedHttpError + | UnexpectedError + | UnexpectedHttpError; + export class LeafRequester { private requestBatcher = new RequestBatcher(); @@ -39,13 +63,17 @@ export class LeafRequester { this.context = context; } - // Read Methods - read(): Promise { + /** + * Read this resource. + */ + async read(): Promise { const READ_KEY = "read"; - return this.requestBatcher.queueProcess({ + const transaction = this.context.solidLdoDataset.startTransaction(); + const result = await this.requestBatcher.queueProcess({ name: READ_KEY, - args: [], - perform: this.performRead.bind(this), + args: [transaction], + perform: (transaction: TransactionalDataset) => + this.performRead(transaction), modifyQueue: (queue, isLoading) => { if (queue.length === 0) { return isLoading[READ_KEY]; @@ -54,9 +82,18 @@ export class LeafRequester { } }, }); + if (result.type !== "error") { + transaction.commit(); + } + return result; } - private async performRead(): Promise { + /** + * Helper method to perform the read action + */ + private async performRead( + transaction: TransactionalDataset, + ): Promise { try { // Fetch options to determine the document type const response = await this.context.fetch(this.uri); @@ -73,31 +110,13 @@ export class LeafRequester { return new UnexpectedHttpError(this.uri, response); } + // Add this resource to the container + addResourceRdfToContainer(this.uri, transaction); + if (DataResult.is(response)) { // Parse Turtle const rawTurtle = await response.text(); - let loadedDataset; - try { - loadedDataset = await parseRdf(rawTurtle, { - baseIRI: this.uri, - }); - } catch (err) { - return new TurtleFormattingError( - this.uri, - err instanceof Error ? err.message : "Failed to parse rdf", - ); - } - - // Start transaction - const transactionalDataset = - this.context.solidLdoDataset.startTransaction(); - const graphNode = namedNode(this.uri); - // Destroy all triples that were once a part of this resouce - loadedDataset.deleteMatches(undefined, undefined, undefined, graphNode); - // Add the triples from the fetched item - transactionalDataset.addAll(loadedDataset); - transactionalDataset.commit(); - return new DataResult(this.uri); + return addRawTurtleToDataset(rawTurtle, transaction, this.uri); } else { // Load Blob const blob = await response.blob(); @@ -108,8 +127,103 @@ export class LeafRequester { } } - // // Create Methods - // abstract createDataResource(overwrite?: boolean): Promise; + /** + * Creates a Resource + * @param overwrite: If true, this will orverwrite the resource if it already + * exists + */ + async createDataResource( + overwrite?: false, + ): Promise; + async createDataResource(overwrite: true): Promise; + async createDataResource( + overwrite?: boolean, + ): Promise; + async createDataResource( + overwrite?: boolean, + ): Promise { + const CREATE_KEY = "createDataResource"; + const transaction = this.context.solidLdoDataset.startTransaction(); + const result = await this.requestBatcher.queueProcess({ + name: CREATE_KEY, + args: [transaction, overwrite], + perform: (transaction: TransactionalDataset, overwrite?: boolean) => + this.performCreateDataResource(transaction, overwrite), + modifyQueue: (queue, isLoading, args) => { + const lastElementInQueue = queue[queue.length - 1]; + return ( + lastElementInQueue && + lastElementInQueue.name === CREATE_KEY && + !!lastElementInQueue.args[1] === !!args[1] + ); + }, + }); + if (result.type !== "error") { + transaction.commit(); + } + return result; + } + + /** + * Helper Method to perform the createDataResourceAction + * @param overwrite + */ + private async performCreateDataResource( + transaction: TransactionalDataset, + overwrite?: false, + ): Promise; + private async performCreateDataResource( + transaction: TransactionalDataset, + overwrite: true, + ): Promise; + private async performCreateDataResource( + transaction: TransactionalDataset, + overwrite?: boolean, + ): Promise; + private async performCreateDataResource( + transaction: TransactionalDataset, + overwrite?: boolean, + ): Promise { + try { + if (overwrite) { + const deleteResult = await this.performDelete(transaction); + // Return if it wasn't deleted + if (deleteResult.type !== "absent") { + return deleteResult; + } + } else { + // Perform a read to check if it exists + const readResult = await this.performRead(transaction); + // If it does exist stop and return. + if (readResult.type !== "absent") { + return readResult; + } + } + // Create the document + const parentUri = getParentUri(this.uri)!; + const response = await this.context.fetch(parentUri, { + method: "post", + headers: { + "content-type": "text/turtle", + slug: getSlug(this.uri), + }, + }); + + if (ServerHttpError.is(response)) { + return new ServerHttpError(this.uri, response); + } + if (UnauthenticatedHttpError.is(response)) { + return new UnauthenticatedHttpError(this.uri, response); + } + if (HttpErrorResult.isnt(response)) { + return new UnexpectedHttpError(this.uri, response); + } + addResourceRdfToContainer(this.uri, transaction); + return new DataResult(this.uri); + } catch (err) { + return UnexpectedError.fromThrown(this.uri, err); + } + } // abstract upload( // blob: Blob, @@ -121,5 +235,64 @@ export class LeafRequester { // changes: DatasetChanges, // ): Promise; - // abstract delete(): Promise; + /** + * Delete this resource + */ + async delete(): Promise { + const DELETE_KEY = "delete"; + const transaction = this.context.solidLdoDataset.startTransaction(); + const result = await this.requestBatcher.queueProcess({ + name: DELETE_KEY, + args: [transaction], + perform: (transaction: TransactionalDataset) => + this.performDelete(transaction), + modifyQueue: (queue, isLoading) => { + if (queue.length === 0) { + return isLoading[DELETE_KEY]; + } else { + return queue[queue.length - 1].name === DELETE_KEY; + } + }, + }); + if (result.type !== "error") { + transaction.commit(); + } + return result; + } + + /** + * Helper method to perform this delete action + */ + private async performDelete( + transaction: TransactionalDataset, + ): Promise { + try { + const response = await this.context.fetch(this.uri, { + method: "delete", + }); + + if (ServerHttpError.is(response)) { + return new ServerHttpError(this.uri, response); + } + if (UnauthenticatedHttpError.is(response)) { + return new UnauthenticatedHttpError(this.uri, response); + } + // Specifically check for a 205. Annoyingly, the server will return 200 even + // if it hasn't been deleted when you're unauthenticated. 404 happens when + // the document never existed + if (response.status === 205 || response.status === 404) { + transaction.deleteMatches( + undefined, + undefined, + undefined, + namedNode(this.uri), + ); + deleteResourceRdfFromContainer(this.uri, transaction); + return new AbsentResult(this.uri); + } + return new UnexpectedHttpError(this.uri, response); + } catch (err) { + return UnexpectedError.fromThrown(this.uri, err); + } + } } diff --git a/packages/solid/src/requester/requesterResults/DataResult.ts b/packages/solid/src/requester/requesterResults/DataResult.ts index 0c935a3..6662a59 100644 --- a/packages/solid/src/requester/requesterResults/DataResult.ts +++ b/packages/solid/src/requester/requesterResults/DataResult.ts @@ -11,5 +11,5 @@ export class DataResult extends RequesterResult { } export class TurtleFormattingError extends ErrorResult { - errorType = "turtleFormatting"; + errorType = "turtleFormatting" as const; } diff --git a/packages/solid/src/requester/requesterResults/HttpErrorResult.ts b/packages/solid/src/requester/requesterResults/HttpErrorResult.ts index b427130..979e9a1 100644 --- a/packages/solid/src/requester/requesterResults/HttpErrorResult.ts +++ b/packages/solid/src/requester/requesterResults/HttpErrorResult.ts @@ -33,11 +33,10 @@ export class UnexpectedHttpError extends HttpErrorResult { } export class UnauthenticatedHttpError extends HttpErrorResult { - status: 401; errorType = "unauthenticated" as const; static is(response: Response) { - return response.status === 404; + return response.status === 401; } } diff --git a/packages/solid/src/util/rdfUtils.ts b/packages/solid/src/util/rdfUtils.ts new file mode 100644 index 0000000..c6fbbe0 --- /dev/null +++ b/packages/solid/src/util/rdfUtils.ts @@ -0,0 +1,106 @@ +import { parseRdf } from "@ldo/ldo"; +import { namedNode, quad as createQuad } from "@rdfjs/data-model"; +import { DataResult } from "../requester/requesterResults/DataResult"; +import { TurtleFormattingError } from "../requester/requesterResults/DataResult"; +import type { Dataset } from "@rdfjs/types"; +import { isContainerUri } from "./uriTypes"; +import { TransactionalDataset } from "@ldo/subscribable-dataset"; + +const ldpContains = namedNode("http://www.w3.org/ns/ldp#contains"); +const rdfType = namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); +const ldpResource = namedNode("http://www.w3.org/ns/ldp#Resource"); +const ldpContainer = namedNode("http://www.w3.org/ns/ldp#Container"); +const ldpBasicContainer = namedNode("http://www.w3.org/ns/ldp#BasicContainer"); + +export function getParentUri(uri: string): string | undefined { + const urlObject = new URL(uri); + const pathItems = urlObject.pathname.split("/"); + if ( + pathItems.length < 2 || + (pathItems.length === 2 && pathItems[1].length === 0) + ) { + return undefined; + } + if (pathItems[pathItems.length - 1] === "") { + pathItems.pop(); + } + pathItems.pop(); + urlObject.pathname = `${pathItems.join("/")}/`; + return urlObject.toString(); +} + +export function getSlug(uri: string): string { + const urlObject = new URL(uri); + const pathItems = urlObject.pathname.split("/"); + return pathItems[pathItems.length - 1]; +} + +export function deleteResourceRdfFromContainer( + resourceUri: string, + dataset: Dataset, +) { + const parentUri = getParentUri(resourceUri); + if (parentUri) { + const parentNode = namedNode(parentUri); + const resourceNode = namedNode(resourceUri); + dataset.delete( + createQuad(parentNode, ldpContains, resourceNode, parentNode), + ); + dataset.deleteMatches(resourceNode, undefined, undefined, parentNode); + } +} + +export function addResourceRdfToContainer( + resourceUri: string, + dataset: Dataset, +) { + const parentUri = getParentUri(resourceUri); + console.log("Before thing"); + console.log(dataset.toString()); + if (parentUri) { + const parentNode = namedNode(parentUri); + const resourceNode = namedNode(resourceUri); + dataset.add(createQuad(parentNode, ldpContains, resourceNode, parentNode)); + console.log("In Between thing"); + console.log(dataset.toString()); + console.log((dataset as TransactionalDataset).getChanges()); + dataset.add(createQuad(resourceNode, rdfType, ldpResource, parentNode)); + if (isContainerUri(resourceUri)) { + dataset.add( + createQuad(resourceNode, rdfType, ldpBasicContainer, parentNode), + ); + dataset.add(createQuad(resourceNode, rdfType, ldpContainer, parentNode)); + } + } + console.log("After thing"); + console.log(dataset.toString()); +} + +export async function addRawTurtleToDataset( + rawTurtle: string, + dataset: Dataset, + baseUri: string, +): Promise { + let loadedDataset: Dataset; + try { + loadedDataset = await parseRdf(rawTurtle, { + baseIRI: baseUri, + }); + } catch (err) { + return new TurtleFormattingError( + baseUri, + err instanceof Error ? err.message : "Failed to parse rdf", + ); + } + + const graphNode = namedNode(baseUri); + // Destroy all triples that were once a part of this resouce + dataset.deleteMatches(undefined, undefined, undefined, graphNode); + // Add the triples from the fetched item + dataset.addAll( + loadedDataset.map((quad) => + createQuad(quad.subject, quad.predicate, quad.object, graphNode), + ), + ); + return new DataResult(baseUri); +} diff --git a/packages/solid/src/uriTypes.ts b/packages/solid/src/util/uriTypes.ts similarity index 100% rename from packages/solid/src/uriTypes.ts rename to packages/solid/src/util/uriTypes.ts diff --git a/packages/solid/test/LeafRequester.test.ts b/packages/solid/test/LeafRequester.test.ts index 8b937e9..743e5de 100644 --- a/packages/solid/test/LeafRequester.test.ts +++ b/packages/solid/test/LeafRequester.test.ts @@ -1,19 +1,15 @@ import type { App } from "@solid/community-server"; +import { getAuthenticatedFetch, ROOT_COONTAINER } from "./solidServer.helper"; +import type { SolidLdoDataset } from "../src/SolidLdoDataset"; +import { createSolidLdoDataset } from "../src/createSolidLdoDataset"; import { LeafRequester } from "../src/requester/LeafRequester"; -import crossFetch from "cross-fetch"; -import { - createApp, - getSecret, - refreshToken, - type ISecretData, - type ITokenData, - getAuthenticatedFetch, -} from "./solidServer.helper"; -import { buildAuthenticatedFetch } from "@inrupt/solid-client-authn-core"; +import { namedNode, quad as createQuad } from "@rdfjs/data-model"; describe("Leaf Requester", () => { let app: App; let authFetch: typeof fetch; + let fetchMock: typeof fetch; + let solidLdoDataset: SolidLdoDataset; beforeAll(async () => { // Start up the server @@ -23,26 +19,218 @@ describe("Leaf Requester", () => { authFetch = await getAuthenticatedFetch(); }); - it("special request", async () => { - const response = await authFetch( - "https://solidweb.me/jackson/everything_public/anonexistentfile.json", - { + beforeEach(async () => { + fetchMock = jest.fn(authFetch); + solidLdoDataset = createSolidLdoDataset({ fetch: fetchMock }); + // Create a new document called sample.ttl + const [result] = await Promise.all([ + authFetch(`${ROOT_COONTAINER}test_leaf/`, { + method: "POST", + headers: { "content-type": "text/turtle", slug: "sample.ttl" }, + body: `@base . + @prefix rdf: . + @prefix rdfs: . + @prefix foaf: . + @prefix rel: . + + <#green-goblin> + rel:enemyOf <#spiderman> ; + a foaf:Person ; # in the context of the Marvel universe + foaf:name "Green Goblin" . + + <#spiderman> + rel:enemyOf <#green-goblin> ; + a foaf:Person ; + foaf:name "Spiderman", "Человек-паук"@ru .`, + }), + authFetch(`${ROOT_COONTAINER}test_leaf/`, { method: "PUT", - headers: { "content-type": "application/json+ld" }, - body: JSON.stringify({ some: "test" }), - }, + headers: { "content-type": "text/plain", slug: "sample.txt" }, + body: `some text.`, + }), + ]); + }); + + afterEach(async () => { + await Promise.all([ + authFetch(`${ROOT_COONTAINER}test_leaf/sample.ttl`, { + method: "DELETE", + }), + authFetch(`${ROOT_COONTAINER}test_leaf/sample2.ttl`, { + method: "DELETE", + }), + authFetch(`${ROOT_COONTAINER}test_leaf/sample.txt`, { + method: "DELETE", + }), + authFetch(`${ROOT_COONTAINER}test_leaf/sample2.txt`, { + method: "DELETE", + }), + ]); + }); + + it("reads data", async () => { + const leafRequester = new LeafRequester( + `${ROOT_COONTAINER}test_leaf/sample.ttl`, + solidLdoDataset.context, + ); + const result = await leafRequester.read(); + expect(result.type).toBe("data"); + expect( + solidLdoDataset.match( + null, + null, + null, + namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`), + ).size, + ).toBe(7); + }); + + it("reads data that doesn't exist", async () => { + const leafRequester = new LeafRequester( + `${ROOT_COONTAINER}test_leaf/doesnotexist.ttl`, + solidLdoDataset.context, + ); + const result = await leafRequester.read(); + expect(result.type).toBe("absent"); + }); + + it("creates a data resource that doesn't exist while not overwriting", async () => { + const leafRequester = new LeafRequester( + `${ROOT_COONTAINER}test_leaf/sample2.ttl`, + solidLdoDataset.context, ); - console.log("STATUS:", response.status); - console.log("HEADERS:", response.headers); - console.log("BODY:", await response.text()); + const result = await leafRequester.createDataResource(); + expect(result.type).toBe("data"); + expect( + solidLdoDataset.has( + createQuad( + namedNode(`${ROOT_COONTAINER}test_leaf/`), + namedNode("http://www.w3.org/ns/ldp#contains"), + namedNode(`${ROOT_COONTAINER}test_leaf/sample2.ttl`), + namedNode(`${ROOT_COONTAINER}test_leaf/`), + ), + ), + ).toBe(true); }); - it("reads", async () => { + it("creates a data resource that doesn't exist while overwriting", async () => { const leafRequester = new LeafRequester( - "https://solidweb.me/jackson/everything-public/someotherfile.json", - { fetch: authFetch }, + `${ROOT_COONTAINER}test_leaf/sample2.ttl`, + solidLdoDataset.context, ); + const result = await leafRequester.createDataResource(true); + expect(result.type).toBe("data"); + console.log(solidLdoDataset.toString()); + expect( + solidLdoDataset.has( + createQuad( + namedNode(`${ROOT_COONTAINER}test_leaf/`), + namedNode("http://www.w3.org/ns/ldp#contains"), + namedNode(`${ROOT_COONTAINER}test_leaf/sample2.ttl`), + namedNode(`${ROOT_COONTAINER}test_leaf/`), + ), + ), + ).toBe(true); + }); - await leafRequester.read(); + it("creates a data resource that does exist while not overwriting", async () => { + const leafRequester = new LeafRequester( + `${ROOT_COONTAINER}test_leaf/sample.ttl`, + solidLdoDataset.context, + ); + const result = await leafRequester.createDataResource(); + expect(result.type).toBe("data"); + expect( + solidLdoDataset.has( + createQuad( + namedNode("http://example.org/#spiderman"), + namedNode("http://www.perceive.net/schemas/relationship/enemyOf"), + namedNode("http://example.org/#green-goblin"), + namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`), + ), + ), + ).toBe(true); + expect( + solidLdoDataset.has( + createQuad( + namedNode(`${ROOT_COONTAINER}test_leaf/`), + namedNode("http://www.w3.org/ns/ldp#contains"), + namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`), + namedNode(`${ROOT_COONTAINER}test_leaf/`), + ), + ), + ).toBe(true); + }); + + it("creates a data resource that does exist while overwriting", async () => { + const leafRequester = new LeafRequester( + `${ROOT_COONTAINER}test_leaf/sample.ttl`, + solidLdoDataset.context, + ); + const result = await leafRequester.createDataResource(true); + expect(result.type).toBe("data"); + expect( + solidLdoDataset.has( + createQuad( + namedNode("http://example.org/#spiderman"), + namedNode("http://www.perceive.net/schemas/relationship/enemyOf"), + namedNode("http://example.org/#green-goblin"), + namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`), + ), + ), + ).toBe(false); + expect( + solidLdoDataset.has( + createQuad( + namedNode(`${ROOT_COONTAINER}test_leaf/`), + namedNode("http://www.w3.org/ns/ldp#contains"), + namedNode(`${ROOT_COONTAINER}test_leaf/sample.ttl`), + namedNode(`${ROOT_COONTAINER}test_leaf/`), + ), + ), + ).toBe(true); + }); + + it("deletes data", async () => { + solidLdoDataset.add( + createQuad( + namedNode("a"), + namedNode("b"), + namedNode("c"), + namedNode(`${ROOT_COONTAINER}/test_leaf/sample.ttl`), + ), + ); + solidLdoDataset.add( + createQuad( + namedNode(`${ROOT_COONTAINER}/test_leaf/`), + namedNode("http://www.w3.org/ns/ldp#contains"), + namedNode(`${ROOT_COONTAINER}/test_leaf/sample.ttl`), + namedNode(`${ROOT_COONTAINER}/test_leaf/`), + ), + ); + const leafRequester = new LeafRequester( + `${ROOT_COONTAINER}/test_leaf/sample.ttl`, + solidLdoDataset.context, + ); + const result = await leafRequester.delete(); + expect(result.type).toBe("absent"); + expect( + solidLdoDataset.match( + null, + null, + null, + namedNode(`${ROOT_COONTAINER}/test_leaf/sample.ttl`), + ).size, + ).toBe(0); + expect( + solidLdoDataset.has( + createQuad( + namedNode(`${ROOT_COONTAINER}/test_leaf/`), + namedNode("http://www.w3.org/ns/ldp#contains"), + namedNode(`${ROOT_COONTAINER}/test_leaf/sample.ttl`), + namedNode(`${ROOT_COONTAINER}/test_leaf/`), + ), + ), + ).toBe(false); }); }); diff --git a/packages/solid/test/setup-tests.ts b/packages/solid/test/setup-tests.ts new file mode 100644 index 0000000..f4378ae --- /dev/null +++ b/packages/solid/test/setup-tests.ts @@ -0,0 +1,3 @@ +import { config } from "dotenv"; + +config(); diff --git a/packages/solid/test/solidServer.helper.ts b/packages/solid/test/solidServer.helper.ts index 8bc0f85..5a71c3e 100644 --- a/packages/solid/test/solidServer.helper.ts +++ b/packages/solid/test/solidServer.helper.ts @@ -13,11 +13,14 @@ import fetch from "cross-fetch"; const config = [ { - + podName: process.env.USER_NAME, + email: process.env.EMAIL, + password: process.env.PASSWORD, }, ]; -const SERVER_DOMAIN = "https://solidweb.me"; +export const SERVER_DOMAIN = process.env.SERVER; +export const ROOT_COONTAINER = `${process.env.SERVER}${process.env.ROOT_CONTAINER}`; // Use an increased timeout, since the CSS server takes too much setup time. jest.setTimeout(40_000); @@ -49,7 +52,7 @@ export interface ISecretData { // From https://communitysolidserver.github.io/CommunitySolidServer/5.x/usage/client-credentials/ export async function getSecret(): Promise { - const result = await fetch(`${SERVER_DOMAIN}/idp/credentials/`, { + const result = await fetch(`${SERVER_DOMAIN}idp/credentials/`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ @@ -74,7 +77,7 @@ export async function refreshToken({ }: ISecretData): Promise { const dpopKey = await generateDpopKeyPair(); const authString = `${encodeURIComponent(id)}:${encodeURIComponent(secret)}`; - const tokenUrl = `${SERVER_DOMAIN}/.oidc/token`; + const tokenUrl = `${SERVER_DOMAIN}.oidc/token`; const accessToken = await fetch(tokenUrl, { method: "POST", headers: { diff --git a/packages/subscribable-dataset/src/ProxyTransactionalDataset.ts b/packages/subscribable-dataset/src/ProxyTransactionalDataset.ts index ab8e265..81502f1 100644 --- a/packages/subscribable-dataset/src/ProxyTransactionalDataset.ts +++ b/packages/subscribable-dataset/src/ProxyTransactionalDataset.ts @@ -247,6 +247,14 @@ export default class ProxyTransactionalDataset< } 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) { @@ -257,18 +265,13 @@ export default class ProxyTransactionalDataset< changes.removed, ); } - } - - // Remove duplicates between the two datasets - if (this.datasetChanges.added && this.datasetChanges.removed) { - const changesIntersection = this.datasetChanges.added.intersection( - this.datasetChanges.removed, + // Delete from added if present + const changesIntersection = this.datasetChanges.added?.intersection( + this.datasetFactory.dataset(changes.removed), ); - if (changesIntersection.size > 0) { + if (changesIntersection && changesIntersection.size > 0) { this.datasetChanges.added = - this.datasetChanges.added.difference(changesIntersection); - this.datasetChanges.removed = - this.datasetChanges.removed.difference(changesIntersection); + this.datasetChanges.added?.difference(changesIntersection); } } diff --git a/packages/subscribable-dataset/test/ProxyTransactionalDataset.test.ts b/packages/subscribable-dataset/test/ProxyTransactionalDataset.test.ts index 181abeb..bc380be 100644 --- a/packages/subscribable-dataset/test/ProxyTransactionalDataset.test.ts +++ b/packages/subscribable-dataset/test/ProxyTransactionalDataset.test.ts @@ -120,6 +120,17 @@ describe("ProxyTransactionalDataset", () => { expect(arr.some((curQuad) => curQuad.equals(tomTypeQuad))).toBe(true); }); + it("Removes then adds a quad and the quad is still added", () => { + const addedQuad = lickyNameQuad; + transactionalDataset.delete(addedQuad); + transactionalDataset.add(addedQuad); + const arr = transactionalDataset.toArray(); + expect(arr.length).toBe(3); + expect(arr.some((curQuad) => curQuad.equals(tomNameQuad))).toBe(true); + expect(arr.some((curQuad) => curQuad.equals(tomTypeQuad))).toBe(true); + expect(arr.some((curQuad) => curQuad.equals(lickyNameQuad))).toBe(true); + }); + it("Commits added changes", () => { transactionalDataset.add(lickyNameQuad); transactionalDataset.commit();