mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Write tests and fix related bugs, refactor code
This commit is contained in:
committed by
Joachim Van Herwegen
parent
6cc7053310
commit
dff4ba8efe
@@ -4,20 +4,29 @@ import arrayifyStream from 'arrayify-stream';
|
||||
import { fetch, Request } from 'cross-fetch';
|
||||
import { Util } from 'n3';
|
||||
import type { Quad } from 'rdf-js';
|
||||
import type { AskQuery, ConstructQuery, GraphPattern, SparqlQuery, Update } from 'sparqljs';
|
||||
import { Generator } from 'sparqljs';
|
||||
import type { AskQuery,
|
||||
ConstructQuery,
|
||||
GraphPattern,
|
||||
SelectQuery,
|
||||
SparqlQuery,
|
||||
Update } from 'sparqljs';
|
||||
import {
|
||||
Generator,
|
||||
Wildcard,
|
||||
} from 'sparqljs';
|
||||
import streamifyArray from 'streamify-array';
|
||||
import type { Patch } from '../ldp/http/Patch';
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { CONTENT_TYPE_QUADS, DATA_TYPE_QUAD } from '../util/ContentTypes';
|
||||
import { ConflictHttpError } from '../util/errors/ConflictHttpError';
|
||||
import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpError';
|
||||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||
import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError';
|
||||
import { LINK_TYPE_LDPC, LINK_TYPE_LDPR } from '../util/LinkTypes';
|
||||
import { CONTAINER_OBJECT, CONTAINS_PREDICATE, RESOURCE_OBJECT, TYPE_PREDICATE } from '../util/MetadataController';
|
||||
import type { ResourceStoreController } from '../util/ResourceStoreController';
|
||||
import { ensureTrailingSlash, readableToString, trimTrailingSlashes } from '../util/Util';
|
||||
import { ensureTrailingSlash, trimTrailingSlashes } from '../util/Util';
|
||||
import type { ContainerManager } from './ContainerManager';
|
||||
import type { ResourceStore } from './ResourceStore';
|
||||
import inDefaultGraph = Util.inDefaultGraph;
|
||||
|
||||
@@ -29,17 +38,21 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
private readonly baseRequestURI: string;
|
||||
private readonly sparqlEndpoint: string;
|
||||
private readonly resourceStoreController: ResourceStoreController;
|
||||
private readonly containerManager: ContainerManager;
|
||||
|
||||
/**
|
||||
* @param baseRequestURI - Will be stripped of all incoming URIs and added to all outgoing ones to find the relative
|
||||
* path.
|
||||
* @param sparqlEndpoint - URL of the SPARQL endpoint to use.
|
||||
* @param resourceStoreController - Instance of ResourceStoreController to use.
|
||||
* @param containerManager - Instance of ContainerManager to use.
|
||||
*/
|
||||
public constructor(baseRequestURI: string, sparqlEndpoint: string, resourceStoreController: ResourceStoreController) {
|
||||
public constructor(baseRequestURI: string, sparqlEndpoint: string, resourceStoreController: ResourceStoreController,
|
||||
containerManager: ContainerManager) {
|
||||
this.baseRequestURI = trimTrailingSlashes(baseRequestURI);
|
||||
this.sparqlEndpoint = sparqlEndpoint;
|
||||
this.resourceStoreController = resourceStoreController;
|
||||
this.containerManager = containerManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,14 +63,16 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
* @returns The newly generated identifier.
|
||||
*/
|
||||
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
|
||||
// Check if the representation has a valid dataType.
|
||||
this.ensureValidDataType(representation);
|
||||
|
||||
// Get the expected behaviour based on the incoming identifier and representation.
|
||||
const { isContainer, path, newIdentifier } = this.resourceStoreController.getBehaviourAddResource(container,
|
||||
representation);
|
||||
|
||||
// Create a new container or resource in the parent container with a specific name based on the incoming headers.
|
||||
return this.handleCreation(path, newIdentifier, path.endsWith('/'), isContainer ?
|
||||
undefined :
|
||||
representation.data, representation.metadata.raw, path.endsWith('/'));
|
||||
return this.handleCreation(path, newIdentifier, path.endsWith('/'), path.endsWith('/'), isContainer, representation
|
||||
.data, representation.metadata.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +87,7 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
const URI = identifier.path;
|
||||
const type = await this.getSparqlResourceType(URI);
|
||||
if (type === LINK_TYPE_LDPR) {
|
||||
await this.deleteSparqlResource(URI);
|
||||
await this.deleteSparqlDocument(URI);
|
||||
} else if (type === LINK_TYPE_LDPC) {
|
||||
await this.deleteSparqlContainer(URI);
|
||||
} else {
|
||||
@@ -106,10 +121,16 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
* @param identifier - Identifier of resource to update.
|
||||
* @param patch - Description of which parts to update.
|
||||
*/
|
||||
public async modifyResource(identifier: ResourceIdentifier, patch: Patch): Promise<void> {
|
||||
public async modifyResource(): Promise<void> {
|
||||
throw new Error('This has not yet been fully implemented correctly.');
|
||||
|
||||
// The incoming SPARQL query (patch.data) still needs to be modified to work on the graph that corresponds to the
|
||||
// identifier!
|
||||
return this.sendSparqlUpdate(await readableToString(patch.data));
|
||||
// if (patch.metadata.contentType !== CONTENT_TYPE_SPARQL_UPDATE || !('algebra' in patch)) {
|
||||
// throw new UnsupportedMediaTypeHttpError('This ResourceStore only supports SPARQL UPDATE data.');
|
||||
// }
|
||||
// const { data } = patch;
|
||||
// return this.sendSparqlUpdate(await readableToString(data));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,14 +139,16 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
* @param representation - New Representation.
|
||||
*/
|
||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
|
||||
// Check if the representation has a valid dataType.
|
||||
this.ensureValidDataType(representation);
|
||||
|
||||
// Get the expected behaviour based on the incoming identifier and representation.
|
||||
const { isContainer, path, newIdentifier } = this.resourceStoreController.getBehaviourSetRepresentation(identifier,
|
||||
representation);
|
||||
|
||||
// Create a new container or resource in the parent container with a specific name based on the incoming headers.
|
||||
await this.handleCreation(path, newIdentifier, true, isContainer ?
|
||||
undefined :
|
||||
representation.data, representation.metadata.raw, false);
|
||||
await this.handleCreation(path, newIdentifier, true, false, isContainer, representation.data, representation
|
||||
.metadata.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,17 +157,18 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
* @param path - The stripped path without the base of the store.
|
||||
* @param newIdentifier - The name of the resource to be created or overwritten.
|
||||
* @param allowRecursiveCreation - Whether necessary but not existing intermediate containers may be created.
|
||||
* @param isContainer - Whether a new container or a resource should be created based on the given parameters.
|
||||
* @param data - Data of the resource. None for a container.
|
||||
* @param metadata - Optional metadata to be stored in the metadata graph.
|
||||
* @param overwriteMetadata - Whether metadata for an already existing container may be overwritten with the provided
|
||||
* metadata.
|
||||
* @param metadata - Optional metadata to be stored in the metadata graph.
|
||||
*/
|
||||
private async handleCreation(path: string, newIdentifier: string, allowRecursiveCreation: boolean,
|
||||
data?: Readable, metadata?: Quad[], overwriteMetadata = false): Promise<ResourceIdentifier> {
|
||||
overwriteMetadata: boolean, isContainer: boolean, data?: Readable, metadata?: Quad[]): Promise<ResourceIdentifier> {
|
||||
await this.ensureValidContainerPath(path, allowRecursiveCreation);
|
||||
const URI = `${this.baseRequestURI}${ensureTrailingSlash(path)}${newIdentifier}`;
|
||||
return typeof data === 'undefined' ?
|
||||
await this.handleContainerCreation(URI, metadata, overwriteMetadata) :
|
||||
return isContainer || typeof data === 'undefined' ?
|
||||
await this.handleContainerCreation(URI, overwriteMetadata, metadata) :
|
||||
await this.handleResourceCreation(URI, data, metadata);
|
||||
}
|
||||
|
||||
@@ -170,14 +194,14 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
/**
|
||||
* Helper function to create a container.
|
||||
* @param containerURI - The URI of the container.
|
||||
* @param metadata - Optional metadata to be stored in the metadata graph.
|
||||
* @param overwriteMetadata - Whether metadata may be overwritten with the provided metadata if the container already
|
||||
* exists.
|
||||
* @param metadata - Optional metadata to be stored in the metadata graph.
|
||||
*
|
||||
* @throws {@link ConflictHttpError}
|
||||
* If a resource or container with that identifier already exists.
|
||||
*/
|
||||
private async handleContainerCreation(containerURI: string, metadata?: Quad[], overwriteMetadata = false):
|
||||
private async handleContainerCreation(containerURI: string, overwriteMetadata: boolean, metadata?: Quad[]):
|
||||
Promise<ResourceIdentifier> {
|
||||
const type = await this.getSparqlResourceType(containerURI);
|
||||
if (type === LINK_TYPE_LDPR) {
|
||||
@@ -205,11 +229,11 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
*/
|
||||
private async ensureValidContainerPath(path: string, allowRecursiveCreation: boolean): Promise<void> {
|
||||
const parentContainers = path.split('/').filter((container): any => container);
|
||||
let currentContainerURI = `${this.baseRequestURI}/`;
|
||||
let currentContainerURI = ensureTrailingSlash(this.baseRequestURI);
|
||||
|
||||
// Check each intermediate container one by one.
|
||||
while (parentContainers.length) {
|
||||
currentContainerURI = `${currentContainerURI}${parentContainers.shift()}/`;
|
||||
currentContainerURI = ensureTrailingSlash(`${currentContainerURI}${parentContainers.shift()}`);
|
||||
const type = await this.getSparqlResourceType(currentContainerURI);
|
||||
if (typeof type === 'undefined') {
|
||||
if (allowRecursiveCreation) {
|
||||
@@ -231,39 +255,33 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
*/
|
||||
private async getSparqlResourceType(URI: string): Promise<string | undefined> {
|
||||
// Check for container first, because a container also contains ldp:Resource.
|
||||
const containerQuery = {
|
||||
queryType: 'ASK',
|
||||
const typeQuery = {
|
||||
queryType: 'SELECT',
|
||||
variables: [ new Wildcard() ],
|
||||
where: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(`${ensureTrailingSlash(URI)}.metadata`),
|
||||
triples: [
|
||||
quad(variable('p'), TYPE_PREDICATE, CONTAINER_OBJECT),
|
||||
type: 'union',
|
||||
patterns: [
|
||||
this.generateGraphObject(`${ensureTrailingSlash(URI)}.metadata`,
|
||||
[ quad(namedNode(ensureTrailingSlash(URI)), TYPE_PREDICATE, variable('type')) ]),
|
||||
this.generateGraphObject(`${trimTrailingSlashes(URI)}.metadata`,
|
||||
[ quad(namedNode(trimTrailingSlashes(URI)), TYPE_PREDICATE, variable('type')) ]),
|
||||
],
|
||||
},
|
||||
],
|
||||
type: 'query',
|
||||
} as unknown as AskQuery;
|
||||
if ((await this.sendSparqlQuery(containerQuery)).boolean === true) {
|
||||
return LINK_TYPE_LDPC;
|
||||
}
|
||||
} as unknown as SelectQuery;
|
||||
|
||||
// Check that the URI matches a resource, if it was not a container.
|
||||
const resourceQuery = {
|
||||
queryType: 'ASK',
|
||||
where: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(`${trimTrailingSlashes(URI)}.metadata`),
|
||||
triples: [
|
||||
quad(variable('p'), TYPE_PREDICATE, RESOURCE_OBJECT),
|
||||
],
|
||||
},
|
||||
],
|
||||
type: 'query',
|
||||
} as unknown as AskQuery;
|
||||
if ((await this.sendSparqlQuery(resourceQuery)).boolean === true) {
|
||||
return LINK_TYPE_LDPR;
|
||||
const result = await this.sendSparqlQuery(typeQuery);
|
||||
if (result && result.results && result.results.bindings) {
|
||||
const types = new Set(result.results.bindings
|
||||
.map((obj: { type: { value: any } }): any => obj.type.value));
|
||||
if (types.has(LINK_TYPE_LDPC)) {
|
||||
return LINK_TYPE_LDPC;
|
||||
}
|
||||
if (types.has(LINK_TYPE_LDPR)) {
|
||||
return LINK_TYPE_LDPR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +293,7 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
private async createContainer(containerURI: string, metadata?: Quad[]): Promise<void> {
|
||||
// Verify the metadata quads to be saved and get the URI from the parent container.
|
||||
const metadataQuads = this.ensureValidQuads('metadata', metadata);
|
||||
const parentContainerURI = this.getParentContainer(containerURI);
|
||||
const parentContainerURI = (await this.containerManager.getContainer({ path: containerURI })).path;
|
||||
|
||||
// First create containerURI/.metadata graph with `containerURI a ldp:Container, ldp:Resource` and metadata triples.
|
||||
// Then create containerURI graph with `containerURI contains containerURI/.metadata` triple.
|
||||
@@ -285,29 +303,15 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
{
|
||||
updateType: 'insert',
|
||||
insert: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(`${containerURI}.metadata`),
|
||||
triples: [
|
||||
quad(namedNode(containerURI), TYPE_PREDICATE, CONTAINER_OBJECT),
|
||||
quad(namedNode(containerURI), TYPE_PREDICATE, RESOURCE_OBJECT),
|
||||
...metadataQuads,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(containerURI),
|
||||
triples: [
|
||||
quad(namedNode(containerURI), CONTAINS_PREDICATE, namedNode(`${containerURI}.metadata`)),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(parentContainerURI),
|
||||
triples: [
|
||||
quad(namedNode(parentContainerURI), CONTAINS_PREDICATE, namedNode(containerURI)),
|
||||
],
|
||||
},
|
||||
this.generateGraphObject(`${containerURI}.metadata`, [
|
||||
quad(namedNode(containerURI), TYPE_PREDICATE, CONTAINER_OBJECT),
|
||||
quad(namedNode(containerURI), TYPE_PREDICATE, RESOURCE_OBJECT),
|
||||
...metadataQuads,
|
||||
]),
|
||||
this.generateGraphObject(containerURI,
|
||||
[ quad(namedNode(containerURI), CONTAINS_PREDICATE, namedNode(`${containerURI}.metadata`)) ]),
|
||||
this.generateGraphObject(parentContainerURI,
|
||||
[ quad(namedNode(parentContainerURI), CONTAINS_PREDICATE, namedNode(containerURI)) ]),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -329,34 +333,15 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
updates: [
|
||||
{
|
||||
updateType: 'insertdelete',
|
||||
delete: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(`${containerURI}.metadata`),
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
],
|
||||
insert: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(`${containerURI}.metadata`),
|
||||
triples: [
|
||||
quad(namedNode(containerURI), TYPE_PREDICATE, CONTAINER_OBJECT),
|
||||
quad(namedNode(containerURI), TYPE_PREDICATE, RESOURCE_OBJECT),
|
||||
...metadata,
|
||||
],
|
||||
},
|
||||
],
|
||||
where: [
|
||||
{
|
||||
type: 'bgp',
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
],
|
||||
delete: [ this.generateGraphObject(`${containerURI}.metadata`,
|
||||
[ quad(variable('s'), variable('p'), variable('o')) ]) ],
|
||||
insert: [ this.generateGraphObject(`${containerURI}.metadata`, [
|
||||
quad(namedNode(containerURI), TYPE_PREDICATE, CONTAINER_OBJECT),
|
||||
quad(namedNode(containerURI), TYPE_PREDICATE, RESOURCE_OBJECT),
|
||||
...metadata,
|
||||
]) ],
|
||||
where: [ this.generateGraphObject(`${containerURI}.metadata`,
|
||||
[ quad(variable('s'), variable('p'), variable('o')) ]) ],
|
||||
},
|
||||
],
|
||||
type: 'update',
|
||||
@@ -387,53 +372,18 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
{
|
||||
updateType: 'insertdelete',
|
||||
delete: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(`${resourceURI}.metadata`),
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(resourceURI),
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
this.generateGraphObject(`${resourceURI}.metadata`,
|
||||
[ quad(variable('s'), variable('p'), variable('o')) ]),
|
||||
this.generateGraphObject(resourceURI, [ quad(variable('s'), variable('p'), variable('o')) ]),
|
||||
],
|
||||
insert: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(`${resourceURI}.metadata`),
|
||||
triples: [
|
||||
quad(namedNode(resourceURI), TYPE_PREDICATE, RESOURCE_OBJECT),
|
||||
...metadataQuads,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(resourceURI),
|
||||
triples: [
|
||||
...dataQuads,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(containerURI),
|
||||
triples: [
|
||||
quad(namedNode(containerURI), CONTAINS_PREDICATE, namedNode(resourceURI)),
|
||||
],
|
||||
},
|
||||
],
|
||||
where: [
|
||||
{
|
||||
type: 'bgp',
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
this.generateGraphObject(`${resourceURI}.metadata`,
|
||||
[ quad(namedNode(resourceURI), TYPE_PREDICATE, RESOURCE_OBJECT), ...metadataQuads ]),
|
||||
this.generateGraphObject(resourceURI, [ ...dataQuads ]),
|
||||
this.generateGraphObject(containerURI,
|
||||
[ quad(namedNode(containerURI), CONTAINS_PREDICATE, namedNode(resourceURI)) ]),
|
||||
],
|
||||
where: [{ type: 'bgp', triples: [ quad(variable('s'), variable('p'), variable('o')) ]}],
|
||||
},
|
||||
],
|
||||
type: 'update',
|
||||
@@ -443,57 +393,14 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to delete a resource.
|
||||
* Helper function to delete a document resource.
|
||||
* @param resourceURI - Identifier of resource to delete.
|
||||
*/
|
||||
private async deleteSparqlResource(resourceURI: string): Promise<void> {
|
||||
private async deleteSparqlDocument(resourceURI: string): Promise<void> {
|
||||
// Get the container URI that contains the resource corresponding to the URI.
|
||||
const containerURI = ensureTrailingSlash(resourceURI.slice(0, resourceURI.lastIndexOf('/')));
|
||||
|
||||
// First remove `resourceURI/.metadata` graph. Then remove resourceURI graph and finally remove
|
||||
// `containerURI contains resourceURI` triple.
|
||||
const deleteResourceQuery = {
|
||||
updates: [
|
||||
{
|
||||
updateType: 'insertdelete',
|
||||
delete: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(`${resourceURI}.metadata`),
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(resourceURI),
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(containerURI),
|
||||
triples: [
|
||||
quad(namedNode(containerURI), CONTAINS_PREDICATE, namedNode(resourceURI)),
|
||||
],
|
||||
},
|
||||
],
|
||||
insert: [],
|
||||
where: [
|
||||
{
|
||||
type: 'bgp',
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
type: 'update',
|
||||
prefixes: {},
|
||||
} as Update;
|
||||
return this.sendSparqlUpdate(deleteResourceQuery);
|
||||
return this.deleteSparqlResource(containerURI, resourceURI);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -507,46 +414,33 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
}
|
||||
|
||||
// Get the parent container from the specified container to remove the containment triple.
|
||||
const parentContainerURI = this.getParentContainer(containerURI);
|
||||
const parentContainerURI = (await this.containerManager.getContainer({ path: containerURI })).path;
|
||||
|
||||
// First remove `containerURI/.metadata` graph. Then remove containerURI graph and finally remove
|
||||
// `parentContainerURI contains containerURI` triple from parentContainerURI graph.
|
||||
return this.deleteSparqlResource(parentContainerURI, containerURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function without extra validation to delete a container resource.
|
||||
* @param parentURI - Identifier of parent container to delete.
|
||||
* @param childURI - Identifier of container or resource to delete.
|
||||
*/
|
||||
private async deleteSparqlResource(parentURI: string, childURI: string): Promise<void> {
|
||||
// First remove `childURI/.metadata` graph. Then remove childURI graph and finally remove
|
||||
// `parentURI contains childURI` triple from parentURI graph.
|
||||
const deleteContainerQuery = {
|
||||
updates: [
|
||||
{
|
||||
updateType: 'insertdelete',
|
||||
delete: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(`${containerURI}.metadata`),
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(containerURI),
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(parentContainerURI),
|
||||
triples: [
|
||||
quad(namedNode(parentContainerURI), CONTAINS_PREDICATE, namedNode(containerURI)),
|
||||
],
|
||||
},
|
||||
this.generateGraphObject(`${childURI}.metadata`,
|
||||
[ quad(variable('s'), variable('p'), variable('o')) ]),
|
||||
this.generateGraphObject(childURI,
|
||||
[ quad(variable('s'), variable('p'), variable('o')) ]),
|
||||
this.generateGraphObject(parentURI,
|
||||
[ quad(namedNode(parentURI), CONTAINS_PREDICATE, namedNode(childURI)) ]),
|
||||
],
|
||||
insert: [],
|
||||
where: [
|
||||
{
|
||||
type: 'bgp',
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
],
|
||||
where: [{ type: 'bgp', triples: [ quad(variable('s'), variable('p'), variable('o')) ]}],
|
||||
},
|
||||
],
|
||||
type: 'update',
|
||||
@@ -564,24 +458,17 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
const containerQuery = {
|
||||
queryType: 'ASK',
|
||||
where: [
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(containerURI),
|
||||
triples: [
|
||||
quad(namedNode(containerURI), CONTAINS_PREDICATE, variable('o')),
|
||||
{
|
||||
type: 'filter',
|
||||
expression: {
|
||||
type: 'operation',
|
||||
operator: '!=',
|
||||
args: [
|
||||
variable('o'),
|
||||
namedNode(`${containerURI}.metadata`),
|
||||
],
|
||||
},
|
||||
this.generateGraphObject(containerURI, [
|
||||
quad(namedNode(containerURI), CONTAINS_PREDICATE, variable('o')),
|
||||
{
|
||||
type: 'filter',
|
||||
expression: {
|
||||
type: 'operation',
|
||||
operator: '!=',
|
||||
args: [ variable('o'), namedNode(`${containerURI}.metadata`) ],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
type: 'query',
|
||||
} as unknown as AskQuery;
|
||||
@@ -603,14 +490,7 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
{
|
||||
type: 'graph',
|
||||
name: namedNode(URI),
|
||||
patterns: [
|
||||
{
|
||||
type: 'bgp',
|
||||
triples: [
|
||||
quad(variable('s'), variable('p'), variable('o')),
|
||||
],
|
||||
},
|
||||
],
|
||||
patterns: [{ type: 'bgp', triples: [ quad(variable('s'), variable('p'), variable('o')) ]}],
|
||||
} as GraphPattern,
|
||||
],
|
||||
type: 'query',
|
||||
@@ -630,14 +510,8 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
|
||||
// Only include the triples of the resource graph in the data readable.
|
||||
const readableData = streamifyArray([ ...data ]);
|
||||
return {
|
||||
dataType: DATA_TYPE_QUAD,
|
||||
data: readableData,
|
||||
metadata: {
|
||||
raw: metadata,
|
||||
contentType: CONTENT_TYPE_QUADS,
|
||||
},
|
||||
};
|
||||
|
||||
return this.generateReturningRepresentation(readableData, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -649,16 +523,11 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
const data: Quad[] = await this.getSparqlRepresentation(containerURI);
|
||||
const metadata: Quad[] = await this.getSparqlRepresentation(`${containerURI}.metadata`);
|
||||
|
||||
// Include both the triples of the resource graph and the metadata graph in the data readable.
|
||||
// Include both the triples of the resource graph and the metadata graph in the data readable to be consistent with
|
||||
// the existing solid implementation.
|
||||
const readableData = streamifyArray([ ...data, ...metadata ]);
|
||||
return {
|
||||
dataType: DATA_TYPE_QUAD,
|
||||
data: readableData,
|
||||
metadata: {
|
||||
raw: metadata,
|
||||
contentType: CONTENT_TYPE_QUADS,
|
||||
},
|
||||
};
|
||||
|
||||
return this.generateReturningRepresentation(readableData, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -681,15 +550,45 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the parent container URI of a container URI.
|
||||
* @param containerURI - Incoming container URI.
|
||||
* Check if the representation has a valid dataType.
|
||||
* @param representation - Incoming Representation.
|
||||
*
|
||||
* @throws {@link UnsupportedMediaTypeHttpError}
|
||||
* If the incoming dataType does not match the store's supported dataType.
|
||||
*/
|
||||
private getParentContainer(containerURI: string): string {
|
||||
const [ , parentContainerURI ] = /^(.*\/)[^/]+\/$/u.exec(containerURI) ?? [];
|
||||
if (typeof parentContainerURI !== 'string') {
|
||||
throw new Error('Invalid containerURI passed.');
|
||||
private ensureValidDataType(representation: Representation): void {
|
||||
if (representation.dataType !== DATA_TYPE_QUAD) {
|
||||
throw new UnsupportedMediaTypeHttpError('The SparqlResourceStore only supports quad representations.');
|
||||
}
|
||||
return parentContainerURI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a graph object from his URI and triples.
|
||||
* @param URI - URI of the graph.
|
||||
* @param triples - Triples of the graph.
|
||||
*/
|
||||
private generateGraphObject(URI: string, triples: any): any {
|
||||
return {
|
||||
type: 'graph',
|
||||
name: namedNode(URI),
|
||||
triples,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the resulting Representation.
|
||||
* @param readable - Outgoing data.
|
||||
* @param quads - Outgoing metadata.
|
||||
*/
|
||||
private generateReturningRepresentation(readable: Readable, quads: Quad[]): Representation {
|
||||
return {
|
||||
dataType: DATA_TYPE_QUAD,
|
||||
data: readable,
|
||||
metadata: {
|
||||
raw: quads,
|
||||
contentType: CONTENT_TYPE_QUADS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -714,9 +613,7 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
const response = await fetch(request, init);
|
||||
|
||||
// Check if the server returned an error and return the json representation of the result.
|
||||
if (response.status >= 400) {
|
||||
throw new Error('Bad response from server');
|
||||
}
|
||||
this.handleServerResponseStatus(response);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@@ -746,8 +643,19 @@ export class SparqlResourceStore implements ResourceStore {
|
||||
const response = await fetch(request, init);
|
||||
|
||||
// Check if the server returned an error.
|
||||
this.handleServerResponseStatus(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the server returned an error.
|
||||
* @param response - Response from the server.
|
||||
*
|
||||
* @throws {@link Error}
|
||||
* If the server returned an error.
|
||||
*/
|
||||
private handleServerResponseStatus(response: Response): void {
|
||||
if (response.status >= 400) {
|
||||
throw new Error('Bad response from server');
|
||||
throw new Error(`Bad response from server: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifie
|
||||
import { ConflictHttpError } from './errors/ConflictHttpError';
|
||||
import { MethodNotAllowedHttpError } from './errors/MethodNotAllowedHttpError';
|
||||
import { NotFoundHttpError } from './errors/NotFoundHttpError';
|
||||
import { UnsupportedMediaTypeHttpError } from './errors/UnsupportedMediaTypeHttpError';
|
||||
import type { InteractionController } from './InteractionController';
|
||||
import { ensureTrailingSlash, trimTrailingSlashes } from './Util';
|
||||
|
||||
@@ -27,19 +26,15 @@ export interface SetBehaviour {
|
||||
export class ResourceStoreController {
|
||||
private readonly baseRequestURI: string;
|
||||
private readonly interactionController: InteractionController;
|
||||
private readonly supportedDataTypes: Set<string>;
|
||||
|
||||
/**
|
||||
* @param baseRequestURI - The base from the store. Will be stripped of all incoming URIs and added to all outgoing
|
||||
* ones to find the relative path.
|
||||
* @param interactionController - Instance of InteractionController to use.
|
||||
* @param supportedDataTypes - All supported data types by the store.
|
||||
*/
|
||||
public constructor(baseRequestURI: string, interactionController: InteractionController,
|
||||
supportedDataTypes: Set<string>) {
|
||||
public constructor(baseRequestURI: string, interactionController: InteractionController) {
|
||||
this.baseRequestURI = trimTrailingSlashes(baseRequestURI);
|
||||
this.interactionController = interactionController;
|
||||
this.supportedDataTypes = supportedDataTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,12 +72,6 @@ export class ResourceStoreController {
|
||||
* @param representation - Incoming representation.
|
||||
*/
|
||||
public getBehaviourAddResource(container: ResourceIdentifier, representation: Representation): SetBehaviour {
|
||||
// Throw an error if the data type is not supported by the store.
|
||||
if (!this.supportedDataTypes.has(representation.dataType)) {
|
||||
throw new UnsupportedMediaTypeHttpError(`This ResourceStore only supports
|
||||
${[ ...this.supportedDataTypes ].join(', ')} representations.`);
|
||||
}
|
||||
|
||||
// Get the path from the request URI, and the Slug and Link header values.
|
||||
const path = this.parseIdentifier(container);
|
||||
const { slug } = representation.metadata;
|
||||
@@ -100,15 +89,9 @@ export class ResourceStoreController {
|
||||
* @param representation - Incoming representation.
|
||||
*/
|
||||
public getBehaviourSetRepresentation(identifier: ResourceIdentifier, representation: Representation): SetBehaviour {
|
||||
// Throw an error if the data type is not supported by the store.
|
||||
if (!this.supportedDataTypes.has(representation.dataType)) {
|
||||
throw new UnsupportedMediaTypeHttpError(`This ResourceStore only supports
|
||||
${[ ...this.supportedDataTypes ].join(', ')} representations.`);
|
||||
}
|
||||
|
||||
// Break up the request URI in the different parts `path` and `slug` as we know their semantics from addResource
|
||||
// to call the InteractionController in the same way.
|
||||
const [ , path, slug ] = /^(.*\/)([^/]+\/?)?$/u.exec(this.parseIdentifier(identifier)) ?? [];
|
||||
const [ , path, slug ] = /^(.*\/)([^/]+\/?)$/u.exec(this.parseIdentifier(identifier)) ?? [];
|
||||
if ((typeof path !== 'string' || ensureTrailingSlash(path) === '/') && typeof slug !== 'string') {
|
||||
throw new ConflictHttpError('Container with that identifier already exists (root).');
|
||||
}
|
||||
|
||||
450
test/unit/storage/SparqlResourceStore.test.ts
Normal file
450
test/unit/storage/SparqlResourceStore.test.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
import { Readable } from 'stream';
|
||||
import { namedNode, triple } from '@rdfjs/data-model';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { DataFactory } from 'n3';
|
||||
import streamifyArray from 'streamify-array';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { QuadRepresentation } from '../../../src/ldp/representation/QuadRepresentation';
|
||||
import type { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
||||
import { SparqlResourceStore } from '../../../src/storage/SparqlResourceStore';
|
||||
import { UrlContainerManager } from '../../../src/storage/UrlContainerManager';
|
||||
import {
|
||||
CONTENT_TYPE_QUADS,
|
||||
DATA_TYPE_BINARY,
|
||||
DATA_TYPE_QUAD,
|
||||
} from '../../../src/util/ContentTypes';
|
||||
import { ConflictHttpError } from '../../../src/util/errors/ConflictHttpError';
|
||||
import { MethodNotAllowedHttpError } from '../../../src/util/errors/MethodNotAllowedHttpError';
|
||||
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
||||
import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
||||
import { InteractionController } from '../../../src/util/InteractionController';
|
||||
import { LINK_TYPE_LDP_BC, LINK_TYPE_LDPC, LINK_TYPE_LDPR } from '../../../src/util/LinkTypes';
|
||||
import { CONTAINS_PREDICATE } from '../../../src/util/MetadataController';
|
||||
import { ResourceStoreController } from '../../../src/util/ResourceStoreController';
|
||||
|
||||
const base = 'http://test.com/';
|
||||
const sparqlEndpoint = 'http://localhost:8889/bigdata/sparql';
|
||||
|
||||
jest.mock('cross-fetch');
|
||||
jest.mock('uuid');
|
||||
|
||||
describe('A SparqlResourceStore', (): void => {
|
||||
let store: SparqlResourceStore;
|
||||
let representation: QuadRepresentation;
|
||||
let spyOnSparqlResourceType: jest.SpyInstance<any, unknown[]>;
|
||||
|
||||
const quad = triple(
|
||||
namedNode('http://test.com/s'),
|
||||
namedNode('http://test.com/p'),
|
||||
namedNode('http://test.com/o'),
|
||||
);
|
||||
|
||||
const metadata = [ triple(
|
||||
namedNode('http://test.com/container'),
|
||||
CONTAINS_PREDICATE,
|
||||
namedNode('http://test.com/resource'),
|
||||
) ];
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
store = new SparqlResourceStore(base, sparqlEndpoint, new ResourceStoreController(base,
|
||||
new InteractionController()), new UrlContainerManager(base));
|
||||
|
||||
representation = {
|
||||
data: streamifyArray([ quad ]),
|
||||
dataType: DATA_TYPE_QUAD,
|
||||
metadata: { raw: [], linkRel: { type: new Set() }} as RepresentationMetadata,
|
||||
};
|
||||
|
||||
spyOnSparqlResourceType = jest.spyOn(store as any, `getSparqlResourceType`);
|
||||
(uuid as jest.Mock).mockReturnValue('rand-om-st-ring');
|
||||
});
|
||||
|
||||
/**
|
||||
* Create the mocked return values for the getSparqlResourceType function.
|
||||
* @param isContainer - Whether the mock should imitate a container.
|
||||
* @param isResource - Whether the mock should imitate a resource.
|
||||
*/
|
||||
const mockResourceType = function(isContainer: boolean, isResource: boolean): void {
|
||||
let jsonResult: any;
|
||||
if (isContainer) {
|
||||
jsonResult = { results: { bindings: [{ type: { type: 'uri', value: LINK_TYPE_LDPC }}]}};
|
||||
} else if (isResource) {
|
||||
jsonResult = { results: { bindings: [{ type: { type: 'uri', value: LINK_TYPE_LDPR }}]}};
|
||||
}
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200, json: (): any => jsonResult } as
|
||||
unknown as Response);
|
||||
};
|
||||
|
||||
it('errors if a resource was not found.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
mockResourceType(false, false);
|
||||
const jsonResult = { results: { bindings: [{ type: { type: 'uri', value: 'unknown' }}]}};
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200, json: (): any => jsonResult } as
|
||||
unknown as Response);
|
||||
|
||||
// Tests
|
||||
await expect(store.getRepresentation({ path: `${base}wrong` })).rejects.toThrow(NotFoundHttpError);
|
||||
await expect(store.addResource({ path: 'http://wrong.com/wrong' }, representation))
|
||||
.rejects.toThrow(NotFoundHttpError);
|
||||
await expect(store.deleteResource({ path: 'wrong' })).rejects.toThrow(NotFoundHttpError);
|
||||
await expect(store.deleteResource({ path: `${base}wrong` })).rejects.toThrow(NotFoundHttpError);
|
||||
await expect(store.setRepresentation({ path: 'http://wrong.com/' }, representation))
|
||||
.rejects.toThrow(NotFoundHttpError);
|
||||
});
|
||||
|
||||
it('(passes the SPARQL query to the endpoint for a PATCH request) errors for modifyResource.',
|
||||
async(): Promise<void> => {
|
||||
await expect(store.modifyResource()).rejects.toThrow(Error);
|
||||
|
||||
// Temporary test to get the 100% coverage for already implemented but unused behaviour in sendSparqlUpdate,
|
||||
// because an error is thrown for now.
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
const sparql = 'INSERT DATA { GRAPH <https://example.com/foo/> { <https://example.com/foo/> <http://www.w3.org/ns/ldp#contains> <https://example.com/foo/.metadata>. } }';
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(await store.sendSparqlUpdate(sparql)).toBeUndefined();
|
||||
|
||||
// // Mock the cross-fetch functions.
|
||||
// (fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
//
|
||||
// // Tests
|
||||
// const sparql = 'INSERT DATA { GRAPH <https://example.com/foo/> { <https://example.com/foo/> <http://www.w3.org/ns/ldp#contains> <https://example.com/foo/.metadata>. } }';
|
||||
// const algebra = translate(sparql, { quads: true });
|
||||
// const patch = {
|
||||
// algebra,
|
||||
// dataType: DATA_TYPE_BINARY,
|
||||
// data: Readable.from(sparql),
|
||||
// metadata: {
|
||||
// raw: [],
|
||||
// profiles: [],
|
||||
// contentType: CONTENT_TYPE_SPARQL_UPDATE,
|
||||
// },
|
||||
// };
|
||||
// await store.modifyResource({ path: `${base}foo` }, patch);
|
||||
// const init = {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': CONTENT_TYPE_SPARQL_UPDATE,
|
||||
// },
|
||||
// body: sparql,
|
||||
// };
|
||||
// expect(fetch as jest.Mock).toBeCalledWith(new Request(sparqlEndpoint), init);
|
||||
// expect(fetch as jest.Mock).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('errors for wrong input data types.', async(): Promise<void> => {
|
||||
(representation as any).dataType = DATA_TYPE_BINARY;
|
||||
await expect(store.addResource({ path: base }, representation)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
||||
await expect(store.setRepresentation({ path: `${base}foo` }, representation)).rejects
|
||||
.toThrow(UnsupportedMediaTypeHttpError);
|
||||
|
||||
// This has not yet been fully implemented correctly.
|
||||
// const patch = {
|
||||
// dataType: DATA_TYPE_QUAD,
|
||||
// data: streamifyArray([ quad ]),
|
||||
// metadata: {
|
||||
// raw: [],
|
||||
// profiles: [],
|
||||
// contentType: CONTENT_TYPE_QUADS,
|
||||
// },
|
||||
// };
|
||||
// await expect(store.modifyResource({ path: `${base}foo` }, patch)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
||||
});
|
||||
|
||||
it('can write and read data.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
// Add
|
||||
mockResourceType(true, false);
|
||||
mockResourceType(false, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
|
||||
// Mock: Get
|
||||
mockResourceType(false, true);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200, json: (): any => ({ results: { bindings: [ quad ]}}) } as
|
||||
unknown as Response);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200, json: (): any => ({ results: { bindings: metadata }}) } as
|
||||
unknown as Response);
|
||||
|
||||
// Tests
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: []};
|
||||
const identifier = await store.addResource({ path: `${base}foo/` }, representation);
|
||||
expect(identifier.path).toBe(`${base}foo/rand-om-st-ring`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}foo/`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(identifier.path);
|
||||
|
||||
const result = await store.getRepresentation(identifier);
|
||||
expect(result).toEqual({
|
||||
dataType: representation.dataType,
|
||||
data: expect.any(Readable),
|
||||
metadata: {
|
||||
raw: metadata,
|
||||
contentType: CONTENT_TYPE_QUADS,
|
||||
},
|
||||
});
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(identifier.path);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(3);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(6);
|
||||
await expect(arrayifyStream(result.data)).resolves.toEqual([ quad ]);
|
||||
});
|
||||
|
||||
it('errors for container creation with path to non container.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
mockResourceType(false, true);
|
||||
|
||||
// Tests
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []};
|
||||
await expect(store.addResource({ path: `${base}foo` }, representation)).rejects.toThrow(MethodNotAllowedHttpError);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(1);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}foo/`);
|
||||
});
|
||||
|
||||
it('errors 405 for POST invalid path ending without slash.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
mockResourceType(false, false);
|
||||
mockResourceType(false, false);
|
||||
mockResourceType(false, true);
|
||||
|
||||
// Tests
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: []};
|
||||
await expect(store.addResource({ path: `${base}doesnotexist` }, representation))
|
||||
.rejects.toThrow(MethodNotAllowedHttpError);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}doesnotexist/`);
|
||||
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: []};
|
||||
await expect(store.addResource({ path: `${base}doesnotexist` }, representation))
|
||||
.rejects.toThrow(MethodNotAllowedHttpError);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}doesnotexist/`);
|
||||
|
||||
representation.metadata = { linkRel: { type: new Set() }, slug: 'file.txt', raw: []};
|
||||
await expect(store.addResource({ path: `${base}existingresource` }, representation))
|
||||
.rejects.toThrow(MethodNotAllowedHttpError);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}existingresource/`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(3);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
it('can write and read a container.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
// Add
|
||||
mockResourceType(false, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
|
||||
// Mock: Get
|
||||
mockResourceType(true, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200, json: (): any => ({ results: { bindings: [ quad ]}}) } as
|
||||
unknown as Response);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200, json: (): any => ({ results: { bindings: metadata }}) } as
|
||||
unknown as Response);
|
||||
|
||||
// Write container (POST)
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, slug: 'myContainer/', raw: metadata };
|
||||
const identifier = await store.addResource({ path: base }, representation);
|
||||
expect(identifier.path).toBe(`${base}myContainer/`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}myContainer/`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(1);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(2);
|
||||
|
||||
// Read container
|
||||
const result = await store.getRepresentation(identifier);
|
||||
expect(result).toEqual({
|
||||
dataType: representation.dataType,
|
||||
data: expect.any(Readable),
|
||||
metadata: {
|
||||
raw: metadata,
|
||||
contentType: CONTENT_TYPE_QUADS,
|
||||
},
|
||||
});
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(identifier.path);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(2);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(5);
|
||||
await expect(arrayifyStream(result.data)).resolves.toEqual([ quad, ...metadata ]);
|
||||
});
|
||||
|
||||
it('can set data.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
const spyOnCreateResource = jest.spyOn(store as any, `createResource`);
|
||||
mockResourceType(false, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
|
||||
// Tests
|
||||
await store.setRepresentation({ path: `${base}file.txt` }, representation);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}file.txt`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(1);
|
||||
expect(spyOnCreateResource).toBeCalledWith(`${base}file.txt`, [ quad ], []);
|
||||
expect(spyOnCreateResource).toBeCalledTimes(1);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('can delete data.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
// Delete
|
||||
const spyOnDeleteSparqlDocument = jest.spyOn(store as any, `deleteSparqlDocument`);
|
||||
mockResourceType(false, true);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
|
||||
// Mock: Get
|
||||
mockResourceType(false, false);
|
||||
|
||||
// Tests
|
||||
await store.deleteResource({ path: `${base}file.txt` });
|
||||
expect(spyOnDeleteSparqlDocument).toBeCalledWith(`${base}file.txt`);
|
||||
expect(spyOnDeleteSparqlDocument).toBeCalledTimes(1);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}file.txt`);
|
||||
|
||||
await expect(store.getRepresentation({ path: `${base}file.txt` })).rejects.toThrow(NotFoundHttpError);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}file.txt`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('creates intermediate container when POSTing resource to path ending with slash.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
const spyOnCreateContainer = jest.spyOn(store as any, `createContainer`);
|
||||
const spyOnCreateResource = jest.spyOn(store as any, `createResource`);
|
||||
mockResourceType(false, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
mockResourceType(false, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
|
||||
// Tests
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, slug: 'file.txt', raw: []};
|
||||
const identifier = await store.addResource({ path: `${base}doesnotexistyet/` }, representation);
|
||||
expect(identifier.path).toBe(`${base}doesnotexistyet/file.txt`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}doesnotexistyet/`);
|
||||
expect(spyOnCreateContainer).toBeCalledWith(`${base}doesnotexistyet/`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}doesnotexistyet/file.txt`);
|
||||
expect(spyOnCreateResource).toBeCalledWith(`${base}doesnotexistyet/file.txt`, [ quad ], []);
|
||||
expect(spyOnCreateContainer).toBeCalledTimes(1);
|
||||
expect(spyOnCreateResource).toBeCalledTimes(1);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(2);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(4);
|
||||
});
|
||||
|
||||
it('errors when deleting root container.', async(): Promise<void> => {
|
||||
// Tests
|
||||
await expect(store.deleteResource({ path: base })).rejects.toThrow(MethodNotAllowedHttpError);
|
||||
});
|
||||
|
||||
it('errors when deleting non empty container.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
const spyOnIsEmptyContainer = jest.spyOn(store as any, `isEmptyContainer`);
|
||||
mockResourceType(true, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200, json: (): any => ({ boolean: true }) } as
|
||||
unknown as Response);
|
||||
|
||||
// Tests
|
||||
await expect(store.deleteResource({ path: `${base}notempty/` })).rejects.toThrow(ConflictHttpError);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}notempty/`);
|
||||
expect(spyOnIsEmptyContainer).toBeCalledWith(`${base}notempty/`);
|
||||
});
|
||||
|
||||
it('can overwrite representation with PUT.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
const spyOnCreateResource = jest.spyOn(store as any, `createResource`);
|
||||
mockResourceType(false, true);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
|
||||
// Tests
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: []};
|
||||
await store.setRepresentation({ path: `${base}alreadyexists.txt` }, representation);
|
||||
expect(spyOnCreateResource).toBeCalledWith(`${base}alreadyexists.txt`, [ quad ], []);
|
||||
expect(spyOnCreateResource).toBeCalledTimes(1);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(1);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('errors when overwriting container with PUT.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
mockResourceType(true, false);
|
||||
mockResourceType(false, true);
|
||||
mockResourceType(true, false);
|
||||
|
||||
// Tests
|
||||
await expect(store.setRepresentation({ path: `${base}alreadyexists` }, representation)).rejects
|
||||
.toThrow(ConflictHttpError);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}alreadyexists`);
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, raw: []};
|
||||
await expect(store.setRepresentation({ path: `${base}alreadyexists/` }, representation)).rejects
|
||||
.toThrow(ConflictHttpError);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}alreadyexists/`);
|
||||
await expect(store.setRepresentation({ path: `${base}alreadyexists/` }, representation)).rejects
|
||||
.toThrow(ConflictHttpError);
|
||||
expect(spyOnSparqlResourceType).toBeCalledWith(`${base}alreadyexists/`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(3);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
it('can overwrite container metadata with POST.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
const spyOnOverwriteContainerMetadata = jest.spyOn(store as any, `overwriteContainerMetadata`);
|
||||
mockResourceType(true, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
|
||||
// Tests
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) },
|
||||
raw: metadata,
|
||||
slug: 'alreadyexists/' };
|
||||
await store.addResource({ path: base }, representation);
|
||||
expect(spyOnOverwriteContainerMetadata).toBeCalledWith(`${base}alreadyexists/`, metadata);
|
||||
expect(spyOnOverwriteContainerMetadata).toBeCalledTimes(1);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(1);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('can delete empty container.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
const spyOnDeleteSparqlContainer = jest.spyOn(store as any, `deleteSparqlContainer`);
|
||||
mockResourceType(true, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200, json: (): any => ({ boolean: false }) } as
|
||||
unknown as Response);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
|
||||
// Tests
|
||||
await store.deleteResource({ path: `${base}foo/` });
|
||||
expect(spyOnDeleteSparqlContainer).toBeCalledWith(`${base}foo/`);
|
||||
expect(spyOnDeleteSparqlContainer).toBeCalledTimes(1);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(1);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
it('errors when passing quads not in the default graph.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
mockResourceType(false, false);
|
||||
|
||||
// Tests
|
||||
const namedGraphQuad = DataFactory.quad(
|
||||
namedNode('http://test.com/s'),
|
||||
namedNode('http://test.com/p'),
|
||||
namedNode('http://test.com/o'),
|
||||
namedNode('http://test.com/g'),
|
||||
);
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: []};
|
||||
representation.data = streamifyArray([ namedGraphQuad ]);
|
||||
await expect(store.addResource({ path: base }, representation)).rejects.toThrow(ConflictHttpError);
|
||||
});
|
||||
|
||||
it('errors when getting bad response from server.', async(): Promise<void> => {
|
||||
// Mock the cross-fetch functions.
|
||||
mockResourceType(false, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 400 } as unknown as Response);
|
||||
|
||||
// Tests
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDPR ]) }, raw: []};
|
||||
await expect(store.setRepresentation({ path: `${base}foo.txt` }, representation)).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
it('creates container with random UUID when POSTing without slug header.', async(): Promise<void> => {
|
||||
// Mock the uuid and cross-fetch functions.
|
||||
mockResourceType(false, false);
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({ status: 200 } as unknown as Response);
|
||||
|
||||
// Tests
|
||||
representation.metadata = { linkRel: { type: new Set([ LINK_TYPE_LDP_BC ]) }, raw: []};
|
||||
const identifier = await store.addResource({ path: base }, representation);
|
||||
expect(identifier.path).toBe(`${base}rand-om-st-ring/`);
|
||||
expect(spyOnSparqlResourceType).toBeCalledTimes(1);
|
||||
expect(fetch as jest.Mock).toBeCalledTimes(2);
|
||||
expect(uuid as jest.Mock).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user