@inrupt/solid-client
Version:
Make your web apps work with Solid Pods.
798 lines (754 loc) • 27.7 kB
text/typescript
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { jest, describe, it, expect } from "@jest/globals";
import { dataset } from "@rdfjs/dataset";
import * as fc from "fast-check";
import { DataFactory as DF, Store } from "n3";
import type {
BlankNode,
DatasetCore,
Quad,
Term,
DatasetCoreFactory,
DataFactory,
} from "@rdfjs/types";
import {
serializeBoolean,
serializeDatetime,
serializeDecimal,
serializeInteger,
xmlSchemaTypes,
} from "./datatypes";
import { isBlankNodeId, type ImmutableDataset } from "./rdf.internal";
import { addRdfJsQuadToDataset } from "./rdfjs.internal";
import { fromRdfJsDataset, toRdfJsDataset } from "./rdfjs";
import { asUrl, getThing, getThingAll } from "./thing/thing";
import { getTermAll } from "./thing/get";
describe("fromRdfJsDataset", () => {
const fcNamedNode = fc
.webUrl({ withFragments: true, withQueryParameters: true })
.map((url) => DF.namedNode(url));
const fcString = fc.string().map((value) => DF.literal(value));
const fcInteger = fc
.integer()
.map((value) =>
DF.literal(serializeInteger(value), DF.namedNode(xmlSchemaTypes.integer)),
);
const fcDecimal = fc
.float()
.map((value) =>
DF.literal(serializeDecimal(value), DF.namedNode(xmlSchemaTypes.decimal)),
);
const fcDatetime = fc
.date({ noInvalidDate: true })
.map((value) =>
DF.literal(
serializeDatetime(value),
DF.namedNode(xmlSchemaTypes.dateTime),
),
);
const fcBoolean = fc
.boolean()
.map((value) =>
DF.literal(serializeBoolean(value), DF.namedNode(xmlSchemaTypes.boolean)),
);
const fcLangString = fc
.tuple(
fc.string(),
fc.oneof(fc.constant("nl-NL"), fc.constant("en-GB"), fc.constant("fr")),
)
.map(([value, lang]) => DF.literal(value, lang));
const fcArbitraryLiteral = fc
.tuple(fc.string(), fc.webUrl({ withFragments: true }))
.map(([value, dataType]) => DF.literal(value, DF.namedNode(dataType)));
const fcLiteral = fc.oneof(
fcString,
fcInteger,
fcDecimal,
fcDatetime,
fcBoolean,
fcLangString,
fcArbitraryLiteral,
);
const fcBlankNode = fc
.string()
.map((asciiString) => DF.blankNode(asciiString));
const fcDefaultGraph = fc.constant(DF.defaultGraph());
const fcGraph = fc.oneof(fcDefaultGraph, fcNamedNode);
const fcQuadSubject = fc.oneof(fcNamedNode, fcBlankNode);
const fcQuadPredicate = fcNamedNode;
const fcQuadObject = fc.oneof(fcNamedNode, fcLiteral, fcBlankNode);
const fcQuad = fc
.tuple(fcQuadSubject, fcQuadPredicate, fcQuadObject, fcGraph)
.map(([subject, predicate, object, graph]) =>
DF.quad(subject, predicate, object, graph),
);
const fcDatasetWithReusedBlankNodes = fc.uniqueArray(fcQuad).map((quads) => {
const reusedBlankNode = DF.blankNode();
function maybeReplaceBlankNode(node: BlankNode): BlankNode {
return Math.random() < 0.5 ? node : reusedBlankNode;
}
function maybeReplaceBlankNodesInQuad(quad: Quad): Quad {
const subject =
quad.subject.termType === "BlankNode"
? maybeReplaceBlankNode(quad.subject)
: quad.subject;
const object =
quad.object.termType === "BlankNode"
? maybeReplaceBlankNode(quad.object)
: quad.object;
return DF.quad(subject, quad.predicate, object, quad.graph);
}
return dataset(quads.map(maybeReplaceBlankNodesInQuad));
});
it("loses no data", () => {
const runs = process.env.CI ? 1000 : 100;
expect.assertions(runs * 2 + 2);
function hasMatchingQuads(a: DatasetCore, b: DatasetCore): boolean {
function blankNodeToNull(term: Term): Term | null {
return term.termType === "BlankNode" ? null : term;
}
const aQuads = Array.from(a);
const bQuads = Array.from(b);
return (
aQuads.every((quad) =>
b.match(
blankNodeToNull(quad.subject),
quad.predicate,
blankNodeToNull(quad.object),
quad.graph,
),
) &&
bQuads.every((quad) =>
a.match(
blankNodeToNull(quad.subject),
quad.predicate,
blankNodeToNull(quad.object),
quad.graph,
),
)
);
}
const fcResult = fc.check(
fc.property(fcDatasetWithReusedBlankNodes, (data) => {
const thereAndBackAgain = toRdfJsDataset(fromRdfJsDataset(data));
expect(thereAndBackAgain.size).toBe(data.size);
expect(hasMatchingQuads(thereAndBackAgain, data)).toBe(true);
}),
{ numRuns: runs },
);
expect(fcResult.counterexample).toBeNull();
expect(fcResult.failed).toBe(false);
});
it("can represent all Quads", () => {
const blankNode1 = DF.blankNode();
const blankNode2 = DF.blankNode();
const subject1IriString = "https://some.pod/resource#subject1";
const subject1 = DF.namedNode(subject1IriString);
const subject2IriString = "https://some.pod/resource#subject2";
const subject2 = DF.namedNode(subject2IriString);
const predicate1IriString = "https://some.vocab/predicate1";
const predicate1 = DF.namedNode(predicate1IriString);
const predicate2IriString = "https://some.vocab/predicate2";
const predicate2 = DF.namedNode(predicate2IriString);
const literalStringValue = "Some string";
const literalString = DF.literal(
literalStringValue,
DF.namedNode(xmlSchemaTypes.string),
);
const literalLangStringValue = "Some lang string";
const literalLangStringLocale = "en-gb";
const literalLangString = DF.literal(
literalLangStringValue,
literalLangStringLocale,
);
const literalIntegerValue = "42";
const literalInteger = DF.literal(
literalIntegerValue,
DF.namedNode(xmlSchemaTypes.integer),
);
const defaultGraph = DF.defaultGraph();
const acrGraphIriString = "https://some.pod/resource?ext=acr";
const acrGraph = DF.namedNode(acrGraphIriString);
const quads = [
DF.quad(subject1, predicate1, literalString, defaultGraph),
DF.quad(subject1, predicate1, literalLangString, defaultGraph),
DF.quad(subject1, predicate1, literalInteger, defaultGraph),
DF.quad(subject1, predicate2, subject2, defaultGraph),
DF.quad(subject2, predicate1, blankNode1, acrGraph),
DF.quad(subject2, predicate1, blankNode2, acrGraph),
DF.quad(blankNode1, predicate1, literalString, acrGraph),
DF.quad(blankNode2, predicate1, literalString, acrGraph),
DF.quad(blankNode2, predicate1, literalInteger, acrGraph),
DF.quad(blankNode2, predicate2, literalInteger, acrGraph),
];
const rdfJsDataset = dataset(quads);
expect(fromRdfJsDataset(rdfJsDataset)).toStrictEqual({
type: "Dataset",
graphs: {
default: expect.objectContaining({
[subject1IriString]: {
url: subject1IriString,
type: "Subject",
predicates: {
[predicate1IriString]: {
literals: {
[xmlSchemaTypes.string]: [literalStringValue],
[xmlSchemaTypes.integer]: [literalIntegerValue],
},
langStrings: {
[literalLangStringLocale]: [literalLangStringValue],
},
},
[predicate2IriString]: {
namedNodes: [subject2IriString],
},
},
},
}),
[acrGraphIriString]: expect.objectContaining({
[subject2IriString]: {
url: subject2IriString,
type: "Subject",
predicates: {
[predicate1IriString]: {
blankNodes: [
expect.stringMatching(/_:/),
expect.stringMatching(/_:/),
],
},
},
},
}),
},
});
const subjectsExcludingBlankNodes = getThingAll(
fromRdfJsDataset(rdfJsDataset),
{ scope: acrGraphIriString },
);
const subjectsIncludingBlankNodes = getThingAll(
fromRdfJsDataset(rdfJsDataset),
{ scope: acrGraphIriString, acceptBlankNodes: true },
);
// There should be two blank nodes in the resulting dataset.
expect(
subjectsIncludingBlankNodes.length - subjectsExcludingBlankNodes.length,
).toBe(2);
});
it("can represent lists", () => {
const first = DF.namedNode(
"http://www.w3.org/1999/02/22-rdf-syntax-ns#first",
);
const rest = DF.namedNode(
"http://www.w3.org/1999/02/22-rdf-syntax-ns#rest",
);
const nil = DF.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#nil");
const item1Node = DF.blankNode();
const item2Node = DF.blankNode();
const quad1 = DF.quad(item1Node, first, DF.literal("First item in a list"));
const quad2 = DF.quad(item1Node, rest, item2Node);
const quad3 = DF.quad(
item2Node,
first,
DF.literal("Second item in a list"),
);
const quad4 = DF.quad(item2Node, rest, nil);
const rdfJsDataset = dataset([quad1, quad2, quad3, quad4]);
const thereAndBackAgain = toRdfJsDataset(fromRdfJsDataset(rdfJsDataset));
expect(thereAndBackAgain.size).toBe(4);
expect(
thereAndBackAgain.match(null, null, DF.literal("First item in a list"))
.size,
).toBe(1);
expect(
thereAndBackAgain.match(null, null, DF.literal("Second item in a list"))
.size,
).toBe(1);
});
it("does not lose any predicates", () => {
const blankNode1 = DF.blankNode();
const blankNode2 = DF.blankNode();
const blankNode3 = DF.blankNode();
const blankNode4 = DF.blankNode();
const predicate1 = DF.namedNode("https://example.com/predicate1");
const predicate2 = DF.namedNode("https://example.com/predicate2");
const predicate3 = DF.namedNode("https://example.com/predicate3");
const acrGraph = DF.namedNode("https://example.com/acrGraph");
const literalString = DF.literal("Arbitrary literal string");
const quads = [
DF.quad(blankNode1, predicate1, blankNode2, acrGraph),
DF.quad(blankNode2, predicate2, blankNode3, acrGraph),
DF.quad(blankNode3, predicate3, blankNode4, acrGraph),
DF.quad(blankNode4, predicate2, literalString, acrGraph),
];
const rdfJsDataset = dataset(quads);
const thereAndBackAgain = toRdfJsDataset(fromRdfJsDataset(rdfJsDataset));
expect(thereAndBackAgain.size).toBe(rdfJsDataset.size);
});
it("does not trip over circular blank nodes", () => {
const namedNode = DF.namedNode("https://example.com/namedNode");
const blankNode1 = DF.blankNode();
const blankNode2 = DF.blankNode();
const blankNode3 = DF.blankNode();
const predicate = DF.namedNode("https://example.com/predicate");
const literalString = DF.literal("Arbitrary literal string");
const quads = [
DF.quad(namedNode, predicate, blankNode2),
DF.quad(blankNode1, predicate, blankNode2),
DF.quad(blankNode2, predicate, blankNode3),
DF.quad(blankNode3, predicate, blankNode1),
DF.quad(blankNode2, predicate, literalString),
];
const rdfJsDataset = dataset(quads);
const thereAndBackAgain = toRdfJsDataset(fromRdfJsDataset(rdfJsDataset));
expect(thereAndBackAgain.size).toBe(rdfJsDataset.size);
});
it("does not trip over blank nodes that appear as the object for different subjects", () => {
const blankNode1 = DF.blankNode();
const blankNode2 = DF.blankNode();
const blankNode3 = DF.blankNode();
const predicate = DF.namedNode("https://example.com/predicate");
const literalString = DF.literal("Arbitrary literal string");
const quads = [
DF.quad(blankNode1, predicate, blankNode2),
DF.quad(blankNode2, predicate, literalString),
DF.quad(blankNode3, predicate, blankNode2),
];
const rdfJsDataset = dataset(quads);
const thereAndBackAgain = toRdfJsDataset(fromRdfJsDataset(rdfJsDataset));
expect(thereAndBackAgain.size).toBe(rdfJsDataset.size);
});
it("does not trip over Datasets that only contain Blank Node Subjects", () => {
const blankNode1 = DF.blankNode();
const blankNode2 = DF.blankNode();
const blankNode3 = DF.blankNode();
const predicate = DF.namedNode("https://example.com/predicate");
const literalString = DF.literal("Arbitrary literal string");
const quads = [
DF.quad(blankNode1, predicate, blankNode2),
DF.quad(blankNode2, predicate, blankNode3),
DF.quad(blankNode3, predicate, blankNode1),
DF.quad(blankNode2, predicate, literalString),
];
const rdfJsDataset = dataset(quads);
const thereAndBackAgain = toRdfJsDataset(fromRdfJsDataset(rdfJsDataset));
expect(thereAndBackAgain.size).toBe(rdfJsDataset.size);
});
describe("addRdfJsQuadToDataset", () => {
it("can parse a simple Quad with a Blank Node Object", () => {
const mockDataset: ImmutableDataset = {
type: "Dataset",
graphs: { default: {} },
};
const mockQuad = DF.quad(
DF.namedNode("https://some.subject"),
DF.namedNode("https://some.predicate"),
DF.blankNode("some-blank-node"),
DF.defaultGraph(),
);
const data = addRdfJsQuadToDataset(mockDataset, mockQuad);
expect(data).toStrictEqual({
type: "Dataset",
graphs: {
default: {
"https://some.subject": {
type: "Subject",
url: "https://some.subject",
predicates: {
"https://some.predicate": {
blankNodes: ["_:some-blank-node"],
},
},
},
},
},
});
});
it("throws an error when passed unknown Graph types", () => {
const mockDataset: ImmutableDataset = {
type: "Dataset",
graphs: { default: {} },
};
const mockQuad = DF.quad(
DF.namedNode("https://arbitrary.subject"),
DF.namedNode("https://arbitrary.predicate"),
DF.namedNode("https://arbitrary.object"),
{ termType: "Unknown term type" } as any,
);
expect(() => addRdfJsQuadToDataset(mockDataset, mockQuad)).toThrow(
"Cannot parse Quads with nodes of type [Unknown term type] as their Graph node.",
);
});
it("throws an error when passed unknown Subject types", () => {
const mockDataset: ImmutableDataset = {
type: "Dataset",
graphs: { default: {} },
};
const mockQuad = DF.quad(
{ termType: "Unknown term type" } as any,
DF.namedNode("https://arbitrary.predicate"),
DF.namedNode("https://arbitrary.object"),
DF.defaultGraph(),
);
expect(() => addRdfJsQuadToDataset(mockDataset, mockQuad)).toThrow(
"Cannot parse Quads with nodes of type [Unknown term type] as their Subject node.",
);
});
it("throws an error when passed unknown Predicate types", () => {
const mockDataset: ImmutableDataset = {
type: "Dataset",
graphs: { default: {} },
};
const mockQuad = DF.quad(
DF.namedNode("https://arbitrary.subject"),
{ termType: "Unknown term type" } as any,
DF.namedNode("https://arbitrary.object"),
DF.defaultGraph(),
);
expect(() => addRdfJsQuadToDataset(mockDataset, mockQuad)).toThrow(
"Cannot parse Quads with nodes of type [Unknown term type] as their Predicate node.",
);
});
it("throws an error when passed unknown Object types", () => {
const mockDataset: ImmutableDataset = {
type: "Dataset",
graphs: { default: {} },
};
const mockQuad = DF.quad(
DF.namedNode("https://arbitrary.subject"),
DF.namedNode("https://arbitrary.predicate"),
{ termType: "Unknown term type" } as any,
DF.defaultGraph(),
);
expect(() => addRdfJsQuadToDataset(mockDataset, mockQuad)).toThrow(
"Objects of type [Unknown term type] are not supported.",
);
});
it("can parse chained Blank Nodes with a single link that end in a dangling Blank Node", () => {
const mockDataset: ImmutableDataset = {
type: "Dataset",
graphs: { default: {} },
};
const chainBlankNode1 = DF.blankNode();
const otherQuad = DF.quad(
DF.namedNode("https://some.subject"),
DF.namedNode("https://some.predicate/1"),
chainBlankNode1,
DF.defaultGraph(),
);
const mockQuad = DF.quad(
chainBlankNode1,
DF.namedNode("https://some.predicate/2"),
DF.blankNode("some-blank-node"),
DF.defaultGraph(),
);
const updatedDataset = [mockQuad, otherQuad].reduce(
addRdfJsQuadToDataset,
mockDataset,
);
// There should be one blank node subject.
expect(
getThingAll(updatedDataset, { acceptBlankNodes: false }),
).toHaveLength(1);
expect(
getThingAll(updatedDataset, { acceptBlankNodes: true }),
).toHaveLength(2);
// The blank nodes should be linked
const blankNodes = getThingAll(updatedDataset, {
acceptBlankNodes: true,
}).filter((thing) => isBlankNodeId(asUrl(thing)));
let bnAreLinked = false;
blankNodes.forEach((bn) => {
const candidateObjects = getTermAll(bn, "https://some.predicate/2");
bnAreLinked ||=
candidateObjects.length > 0 &&
candidateObjects.some((obj) => obj.termType === "BlankNode");
});
// The named node should be linked to a blank node
getTermAll(
getThing(updatedDataset, "https://some.subject")!,
"https://some.predicate/1",
).some((term) => term.termType === "BlankNode");
});
it("can parse chained Blank Nodes that end in a dangling Blank Node", () => {
const mockDataset: ImmutableDataset = {
type: "Dataset",
graphs: { default: {} },
};
const chainBlankNode1 = DF.blankNode();
const chainBlankNode2 = DF.blankNode();
const otherQuad = DF.quad(
DF.namedNode("https://some.subject"),
DF.namedNode("https://some.predicate/1"),
chainBlankNode1,
DF.defaultGraph(),
);
const inBetweenQuad = DF.quad(
chainBlankNode1,
DF.namedNode("https://some.predicate/2"),
chainBlankNode2,
DF.defaultGraph(),
);
const mockQuad = DF.quad(
chainBlankNode2,
DF.namedNode("https://some.predicate/3"),
DF.blankNode("some-blank-node"),
DF.defaultGraph(),
);
const updatedDataset = [mockQuad, inBetweenQuad, otherQuad].reduce(
addRdfJsQuadToDataset,
mockDataset,
);
// There should be 2 blank node subjects
expect(
getThingAll(updatedDataset, { acceptBlankNodes: false }),
).toHaveLength(1);
expect(
getThingAll(updatedDataset, { acceptBlankNodes: true }),
).toHaveLength(3);
// The blank nodes subjects and the blank node object should be linked.
const blankNodes = getThingAll(updatedDataset, {
acceptBlankNodes: true,
}).filter((thing) => isBlankNodeId(asUrl(thing)));
// Count the number of links between blank nodes,
// based on known predicates.
const bnLinks = blankNodes.reduce(
(prev, cur) =>
prev +
[
...getTermAll(cur, "https://some.predicate/2"),
...getTermAll(cur, "https://some.predicate/3"),
].filter((obj) => obj.termType === "BlankNode").length,
0,
);
// There should be a chain of links between blank nodes.
expect(bnLinks).toBe(2);
// The named node should be linked to a blank node.
getTermAll(
getThing(updatedDataset, "https://some.subject")!,
"https://some.predicate/1",
).some((term) => term.termType === "BlankNode");
});
});
});
describe("toRdfJsDataset", () => {
const isNotEmpty = (value: object) => {
if (typeof value !== "object") {
return false;
}
if (value === null) {
return false;
}
if (Array.isArray(value)) {
return value.length > 0;
}
return Object.keys(value).length > 0;
};
const fcLiterals = fc
.dictionary(
fc.webUrl({ withFragments: true }),
fc.uniqueArray(fc.string(), { minLength: 1 }),
)
.filter(isNotEmpty);
// Replaced deprecated hexaString with custom implementation for v4
const hexaChars = "0123456789abcdef";
const hexa = () => {
return fc.integer({ min: 0, max: 15 }).map(
(n) => hexaChars[n],
(c) => hexaChars.indexOf(<string>c),
);
};
const hexaString = (constraints: fc.StringConstraints = {}) =>
fc.string({ ...constraints, unit: hexa() });
const fcLangStrings = fc
.dictionary(
hexaString({ minLength: 1 }).map((str) => str.toLowerCase()),
fc.uniqueArray(fc.string(), { minLength: 1 }),
)
.filter(isNotEmpty);
const fcLocalNodeIri = fc.webUrl({ withFragments: true }).map((url) => {
const originalUrl = new URL(url);
return `https://inrupt.com/.well-known/sdk-local-node/${originalUrl.hash}`;
});
const fcNamedNodes = fc.uniqueArray(
fc.oneof(
fcLocalNodeIri,
fc.webUrl({ withFragments: true, withQueryParameters: true }),
),
{
minLength: 1,
},
);
// withDeletedKeys option was removed in v4, achieve similar functionality with filter
const fcObjects = fc
.record({
literals: fcLiterals,
langStrings: fcLangStrings,
namedNodes: fcNamedNodes,
// blankNodes: fcBlankNodes,
})
.map((obj) => {
// Randomly delete some keys to achieve similar behavior to withDeletedKeys
const keys = Object.keys(obj) as Array<keyof typeof obj>;
if (keys.length <= 1) return obj; // Keep at least one property
const result = { ...obj };
// Delete random keys with 50% chance for each
keys.forEach((key) => {
if (Math.random() < 0.5 && Object.keys(result).length > 1) {
delete result[key];
}
});
return result;
})
.filter(isNotEmpty);
// Unfortunately I haven't figured out how to generate the nested blank node
// structures with fast-check yet, so this does not generate those:
const fcPredicates = fc
.dictionary(fc.webUrl({ withFragments: true }), fcObjects)
.filter(isNotEmpty);
const fcGraph = fc
.dictionary(
fc.oneof(
fcLocalNodeIri,
fc.webUrl({ withFragments: true, withQueryParameters: true }),
),
fc.record({
type: fc.constant("Subject"),
url: fc.webUrl({ withFragments: true, withQueryParameters: true }),
predicates: fcPredicates,
}),
)
.filter(isNotEmpty)
.map((graph) => {
Object.keys(graph).forEach((subjectIri) => {
graph[subjectIri].url = subjectIri;
});
return graph;
});
const fcDataset = fc.record({
type: fc.constant("Dataset"),
graphs: fc
.tuple(
fc.dictionary(fc.webUrl({ withQueryParameters: true }), fcGraph),
fcGraph,
)
.map(([otherGraphs, defaultGraph]) => ({
...otherGraphs,
default: defaultGraph,
})),
});
it("loses no data when serializing and deserializing to RDF/JS Datasets", () => {
const runs = process.env.CI ? 100 : 1;
expect.assertions(runs + 2);
const fcResult = fc.check(
fc.property(fcDataset, (data) => {
expect(
sortObject(fromRdfJsDataset(toRdfJsDataset(data as any))),
).toStrictEqual(sortObject(data));
}),
{ numRuns: runs },
);
expect(fcResult.counterexample).toBeNull();
expect(fcResult.failed).toBe(false);
});
it("can represent dangling Blank Nodes", () => {
const datasetWithDanglingBlankNodes: ImmutableDataset = {
type: "Dataset",
graphs: {
default: {
"_:danglingSubjectBlankNode": {
type: "Subject",
url: "_:danglingSubjectBlankNode",
predicates: {
"http://www.w3.org/ns/auth/acl#origin": {
blankNodes: [{}],
},
},
},
},
},
};
const rdfJsDataset = toRdfJsDataset(datasetWithDanglingBlankNodes);
expect(rdfJsDataset.size).toBe(1);
const quad = Array.from(rdfJsDataset)[0];
expect(quad.subject.termType).toBe("BlankNode");
expect(quad.predicate.value).toBe("http://www.w3.org/ns/auth/acl#origin");
expect(quad.object.termType).toBe("BlankNode");
});
it("can take a custom DataFactory", () => {
const customDataFactory = {
quad: jest.fn(DF.quad),
namedNode: jest.fn(DF.namedNode),
literal: jest.fn(DF.literal),
blankNode: jest.fn(DF.blankNode),
defaultGraph: jest.fn(DF.defaultGraph),
} as DataFactory;
const customDatasetFactory = {
dataset: jest.fn((quads: Quad[]) => new Store(quads)),
} as DatasetCoreFactory;
const sourceDataset: ImmutableDataset = {
type: "Dataset",
graphs: {
default: {
"https://arbitrary.pod/resource#thing": {
type: "Subject",
url: "https://arbitrary.pod/resource#thing",
predicates: {
"https://arbitrary.vocab/predicate": {
namedNodes: ["https://arbitrary.pod/other-resource#thing"],
literals: {
"https://arbitrary.vocab/literal-type": ["Arbitrary value"],
},
blankNodes: ["_:arbitrary-blank-node"],
},
},
},
},
},
};
toRdfJsDataset(sourceDataset, {
dataFactory: customDataFactory,
datasetFactory: customDatasetFactory,
});
expect(customDataFactory.quad).toHaveBeenCalled();
expect(customDataFactory.namedNode).toHaveBeenCalled();
expect(customDataFactory.literal).toHaveBeenCalled();
expect(customDataFactory.blankNode).toHaveBeenCalled();
expect(customDataFactory.defaultGraph).toHaveBeenCalled();
expect(customDatasetFactory.dataset).toHaveBeenCalled();
});
});
function sortObject(value: Record<string, any>): Record<string, any> {
if (typeof value !== "object") {
return value;
}
if (Array.isArray(value)) {
return [...value].sort();
}
if (value === null) {
return value;
}
const keys = Object.keys(value);
keys.sort();
return keys.reduce(
(newObject, key) => ({ ...newObject, [key]: sortObject(value[key]) }),
{},
);
}