fixed problem with transaction dataset forgetting added triples

main
jaxoncreed 2 years ago
parent fa141fed5d
commit 33c2c5cf9f
  1. 1
      .gitignore
  2. 8
      package-lock.json
  3. 1
      packages/solid/jest.config.js
  4. 1
      packages/solid/package.json
  5. 7
      packages/solid/src/SolidLdoDataset.ts
  6. 20
      packages/solid/src/createSolidLdoDataset.ts
  7. 247
      packages/solid/src/requester/LeafRequester.ts
  8. 2
      packages/solid/src/requester/requesterResults/DataResult.ts
  9. 3
      packages/solid/src/requester/requesterResults/HttpErrorResult.ts
  10. 106
      packages/solid/src/util/rdfUtils.ts
  11. 0
      packages/solid/src/util/uriTypes.ts
  12. 236
      packages/solid/test/LeafRequester.test.ts
  13. 3
      packages/solid/test/setup-tests.ts
  14. 11
      packages/solid/test/solidServer.helper.ts
  15. 23
      packages/subscribable-dataset/src/ProxyTransactionalDataset.ts
  16. 11
      packages/subscribable-dataset/test/ProxyTransactionalDataset.test.ts

1
.gitignore vendored

@ -5,6 +5,7 @@ packages/*/dist
.idea
.DS_Store
.env
.env.local
.env.development.local
.env.test.local

8
package-lock.json generated

@ -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",

@ -3,4 +3,5 @@ const sharedConfig = require("../../jest.config.js");
module.exports = {
...sharedConfig,
rootDir: "./",
setupFiles: ["<rootDir>/test/setup-tests.ts"],
};

@ -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",

@ -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");
// }
}

@ -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;

@ -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<ReadResult> {
/**
* Read this resource.
*/
async read(): Promise<ReadResult> {
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<Quad>) =>
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<ReadResult> {
/**
* Helper method to perform the read action
*/
private async performRead(
transaction: TransactionalDataset<Quad>,
): Promise<ReadResult> {
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<DataLeaf | ResourceError>;
/**
* Creates a Resource
* @param overwrite: If true, this will orverwrite the resource if it already
* exists
*/
async createDataResource(
overwrite?: false,
): Promise<CreateResultWithoutOverwrite>;
async createDataResource(overwrite: true): Promise<CreateResult>;
async createDataResource(
overwrite?: boolean,
): Promise<CreateResultWithoutOverwrite | CreateResult>;
async createDataResource(
overwrite?: boolean,
): Promise<CreateResultWithoutOverwrite> {
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<Quad>, 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<Quad>,
overwrite?: false,
): Promise<CreateResultWithoutOverwrite>;
private async performCreateDataResource(
transaction: TransactionalDataset<Quad>,
overwrite: true,
): Promise<CreateResult>;
private async performCreateDataResource(
transaction: TransactionalDataset<Quad>,
overwrite?: boolean,
): Promise<CreateResultWithoutOverwrite | CreateResult>;
private async performCreateDataResource(
transaction: TransactionalDataset<Quad>,
overwrite?: boolean,
): Promise<CreateResultWithoutOverwrite> {
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<DataLeaf | ResourceError>;
// abstract delete(): Promise<AbsentLeaf | ResourceError>;
/**
* Delete this resource
*/
async delete(): Promise<DeleteResult> {
const DELETE_KEY = "delete";
const transaction = this.context.solidLdoDataset.startTransaction();
const result = await this.requestBatcher.queueProcess({
name: DELETE_KEY,
args: [transaction],
perform: (transaction: TransactionalDataset<Quad>) =>
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<Quad>,
): Promise<DeleteResult> {
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);
}
}
}

@ -11,5 +11,5 @@ export class DataResult extends RequesterResult {
}
export class TurtleFormattingError extends ErrorResult {
errorType = "turtleFormatting";
errorType = "turtleFormatting" as const;
}

@ -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;
}
}

@ -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<DataResult | TurtleFormattingError> {
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);
}

@ -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 <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rel: <http://www.perceive.net/schemas/relationship/> .
<#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);
});
});

@ -0,0 +1,3 @@
import { config } from "dotenv";
config();

@ -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<ISecretData> {
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<ITokenData> {
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: {

@ -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);
}
}

@ -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();

Loading…
Cancel
Save