mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Integrate wrapStreamError to prevent uncaught errors
This commit is contained in:
parent
1a30b51461
commit
e4183333fd
@ -1,4 +1,3 @@
|
|||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import type { AclManager } from '../authorization/AclManager';
|
import type { AclManager } from '../authorization/AclManager';
|
||||||
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
||||||
import type { LoggerFactory } from '../logging/LoggerFactory';
|
import type { LoggerFactory } from '../logging/LoggerFactory';
|
||||||
@ -6,6 +5,7 @@ import { getLoggerFor, setGlobalLoggerFactory } from '../logging/LogUtil';
|
|||||||
import type { ExpressHttpServerFactory } from '../server/ExpressHttpServerFactory';
|
import type { ExpressHttpServerFactory } from '../server/ExpressHttpServerFactory';
|
||||||
import type { ResourceStore } from '../storage/ResourceStore';
|
import type { ResourceStore } from '../storage/ResourceStore';
|
||||||
import { TEXT_TURTLE } from '../util/ContentTypes';
|
import { TEXT_TURTLE } from '../util/ContentTypes';
|
||||||
|
import { guardedStreamFrom } from '../util/StreamUtil';
|
||||||
import { CONTENT_TYPE } from '../util/UriConstants';
|
import { CONTENT_TYPE } from '../util/UriConstants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,7 +65,7 @@ export class Setup {
|
|||||||
baseAclId,
|
baseAclId,
|
||||||
{
|
{
|
||||||
binary: true,
|
binary: true,
|
||||||
data: streamifyArray([ acl ]),
|
data: guardedStreamFrom([ acl ]),
|
||||||
metadata,
|
metadata,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
|
import type { Guarded } from '../../../util/GuardedStream';
|
||||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||||
import { ResponseDescription } from './ResponseDescription';
|
import { ResponseDescription } from './ResponseDescription';
|
||||||
|
|
||||||
@ -10,7 +11,7 @@ export class OkResponseDescription extends ResponseDescription {
|
|||||||
* @param metadata - Metadata concerning the response.
|
* @param metadata - Metadata concerning the response.
|
||||||
* @param data - Potential data. @ignored
|
* @param data - Potential data. @ignored
|
||||||
*/
|
*/
|
||||||
public constructor(metadata: RepresentationMetadata, data?: Readable) {
|
public constructor(metadata: RepresentationMetadata, data?: Guarded<Readable>) {
|
||||||
super(200, metadata, data);
|
super(200, metadata, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
|
import type { Guarded } from '../../../util/GuardedStream';
|
||||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -7,14 +8,14 @@ import type { RepresentationMetadata } from '../../representation/Representation
|
|||||||
export class ResponseDescription {
|
export class ResponseDescription {
|
||||||
public readonly statusCode: number;
|
public readonly statusCode: number;
|
||||||
public readonly metadata?: RepresentationMetadata;
|
public readonly metadata?: RepresentationMetadata;
|
||||||
public readonly data?: Readable;
|
public readonly data?: Guarded<Readable>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param statusCode - Status code to return.
|
* @param statusCode - Status code to return.
|
||||||
* @param metadata - Metadata corresponding to the response (and data potentially).
|
* @param metadata - Metadata corresponding to the response (and data potentially).
|
||||||
* @param data - Data that needs to be returned. @ignored
|
* @param data - Data that needs to be returned. @ignored
|
||||||
*/
|
*/
|
||||||
public constructor(statusCode: number, metadata?: RepresentationMetadata, data?: Readable) {
|
public constructor(statusCode: number, metadata?: RepresentationMetadata, data?: Guarded<Readable>) {
|
||||||
this.statusCode = statusCode;
|
this.statusCode = statusCode;
|
||||||
this.metadata = metadata;
|
this.metadata = metadata;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
|
import type { Guarded } from '../../util/GuardedStream';
|
||||||
import type { RepresentationMetadata } from './RepresentationMetadata';
|
import type { RepresentationMetadata } from './RepresentationMetadata';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -12,7 +13,7 @@ export interface Representation {
|
|||||||
/**
|
/**
|
||||||
* The raw data stream for this representation.
|
* The raw data stream for this representation.
|
||||||
*/
|
*/
|
||||||
data: Readable;
|
data: Guarded<Readable>;
|
||||||
/**
|
/**
|
||||||
* Whether the data stream consists of binary/string chunks
|
* Whether the data stream consists of binary/string chunks
|
||||||
* (as opposed to complex objects).
|
* (as opposed to complex objects).
|
||||||
|
@ -3,6 +3,7 @@ import cors from 'cors';
|
|||||||
import type { Express } from 'express';
|
import type { Express } from 'express';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
|
import { guardStream } from '../util/GuardedStream';
|
||||||
import type { HttpHandler } from './HttpHandler';
|
import type { HttpHandler } from './HttpHandler';
|
||||||
import type { HttpServerFactory } from './HttpServerFactory';
|
import type { HttpServerFactory } from './HttpServerFactory';
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ export class ExpressHttpServerFactory implements HttpServerFactory {
|
|||||||
app.use(async(request, response, done): Promise<void> => {
|
app.use(async(request, response, done): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
this.logger.info(`Received request for ${request.url}`);
|
this.logger.info(`Received request for ${request.url}`);
|
||||||
await this.handler.handleSafe({ request, response });
|
await this.handler.handleSafe({ request: guardStream(request), response });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errMsg = error instanceof Error ? `${error.name}: ${error.message}\n${error.stack}` : 'Unknown error.';
|
const errMsg = error instanceof Error ? `${error.name}: ${error.message}\n${error.stack}` : 'Unknown error.';
|
||||||
this.logger.error(errMsg);
|
this.logger.error(errMsg);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { IncomingMessage } from 'http';
|
import type { IncomingMessage } from 'http';
|
||||||
|
import type { Guarded } from '../util/GuardedStream';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An incoming HTTP request;
|
* An incoming HTTP request;
|
||||||
*/
|
*/
|
||||||
export type HttpRequest = IncomingMessage;
|
export type HttpRequest = Guarded<IncomingMessage>;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import type { Quad } from 'rdf-js';
|
import type { Quad } from 'rdf-js';
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import type { Representation } from '../ldp/representation/Representation';
|
import type { Representation } from '../ldp/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
||||||
@ -12,6 +11,7 @@ import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpEr
|
|||||||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||||
import { NotImplementedError } from '../util/errors/NotImplementedError';
|
import { NotImplementedError } from '../util/errors/NotImplementedError';
|
||||||
import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError';
|
import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError';
|
||||||
|
import type { Guarded } from '../util/GuardedStream';
|
||||||
import {
|
import {
|
||||||
ensureTrailingSlash,
|
ensureTrailingSlash,
|
||||||
getParentContainer,
|
getParentContainer,
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
} from '../util/PathUtil';
|
} from '../util/PathUtil';
|
||||||
import { parseQuads } from '../util/QuadUtil';
|
import { parseQuads } from '../util/QuadUtil';
|
||||||
import { generateResourceQuads } from '../util/ResourceUtil';
|
import { generateResourceQuads } from '../util/ResourceUtil';
|
||||||
|
import { guardedStreamFrom } from '../util/StreamUtil';
|
||||||
import { CONTENT_TYPE, HTTP, LDP, RDF } from '../util/UriConstants';
|
import { CONTENT_TYPE, HTTP, LDP, RDF } from '../util/UriConstants';
|
||||||
import type { DataAccessor } from './accessors/DataAccessor';
|
import type { DataAccessor } from './accessors/DataAccessor';
|
||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore } from './ResourceStore';
|
||||||
@ -70,9 +71,9 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
metadata.contentType = INTERNAL_QUADS;
|
metadata.contentType = INTERNAL_QUADS;
|
||||||
result = {
|
result = {
|
||||||
binary: false,
|
binary: false,
|
||||||
get data(): Readable {
|
get data(): Guarded<Readable> {
|
||||||
// This allows other modules to still add metadata before the output data is written
|
// This allows other modules to still add metadata before the output data is written
|
||||||
return streamifyArray(result.metadata.quads());
|
return guardedStreamFrom(result.metadata.quads());
|
||||||
},
|
},
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
@ -365,7 +366,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
|||||||
protected getEmptyContainerRepresentation(container: ResourceIdentifier): Representation {
|
protected getEmptyContainerRepresentation(container: ResourceIdentifier): Representation {
|
||||||
return {
|
return {
|
||||||
binary: true,
|
binary: true,
|
||||||
data: streamifyArray([]),
|
data: guardedStreamFrom([]),
|
||||||
metadata: new RepresentationMetadata(container.path),
|
metadata: new RepresentationMetadata(container.path),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import type { Representation } from '../ldp/representation/Representation';
|
|||||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
|
import type { Guarded } from '../util/GuardedStream';
|
||||||
import type { AtomicResourceStore } from './AtomicResourceStore';
|
import type { AtomicResourceStore } from './AtomicResourceStore';
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
import type { ExpiringLock } from './ExpiringLock';
|
import type { ExpiringLock } from './ExpiringLock';
|
||||||
@ -118,7 +119,7 @@ export class LockingResourceStore implements AtomicResourceStore {
|
|||||||
* @param source - The readable to wrap
|
* @param source - The readable to wrap
|
||||||
* @param lock - The lock for the corresponding identifier.
|
* @param lock - The lock for the corresponding identifier.
|
||||||
*/
|
*/
|
||||||
protected createExpiringReadable(source: Readable, lock: ExpiringLock): Readable {
|
protected createExpiringReadable(source: Guarded<Readable>, lock: ExpiringLock): Readable {
|
||||||
// Destroy the source when a timeout occurs.
|
// Destroy the source when a timeout occurs.
|
||||||
lock.on('expired', (): void => {
|
lock.on('expired', (): void => {
|
||||||
source.destroy(new Error(`Stream reading timout exceeded`));
|
source.destroy(new Error(`Stream reading timout exceeded`));
|
||||||
|
@ -2,6 +2,7 @@ import type { Readable } from 'stream';
|
|||||||
import type { Representation } from '../../ldp/representation/Representation';
|
import type { Representation } from '../../ldp/representation/Representation';
|
||||||
import type { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
import type { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
||||||
|
import type { Guarded } from '../../util/GuardedStream';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DataAccessor is the building block closest to the actual data storage.
|
* A DataAccessor is the building block closest to the actual data storage.
|
||||||
@ -27,7 +28,7 @@ export interface DataAccessor {
|
|||||||
* It can be assumed that the incoming identifier will always correspond to a document.
|
* It can be assumed that the incoming identifier will always correspond to a document.
|
||||||
* @param identifier - Identifier for which the data is requested.
|
* @param identifier - Identifier for which the data is requested.
|
||||||
*/
|
*/
|
||||||
getData: (identifier: ResourceIdentifier) => Promise<Readable>;
|
getData: (identifier: ResourceIdentifier) => Promise<Guarded<Readable>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the metadata corresponding to the identifier.
|
* Returns the metadata corresponding to the identifier.
|
||||||
@ -42,7 +43,8 @@ export interface DataAccessor {
|
|||||||
* @param data - Data to store.
|
* @param data - Data to store.
|
||||||
* @param metadata - Metadata to store.
|
* @param metadata - Metadata to store.
|
||||||
*/
|
*/
|
||||||
writeDocument: (identifier: ResourceIdentifier, data: Readable, metadata: RepresentationMetadata) => Promise<void>;
|
writeDocument: (identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata) =>
|
||||||
|
Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes metadata for a container.
|
* Writes metadata for a container.
|
||||||
|
@ -12,6 +12,8 @@ import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
|
|||||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||||
import { isSystemError } from '../../util/errors/SystemError';
|
import { isSystemError } from '../../util/errors/SystemError';
|
||||||
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
|
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
|
||||||
|
import { guardStream } from '../../util/GuardedStream';
|
||||||
|
import type { Guarded } from '../../util/GuardedStream';
|
||||||
import { isContainerIdentifier } from '../../util/PathUtil';
|
import { isContainerIdentifier } from '../../util/PathUtil';
|
||||||
import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
|
import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
|
||||||
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
|
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
|
||||||
@ -45,12 +47,12 @@ export class FileDataAccessor implements DataAccessor {
|
|||||||
* Will return data stream directly to the file corresponding to the resource.
|
* Will return data stream directly to the file corresponding to the resource.
|
||||||
* Will throw NotFoundHttpError if the input is a container.
|
* Will throw NotFoundHttpError if the input is a container.
|
||||||
*/
|
*/
|
||||||
public async getData(identifier: ResourceIdentifier): Promise<Readable> {
|
public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
|
||||||
const link = await this.resourceMapper.mapUrlToFilePath(identifier);
|
const link = await this.resourceMapper.mapUrlToFilePath(identifier);
|
||||||
const stats = await this.getStats(link.filePath);
|
const stats = await this.getStats(link.filePath);
|
||||||
|
|
||||||
if (stats.isFile()) {
|
if (stats.isFile()) {
|
||||||
return createReadStream(link.filePath);
|
return guardStream(createReadStream(link.filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotFoundHttpError();
|
throw new NotFoundHttpError();
|
||||||
@ -76,7 +78,7 @@ export class FileDataAccessor implements DataAccessor {
|
|||||||
* Writes the given data as a file (and potential metadata as additional file).
|
* Writes the given data as a file (and potential metadata as additional file).
|
||||||
* The metadata file will be written first and will be deleted if something goes wrong writing the actual data.
|
* The metadata file will be written first and will be deleted if something goes wrong writing the actual data.
|
||||||
*/
|
*/
|
||||||
public async writeDocument(identifier: ResourceIdentifier, data: Readable, metadata: RepresentationMetadata):
|
public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata):
|
||||||
Promise<void> {
|
Promise<void> {
|
||||||
if (this.isMetadataPath(identifier.path)) {
|
if (this.isMetadataPath(identifier.path)) {
|
||||||
throw new ConflictHttpError('Not allowed to create files with the metadata extension.');
|
throw new ConflictHttpError('Not allowed to create files with the metadata extension.');
|
||||||
@ -264,7 +266,7 @@ export class FileDataAccessor implements DataAccessor {
|
|||||||
// Check if the metadata file exists first
|
// Check if the metadata file exists first
|
||||||
await fsPromises.lstat(metadataPath);
|
await fsPromises.lstat(metadataPath);
|
||||||
|
|
||||||
const readMetadataStream = createReadStream(metadataPath);
|
const readMetadataStream = guardStream(createReadStream(metadataPath));
|
||||||
return await parseQuads(readMetadataStream);
|
return await parseQuads(readMetadataStream);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
|
// Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
import arrayifyStream from 'arrayify-stream';
|
import arrayifyStream from 'arrayify-stream';
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import type { NamedNode } from 'rdf-js';
|
import type { NamedNode } from 'rdf-js';
|
||||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
||||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||||
|
import type { Guarded } from '../../util/GuardedStream';
|
||||||
import { ensureTrailingSlash, isContainerIdentifier } from '../../util/PathUtil';
|
import { ensureTrailingSlash, isContainerIdentifier } from '../../util/PathUtil';
|
||||||
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
|
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
|
||||||
|
import { guardedStreamFrom } from '../../util/StreamUtil';
|
||||||
import type { DataAccessor } from './DataAccessor';
|
import type { DataAccessor } from './DataAccessor';
|
||||||
|
|
||||||
interface DataEntry {
|
interface DataEntry {
|
||||||
@ -19,27 +21,6 @@ interface ContainerEntry {
|
|||||||
}
|
}
|
||||||
type CacheEntry = DataEntry | ContainerEntry;
|
type CacheEntry = DataEntry | ContainerEntry;
|
||||||
|
|
||||||
class ArrayReadable extends Readable {
|
|
||||||
private readonly data: any[];
|
|
||||||
private idx: number;
|
|
||||||
|
|
||||||
public constructor(data: any[]) {
|
|
||||||
super({ objectMode: true });
|
|
||||||
this.data = data;
|
|
||||||
this.idx = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
public _read(): void {
|
|
||||||
if (this.idx < this.data.length) {
|
|
||||||
this.push(this.data[this.idx]);
|
|
||||||
this.idx += 1;
|
|
||||||
} else {
|
|
||||||
this.push(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InMemoryDataAccessor implements DataAccessor {
|
export class InMemoryDataAccessor implements DataAccessor {
|
||||||
private readonly base: string;
|
private readonly base: string;
|
||||||
private readonly store: ContainerEntry;
|
private readonly store: ContainerEntry;
|
||||||
@ -56,12 +37,12 @@ export class InMemoryDataAccessor implements DataAccessor {
|
|||||||
// All data is supported since streams never get read, only copied
|
// All data is supported since streams never get read, only copied
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getData(identifier: ResourceIdentifier): Promise<Readable> {
|
public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
|
||||||
const entry = this.getEntry(identifier);
|
const entry = this.getEntry(identifier);
|
||||||
if (!this.isDataEntry(entry)) {
|
if (!this.isDataEntry(entry)) {
|
||||||
throw new NotFoundHttpError();
|
throw new NotFoundHttpError();
|
||||||
}
|
}
|
||||||
return new ArrayReadable(entry.data);
|
return guardedStreamFrom(entry.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
|
public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
|
||||||
@ -72,7 +53,7 @@ export class InMemoryDataAccessor implements DataAccessor {
|
|||||||
return this.generateMetadata(identifier, entry);
|
return this.generateMetadata(identifier, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async writeDocument(identifier: ResourceIdentifier, data: Readable, metadata: RepresentationMetadata):
|
public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata):
|
||||||
Promise<void> {
|
Promise<void> {
|
||||||
const { parent, name } = this.getParentEntry(identifier);
|
const { parent, name } = this.getParentEntry(identifier);
|
||||||
parent.entries[name] = {
|
parent.entries[name] = {
|
||||||
|
@ -23,6 +23,8 @@ import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
|
|||||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||||
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
|
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
|
||||||
|
import { guardStream } from '../../util/GuardedStream';
|
||||||
|
import type { Guarded } from '../../util/GuardedStream';
|
||||||
import { ensureTrailingSlash, getParentContainer, isContainerIdentifier } from '../../util/PathUtil';
|
import { ensureTrailingSlash, getParentContainer, isContainerIdentifier } from '../../util/PathUtil';
|
||||||
import { generateResourceQuads } from '../../util/ResourceUtil';
|
import { generateResourceQuads } from '../../util/ResourceUtil';
|
||||||
import { CONTENT_TYPE, LDP } from '../../util/UriConstants';
|
import { CONTENT_TYPE, LDP } from '../../util/UriConstants';
|
||||||
@ -70,9 +72,9 @@ export class SparqlDataAccessor implements DataAccessor {
|
|||||||
* Returns all triples stored for the corresponding identifier.
|
* Returns all triples stored for the corresponding identifier.
|
||||||
* Note that this will not throw a 404 if no results were found.
|
* Note that this will not throw a 404 if no results were found.
|
||||||
*/
|
*/
|
||||||
public async getData(identifier: ResourceIdentifier): Promise<Readable> {
|
public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
|
||||||
const name = namedNode(identifier.path);
|
const name = namedNode(identifier.path);
|
||||||
return this.sendSparqlConstruct(this.sparqlConstruct(name));
|
return await this.sendSparqlConstruct(this.sparqlConstruct(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,7 +116,7 @@ export class SparqlDataAccessor implements DataAccessor {
|
|||||||
/**
|
/**
|
||||||
* Reads the given data stream and stores it together with the metadata.
|
* Reads the given data stream and stores it together with the metadata.
|
||||||
*/
|
*/
|
||||||
public async writeDocument(identifier: ResourceIdentifier, data: Readable, metadata: RepresentationMetadata):
|
public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata):
|
||||||
Promise<void> {
|
Promise<void> {
|
||||||
if (this.isMetadataIdentifier(identifier)) {
|
if (this.isMetadataIdentifier(identifier)) {
|
||||||
throw new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.');
|
throw new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.');
|
||||||
@ -292,11 +294,11 @@ export class SparqlDataAccessor implements DataAccessor {
|
|||||||
* Sends a SPARQL CONSTRUCT query to the endpoint and returns a stream of quads.
|
* Sends a SPARQL CONSTRUCT query to the endpoint and returns a stream of quads.
|
||||||
* @param sparqlQuery - Query to execute.
|
* @param sparqlQuery - Query to execute.
|
||||||
*/
|
*/
|
||||||
private async sendSparqlConstruct(sparqlQuery: ConstructQuery): Promise<Readable> {
|
private async sendSparqlConstruct(sparqlQuery: ConstructQuery): Promise<Guarded<Readable>> {
|
||||||
const query = this.generator.stringify(sparqlQuery);
|
const query = this.generator.stringify(sparqlQuery);
|
||||||
this.logger.info(`Sending SPARQL CONSTRUCT query to ${this.endpoint}: ${query}`);
|
this.logger.info(`Sending SPARQL CONSTRUCT query to ${this.endpoint}: ${query}`);
|
||||||
try {
|
try {
|
||||||
return await this.fetcher.fetchTriples(this.endpoint, query);
|
return guardStream(await this.fetcher.fetchTriples(this.endpoint, query));
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${error.message}`);
|
this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${error.message}`);
|
||||||
|
@ -4,6 +4,7 @@ import type { Representation } from '../../ldp/representation/Representation';
|
|||||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||||
import type { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
|
import type { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
|
||||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||||
|
import { guardStream } from '../../util/GuardedStream';
|
||||||
import { CONTENT_TYPE } from '../../util/UriConstants';
|
import { CONTENT_TYPE } from '../../util/UriConstants';
|
||||||
import { validateRequestArgs, matchingTypes } from './ConversionUtil';
|
import { validateRequestArgs, matchingTypes } from './ConversionUtil';
|
||||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||||
@ -34,7 +35,7 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
|
|||||||
const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType });
|
const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType });
|
||||||
return {
|
return {
|
||||||
binary: true,
|
binary: true,
|
||||||
data: rdfSerializer.serialize(quads.data, { contentType }) as Readable,
|
data: guardStream(rdfSerializer.serialize(quads.data, { contentType }) as Readable),
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdenti
|
|||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||||
|
import { guardStream } from '../../util/GuardedStream';
|
||||||
import { CONTENT_TYPE } from '../../util/UriConstants';
|
import { CONTENT_TYPE } from '../../util/UriConstants';
|
||||||
import type { ResourceLocker } from '../ResourceLocker';
|
import type { ResourceLocker } from '../ResourceLocker';
|
||||||
import type { ResourceStore } from '../ResourceStore';
|
import type { ResourceStore } from '../ResourceStore';
|
||||||
@ -77,7 +78,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
|
|||||||
const metadata = new RepresentationMetadata(input.identifier.path, { [CONTENT_TYPE]: INTERNAL_QUADS });
|
const metadata = new RepresentationMetadata(input.identifier.path, { [CONTENT_TYPE]: INTERNAL_QUADS });
|
||||||
const representation: Representation = {
|
const representation: Representation = {
|
||||||
binary: false,
|
binary: false,
|
||||||
data: store.match() as Readable,
|
data: guardStream(store.match() as Readable),
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
await this.source.setRepresentation(input.identifier, representation);
|
await this.source.setRepresentation(input.identifier, representation);
|
||||||
|
0
src/util/MetadataController.ts
Normal file
0
src/util/MetadataController.ts
Normal file
@ -4,6 +4,7 @@ import { DataFactory, StreamParser, StreamWriter } from 'n3';
|
|||||||
import type { Literal, NamedNode, Quad } from 'rdf-js';
|
import type { Literal, NamedNode, Quad } from 'rdf-js';
|
||||||
import streamifyArray from 'streamify-array';
|
import streamifyArray from 'streamify-array';
|
||||||
import { TEXT_TURTLE } from './ContentTypes';
|
import { TEXT_TURTLE } from './ContentTypes';
|
||||||
|
import type { Guarded } from './GuardedStream';
|
||||||
import { pipeSafely } from './StreamUtil';
|
import { pipeSafely } from './StreamUtil';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,7 +20,7 @@ export const pushQuad =
|
|||||||
*
|
*
|
||||||
* @returns The Readable object.
|
* @returns The Readable object.
|
||||||
*/
|
*/
|
||||||
export const serializeQuads = (quads: Quad[]): Readable =>
|
export const serializeQuads = (quads: Quad[]): Guarded<Readable> =>
|
||||||
pipeSafely(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
|
pipeSafely(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,5 +29,5 @@ export const serializeQuads = (quads: Quad[]): Readable =>
|
|||||||
*
|
*
|
||||||
* @returns A promise containing the array of quads.
|
* @returns A promise containing the array of quads.
|
||||||
*/
|
*/
|
||||||
export const parseQuads = async(readable: Readable): Promise<Quad[]> =>
|
export const parseQuads = async(readable: Guarded<Readable>): Promise<Quad[]> =>
|
||||||
arrayifyStream(pipeSafely(readable, new StreamParser({ format: TEXT_TURTLE })));
|
arrayifyStream(pipeSafely(readable, new StreamParser({ format: TEXT_TURTLE })));
|
||||||
|
@ -5,6 +5,7 @@ import { RepresentationMetadata } from '../../src/ldp/representation/Representat
|
|||||||
import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor';
|
import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor';
|
||||||
import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
|
import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
|
||||||
import { ExtensionBasedMapper } from '../../src/storage/mapping/ExtensionBasedMapper';
|
import { ExtensionBasedMapper } from '../../src/storage/mapping/ExtensionBasedMapper';
|
||||||
|
import { guardStream } from '../../src/util/GuardedStream';
|
||||||
import { ensureTrailingSlash } from '../../src/util/PathUtil';
|
import { ensureTrailingSlash } from '../../src/util/PathUtil';
|
||||||
import { CONTENT_TYPE, LDP } from '../../src/util/UriConstants';
|
import { CONTENT_TYPE, LDP } from '../../src/util/UriConstants';
|
||||||
import { AuthenticatedDataAccessorBasedConfig } from '../configs/AuthenticatedDataAccessorBasedConfig';
|
import { AuthenticatedDataAccessorBasedConfig } from '../configs/AuthenticatedDataAccessorBasedConfig';
|
||||||
@ -42,7 +43,7 @@ describe.each([ dataAccessorStore, inMemoryDataAccessorStore ])('A server using
|
|||||||
// Use store instead of file access so tests also work for non-file backends
|
// Use store instead of file access so tests also work for non-file backends
|
||||||
await config.store.setRepresentation({ path: `${BASE}/permanent.txt` }, {
|
await config.store.setRepresentation({ path: `${BASE}/permanent.txt` }, {
|
||||||
binary: true,
|
binary: true,
|
||||||
data: createReadStream(join(__dirname, '../assets/permanent.txt')),
|
data: guardStream(createReadStream(join(__dirname, '../assets/permanent.txt'))),
|
||||||
metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/plain' }),
|
metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/plain' }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import type { Representation } from '../../src/ldp/representation/Representation';
|
import type { Representation } from '../../src/ldp/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
|
||||||
import { ChainedConverter } from '../../src/storage/conversion/ChainedConverter';
|
import { ChainedConverter } from '../../src/storage/conversion/ChainedConverter';
|
||||||
import { QuadToRdfConverter } from '../../src/storage/conversion/QuadToRdfConverter';
|
import { QuadToRdfConverter } from '../../src/storage/conversion/QuadToRdfConverter';
|
||||||
import { RdfToQuadConverter } from '../../src/storage/conversion/RdfToQuadConverter';
|
import { RdfToQuadConverter } from '../../src/storage/conversion/RdfToQuadConverter';
|
||||||
import { readableToString } from '../../src/util/StreamUtil';
|
import { guardedStreamFrom, readableToString } from '../../src/util/StreamUtil';
|
||||||
import { CONTENT_TYPE } from '../../src/util/UriConstants';
|
import { CONTENT_TYPE } from '../../src/util/UriConstants';
|
||||||
|
|
||||||
describe('A ChainedConverter', (): void => {
|
describe('A ChainedConverter', (): void => {
|
||||||
@ -18,7 +17,9 @@ describe('A ChainedConverter', (): void => {
|
|||||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'application/ld+json' });
|
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'application/ld+json' });
|
||||||
const representation: Representation = {
|
const representation: Representation = {
|
||||||
binary: true,
|
binary: true,
|
||||||
data: streamifyArray([ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ]),
|
data: guardedStreamFrom(
|
||||||
|
[ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ],
|
||||||
|
),
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ describe('A ChainedConverter', (): void => {
|
|||||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||||
const representation: Representation = {
|
const representation: Representation = {
|
||||||
binary: true,
|
binary: true,
|
||||||
data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
|
data: guardedStreamFrom([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,13 +2,13 @@ import { EventEmitter } from 'events';
|
|||||||
import { PassThrough } from 'stream';
|
import { PassThrough } from 'stream';
|
||||||
import type { MockResponse } from 'node-mocks-http';
|
import type { MockResponse } from 'node-mocks-http';
|
||||||
import { createResponse } from 'node-mocks-http';
|
import { createResponse } from 'node-mocks-http';
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import { BasicResponseWriter } from '../../../../src/ldp/http/BasicResponseWriter';
|
import { BasicResponseWriter } from '../../../../src/ldp/http/BasicResponseWriter';
|
||||||
import type { MetadataWriter } from '../../../../src/ldp/http/metadata/MetadataWriter';
|
import type { MetadataWriter } from '../../../../src/ldp/http/metadata/MetadataWriter';
|
||||||
import type { ResponseDescription } from '../../../../src/ldp/http/response/ResponseDescription';
|
import type { ResponseDescription } from '../../../../src/ldp/http/response/ResponseDescription';
|
||||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||||
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
|
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
|
||||||
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
|
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
|
||||||
|
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
|
||||||
import { CONTENT_TYPE } from '../../../../src/util/UriConstants';
|
import { CONTENT_TYPE } from '../../../../src/util/UriConstants';
|
||||||
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
|
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ describe('A BasicResponseWriter', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('responds with a body if the description has a body.', async(): Promise<void> => {
|
it('responds with a body if the description has a body.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]);
|
const data = guardedStreamFrom([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]);
|
||||||
result = { statusCode: 201, data };
|
result = { statusCode: 201, data };
|
||||||
|
|
||||||
const end = new Promise((resolve): void => {
|
const end = new Promise((resolve): void => {
|
||||||
@ -69,7 +69,7 @@ describe('A BasicResponseWriter', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle the data stream erroring.', async(): Promise<void> => {
|
it('can handle the data stream erroring.', async(): Promise<void> => {
|
||||||
const data = new PassThrough();
|
const data = guardedStreamFrom([]);
|
||||||
data.read = (): any => {
|
data.read = (): any => {
|
||||||
data.emit('error', new Error('bad data!'));
|
data.emit('error', new Error('bad data!'));
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
import arrayifyStream from 'arrayify-stream';
|
import arrayifyStream from 'arrayify-stream';
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import type { Representation } from '../../../src/ldp/representation/Representation';
|
import type { Representation } from '../../../src/ldp/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
|
||||||
@ -13,7 +12,9 @@ import { MethodNotAllowedHttpError } from '../../../src/util/errors/MethodNotAll
|
|||||||
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
||||||
import { NotImplementedError } from '../../../src/util/errors/NotImplementedError';
|
import { NotImplementedError } from '../../../src/util/errors/NotImplementedError';
|
||||||
import { UnsupportedHttpError } from '../../../src/util/errors/UnsupportedHttpError';
|
import { UnsupportedHttpError } from '../../../src/util/errors/UnsupportedHttpError';
|
||||||
|
import type { Guarded } from '../../../src/util/GuardedStream';
|
||||||
import * as quadUtil from '../../../src/util/QuadUtil';
|
import * as quadUtil from '../../../src/util/QuadUtil';
|
||||||
|
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
|
||||||
import { CONTENT_TYPE, HTTP, LDP, RDF } from '../../../src/util/UriConstants';
|
import { CONTENT_TYPE, HTTP, LDP, RDF } from '../../../src/util/UriConstants';
|
||||||
import { toNamedNode } from '../../../src/util/UriUtil';
|
import { toNamedNode } from '../../../src/util/UriUtil';
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ class SimpleDataAccessor implements DataAccessor {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getData(identifier: ResourceIdentifier): Promise<Readable> {
|
public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
|
||||||
this.checkExists(identifier);
|
this.checkExists(identifier);
|
||||||
return this.data[identifier.path].data;
|
return this.data[identifier.path].data;
|
||||||
}
|
}
|
||||||
@ -83,11 +84,11 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
|
|
||||||
representation = {
|
representation = {
|
||||||
binary: true,
|
binary: true,
|
||||||
data: streamifyArray([ resourceData ]),
|
data: guardedStreamFrom([ resourceData ]),
|
||||||
metadata: new RepresentationMetadata(
|
metadata: new RepresentationMetadata(
|
||||||
{ [CONTENT_TYPE]: 'text/plain', [RDF.type]: DataFactory.namedNode(LDP.Resource) },
|
{ [CONTENT_TYPE]: 'text/plain', [RDF.type]: DataFactory.namedNode(LDP.Resource) },
|
||||||
),
|
),
|
||||||
} as Representation;
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getting a Representation', (): void => {
|
describe('getting a Representation', (): void => {
|
||||||
@ -176,7 +177,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
const resourceID = { path: root };
|
const resourceID = { path: root };
|
||||||
representation.metadata.add(RDF.type, toNamedNode(LDP.Container));
|
representation.metadata.add(RDF.type, toNamedNode(LDP.Container));
|
||||||
representation.metadata.contentType = 'text/turtle';
|
representation.metadata.contentType = 'text/turtle';
|
||||||
representation.data = streamifyArray([ `<${`${root}resource/`}> a <coolContainer>.` ]);
|
representation.data = guardedStreamFrom([ `<${`${root}resource/`}> a <coolContainer>.` ]);
|
||||||
const result = await store.addResource(resourceID, representation);
|
const result = await store.addResource(resourceID, representation);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
path: expect.stringMatching(new RegExp(`^${root}[^/]+/$`, 'u')),
|
path: expect.stringMatching(new RegExp(`^${root}[^/]+/$`, 'u')),
|
||||||
@ -287,7 +288,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
// Generate based on URI
|
// Generate based on URI
|
||||||
representation.metadata.removeAll(RDF.type);
|
representation.metadata.removeAll(RDF.type);
|
||||||
representation.metadata.contentType = 'text/turtle';
|
representation.metadata.contentType = 'text/turtle';
|
||||||
representation.data = streamifyArray([ `<${`${root}resource/`}> a <coolContainer>.` ]);
|
representation.data = guardedStreamFrom([ `<${`${root}resource/`}> a <coolContainer>.` ]);
|
||||||
await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined();
|
await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined();
|
||||||
expect(accessor.data[resourceID.path]).toBeTruthy();
|
expect(accessor.data[resourceID.path]).toBeTruthy();
|
||||||
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
|
||||||
@ -298,7 +299,9 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.metadata.add(RDF.type, toNamedNode(LDP.Container));
|
representation.metadata.add(RDF.type, toNamedNode(LDP.Container));
|
||||||
representation.metadata.contentType = 'text/turtle';
|
representation.metadata.contentType = 'text/turtle';
|
||||||
representation.metadata.identifier = DataFactory.namedNode(`${root}resource/`);
|
representation.metadata.identifier = DataFactory.namedNode(`${root}resource/`);
|
||||||
representation.data = streamifyArray([ `<${`${root}resource/`}> <http://www.w3.org/ns/ldp#contains> <uri>.` ]);
|
representation.data = guardedStreamFrom(
|
||||||
|
[ `<${`${root}resource/`}> <http://www.w3.org/ns/ldp#contains> <uri>.` ],
|
||||||
|
);
|
||||||
await expect(store.setRepresentation(resourceID, representation))
|
await expect(store.setRepresentation(resourceID, representation))
|
||||||
.rejects.toThrow(new ConflictHttpError('Container bodies are not allowed to have containment triples.'));
|
.rejects.toThrow(new ConflictHttpError('Container bodies are not allowed to have containment triples.'));
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import type { Readable } from 'stream';
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||||
import { FileDataAccessor } from '../../../../src/storage/accessors/FileDataAccessor';
|
import { FileDataAccessor } from '../../../../src/storage/accessors/FileDataAccessor';
|
||||||
@ -9,7 +9,8 @@ import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError
|
|||||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||||
import type { SystemError } from '../../../../src/util/errors/SystemError';
|
import type { SystemError } from '../../../../src/util/errors/SystemError';
|
||||||
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
||||||
import { readableToString } from '../../../../src/util/StreamUtil';
|
import type { Guarded } from '../../../../src/util/GuardedStream';
|
||||||
|
import { guardedStreamFrom, readableToString } from '../../../../src/util/StreamUtil';
|
||||||
import { CONTENT_TYPE, DCTERMS, LDP, POSIX, RDF, XSD } from '../../../../src/util/UriConstants';
|
import { CONTENT_TYPE, DCTERMS, LDP, POSIX, RDF, XSD } from '../../../../src/util/UriConstants';
|
||||||
import { toNamedNode, toTypedLiteral } from '../../../../src/util/UriUtil';
|
import { toNamedNode, toTypedLiteral } from '../../../../src/util/UriUtil';
|
||||||
import { mockFs } from '../../../util/Util';
|
import { mockFs } from '../../../util/Util';
|
||||||
@ -24,12 +25,15 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
let accessor: FileDataAccessor;
|
let accessor: FileDataAccessor;
|
||||||
let cache: { data: any };
|
let cache: { data: any };
|
||||||
let metadata: RepresentationMetadata;
|
let metadata: RepresentationMetadata;
|
||||||
|
let data: Guarded<Readable>;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
cache = mockFs(rootFilePath, now);
|
cache = mockFs(rootFilePath, now);
|
||||||
accessor = new FileDataAccessor(new ExtensionBasedMapper(base, rootFilePath));
|
accessor = new FileDataAccessor(new ExtensionBasedMapper(base, rootFilePath));
|
||||||
|
|
||||||
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM });
|
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM });
|
||||||
|
|
||||||
|
data = guardedStreamFrom([ 'data' ]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can only handle binary data.', async(): Promise<void> => {
|
it('can only handle binary data.', async(): Promise<void> => {
|
||||||
@ -140,23 +144,21 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
|
|
||||||
describe('writing a document', (): void => {
|
describe('writing a document', (): void => {
|
||||||
it('throws a 404 if the identifier does not start with the base.', async(): Promise<void> => {
|
it('throws a 404 if the identifier does not start with the base.', async(): Promise<void> => {
|
||||||
await expect(accessor.writeDocument({ path: 'badpath' }, streamifyArray([]), metadata))
|
await expect(accessor.writeDocument({ path: 'badpath' }, data, metadata))
|
||||||
.rejects.toThrow(NotFoundHttpError);
|
.rejects.toThrow(NotFoundHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error when writing to a metadata path.', async(): Promise<void> => {
|
it('throws an error when writing to a metadata path.', async(): Promise<void> => {
|
||||||
await expect(accessor.writeDocument({ path: `${base}resource.meta` }, streamifyArray([]), metadata))
|
await expect(accessor.writeDocument({ path: `${base}resource.meta` }, data, metadata))
|
||||||
.rejects.toThrow(new ConflictHttpError('Not allowed to create files with the metadata extension.'));
|
.rejects.toThrow(new ConflictHttpError('Not allowed to create files with the metadata extension.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('writes the data to the corresponding file.', async(): Promise<void> => {
|
it('writes the data to the corresponding file.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([ 'data' ]);
|
|
||||||
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
|
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
|
||||||
expect(cache.data.resource).toBe('data');
|
expect(cache.data.resource).toBe('data');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('writes metadata to the corresponding metadata file.', async(): Promise<void> => {
|
it('writes metadata to the corresponding metadata file.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([ 'data' ]);
|
|
||||||
metadata = new RepresentationMetadata(`${base}res.ttl`, { [CONTENT_TYPE]: 'text/turtle', likes: 'apples' });
|
metadata = new RepresentationMetadata(`${base}res.ttl`, { [CONTENT_TYPE]: 'text/turtle', likes: 'apples' });
|
||||||
await expect(accessor.writeDocument({ path: `${base}res.ttl` }, data, metadata)).resolves.toBeUndefined();
|
await expect(accessor.writeDocument({ path: `${base}res.ttl` }, data, metadata)).resolves.toBeUndefined();
|
||||||
expect(cache.data['res.ttl']).toBe('data');
|
expect(cache.data['res.ttl']).toBe('data');
|
||||||
@ -164,7 +166,6 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not write metadata that is stored by the file system.', async(): Promise<void> => {
|
it('does not write metadata that is stored by the file system.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([ 'data' ]);
|
|
||||||
metadata.add(RDF.type, toNamedNode(LDP.Resource));
|
metadata.add(RDF.type, toNamedNode(LDP.Resource));
|
||||||
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
|
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
|
||||||
expect(cache.data.resource).toBe('data');
|
expect(cache.data.resource).toBe('data');
|
||||||
@ -173,7 +174,6 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
|
|
||||||
it('deletes existing metadata if nothing new needs to be stored.', async(): Promise<void> => {
|
it('deletes existing metadata if nothing new needs to be stored.', async(): Promise<void> => {
|
||||||
cache.data = { resource: 'data', 'resource.meta': 'metadata!' };
|
cache.data = { resource: 'data', 'resource.meta': 'metadata!' };
|
||||||
const data = streamifyArray([ 'data' ]);
|
|
||||||
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
|
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
|
||||||
expect(cache.data.resource).toBe('data');
|
expect(cache.data.resource).toBe('data');
|
||||||
expect(cache.data['resource.meta']).toBeUndefined();
|
expect(cache.data['resource.meta']).toBeUndefined();
|
||||||
@ -184,13 +184,11 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
jest.requireMock('fs').promises.unlink = (): any => {
|
jest.requireMock('fs').promises.unlink = (): any => {
|
||||||
throw new Error('error');
|
throw new Error('error');
|
||||||
};
|
};
|
||||||
const data = streamifyArray([ 'data' ]);
|
|
||||||
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
|
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
|
||||||
.rejects.toThrow(new Error('error'));
|
.rejects.toThrow(new Error('error'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if something went wrong writing a file.', async(): Promise<void> => {
|
it('throws if something went wrong writing a file.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([ 'data' ]);
|
|
||||||
data.read = (): any => {
|
data.read = (): any => {
|
||||||
data.emit('error', new Error('error'));
|
data.emit('error', new Error('error'));
|
||||||
return null;
|
return null;
|
||||||
@ -200,7 +198,6 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('deletes the metadata file if something went wrong writing the file.', async(): Promise<void> => {
|
it('deletes the metadata file if something went wrong writing the file.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([ 'data' ]);
|
|
||||||
data.read = (): any => {
|
data.read = (): any => {
|
||||||
data.emit('error', new Error('error'));
|
data.emit('error', new Error('error'));
|
||||||
return null;
|
return null;
|
||||||
@ -216,10 +213,10 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
metadata.identifier = DataFactory.namedNode(`${base}resource`);
|
metadata.identifier = DataFactory.namedNode(`${base}resource`);
|
||||||
metadata.contentType = 'text/plain';
|
metadata.contentType = 'text/plain';
|
||||||
metadata.add('new', 'metadata');
|
metadata.add('new', 'metadata');
|
||||||
await expect(accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'text' ]), metadata))
|
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
|
||||||
.resolves.toBeUndefined();
|
.resolves.toBeUndefined();
|
||||||
expect(cache.data).toEqual({
|
expect(cache.data).toEqual({
|
||||||
'resource$.txt': 'text',
|
'resource$.txt': 'data',
|
||||||
'resource.meta': expect.stringMatching(`<${base}resource> <new> "metadata".`),
|
'resource.meta': expect.stringMatching(`<${base}resource> <new> "metadata".`),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -235,11 +232,11 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
|
|
||||||
// `unlink` throwing ENOENT should not be an issue if the content-type does not change
|
// `unlink` throwing ENOENT should not be an issue if the content-type does not change
|
||||||
metadata.contentType = 'text/turtle';
|
metadata.contentType = 'text/turtle';
|
||||||
await expect(accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'text' ]), metadata))
|
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
|
||||||
.resolves.toBeUndefined();
|
.resolves.toBeUndefined();
|
||||||
|
|
||||||
metadata.contentType = 'text/plain';
|
metadata.contentType = 'text/plain';
|
||||||
await expect(accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'text' ]), metadata))
|
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
|
||||||
.rejects.toThrow(new Error('error'));
|
.rejects.toThrow(new Error('error'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import streamifyArray from 'streamify-array';
|
import type { Readable } from 'stream';
|
||||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||||
import { InMemoryDataAccessor } from '../../../../src/storage/accessors/InMemoryDataAccessor';
|
import { InMemoryDataAccessor } from '../../../../src/storage/accessors/InMemoryDataAccessor';
|
||||||
import { APPLICATION_OCTET_STREAM } from '../../../../src/util/ContentTypes';
|
import { APPLICATION_OCTET_STREAM } from '../../../../src/util/ContentTypes';
|
||||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||||
import { readableToString } from '../../../../src/util/StreamUtil';
|
import type { Guarded } from '../../../../src/util/GuardedStream';
|
||||||
|
import { guardedStreamFrom, readableToString } from '../../../../src/util/StreamUtil';
|
||||||
import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants';
|
import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants';
|
||||||
import { toNamedNode } from '../../../../src/util/UriUtil';
|
import { toNamedNode } from '../../../../src/util/UriUtil';
|
||||||
|
|
||||||
@ -11,11 +12,14 @@ describe('An InMemoryDataAccessor', (): void => {
|
|||||||
const base = 'http://test.com/';
|
const base = 'http://test.com/';
|
||||||
let accessor: InMemoryDataAccessor;
|
let accessor: InMemoryDataAccessor;
|
||||||
let metadata: RepresentationMetadata;
|
let metadata: RepresentationMetadata;
|
||||||
|
let data: Guarded<Readable>;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
accessor = new InMemoryDataAccessor(base);
|
accessor = new InMemoryDataAccessor(base);
|
||||||
|
|
||||||
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM });
|
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM });
|
||||||
|
|
||||||
|
data = guardedStreamFrom([ 'data' ]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can only handle all data.', async(): Promise<void> => {
|
it('can only handle all data.', async(): Promise<void> => {
|
||||||
@ -33,12 +37,11 @@ describe('An InMemoryDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if part of the path matches a document.', async(): Promise<void> => {
|
it('throws an error if part of the path matches a document.', async(): Promise<void> => {
|
||||||
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata);
|
await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
|
||||||
await expect(accessor.getData({ path: `${base}resource/resource2` })).rejects.toThrow(new Error('Invalid path.'));
|
await expect(accessor.getData({ path: `${base}resource/resource2` })).rejects.toThrow(new Error('Invalid path.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the corresponding data every time.', async(): Promise<void> => {
|
it('returns the corresponding data every time.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([ 'data' ]);
|
|
||||||
await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
|
await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
|
||||||
|
|
||||||
// Run twice to make sure the data is stored correctly
|
// Run twice to make sure the data is stored correctly
|
||||||
@ -53,12 +56,12 @@ describe('An InMemoryDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('errors when trying to access the parent of root.', async(): Promise<void> => {
|
it('errors when trying to access the parent of root.', async(): Promise<void> => {
|
||||||
await expect(accessor.writeDocument({ path: `${base}` }, streamifyArray([ 'data' ]), metadata))
|
await expect(accessor.writeDocument({ path: `${base}` }, data, metadata))
|
||||||
.rejects.toThrow(new Error('Root container has no parent.'));
|
.rejects.toThrow(new Error('Root container has no parent.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws a 404 if the trailing slash does not match its type.', async(): Promise<void> => {
|
it('throws a 404 if the trailing slash does not match its type.', async(): Promise<void> => {
|
||||||
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata);
|
await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
|
||||||
await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError);
|
await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError);
|
||||||
await accessor.writeContainer({ path: `${base}container/` }, metadata);
|
await accessor.writeContainer({ path: `${base}container/` }, metadata);
|
||||||
await expect(accessor.getMetadata({ path: `${base}container` })).rejects.toThrow(NotFoundHttpError);
|
await expect(accessor.getMetadata({ path: `${base}container` })).rejects.toThrow(NotFoundHttpError);
|
||||||
@ -66,14 +69,14 @@ describe('An InMemoryDataAccessor', (): void => {
|
|||||||
|
|
||||||
it('returns empty metadata if there was none stored.', async(): Promise<void> => {
|
it('returns empty metadata if there was none stored.', async(): Promise<void> => {
|
||||||
metadata = new RepresentationMetadata();
|
metadata = new RepresentationMetadata();
|
||||||
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata);
|
await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
|
||||||
metadata = await accessor.getMetadata({ path: `${base}resource` });
|
metadata = await accessor.getMetadata({ path: `${base}resource` });
|
||||||
expect(metadata.quads()).toHaveLength(0);
|
expect(metadata.quads()).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates the containment metadata for a container.', async(): Promise<void> => {
|
it('generates the containment metadata for a container.', async(): Promise<void> => {
|
||||||
await accessor.writeContainer({ path: `${base}container/` }, metadata);
|
await accessor.writeContainer({ path: `${base}container/` }, metadata);
|
||||||
await accessor.writeDocument({ path: `${base}container/resource` }, streamifyArray([ 'data' ]), metadata);
|
await accessor.writeDocument({ path: `${base}container/resource` }, data, metadata);
|
||||||
await accessor.writeContainer({ path: `${base}container/container2` }, metadata);
|
await accessor.writeContainer({ path: `${base}container/container2` }, metadata);
|
||||||
metadata = await accessor.getMetadata({ path: `${base}container/` });
|
metadata = await accessor.getMetadata({ path: `${base}container/` });
|
||||||
expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray(
|
expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray(
|
||||||
@ -83,7 +86,7 @@ describe('An InMemoryDataAccessor', (): void => {
|
|||||||
|
|
||||||
it('adds stored metadata when requesting document metadata.', async(): Promise<void> => {
|
it('adds stored metadata when requesting document metadata.', async(): Promise<void> => {
|
||||||
const inputMetadata = new RepresentationMetadata(`${base}resource`, { [RDF.type]: toNamedNode(LDP.Resource) });
|
const inputMetadata = new RepresentationMetadata(`${base}resource`, { [RDF.type]: toNamedNode(LDP.Resource) });
|
||||||
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), inputMetadata);
|
await accessor.writeDocument({ path: `${base}resource` }, data, inputMetadata);
|
||||||
metadata = await accessor.getMetadata({ path: `${base}resource` });
|
metadata = await accessor.getMetadata({ path: `${base}resource` });
|
||||||
expect(metadata.identifier.value).toBe(`${base}resource`);
|
expect(metadata.identifier.value).toBe(`${base}resource`);
|
||||||
const quads = metadata.quads();
|
const quads = metadata.quads();
|
||||||
@ -107,7 +110,7 @@ describe('An InMemoryDataAccessor', (): void => {
|
|||||||
await accessor.writeContainer({ path: `${base}container/` }, inputMetadata);
|
await accessor.writeContainer({ path: `${base}container/` }, inputMetadata);
|
||||||
const resourceMetadata = new RepresentationMetadata();
|
const resourceMetadata = new RepresentationMetadata();
|
||||||
await accessor.writeDocument(
|
await accessor.writeDocument(
|
||||||
{ path: `${base}container/resource` }, streamifyArray([ 'data' ]), resourceMetadata,
|
{ path: `${base}container/resource` }, data, resourceMetadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
const newMetadata = new RepresentationMetadata(inputMetadata);
|
const newMetadata = new RepresentationMetadata(inputMetadata);
|
||||||
@ -128,7 +131,7 @@ describe('An InMemoryDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('errors when writing to an invalid container path..', async(): Promise<void> => {
|
it('errors when writing to an invalid container path..', async(): Promise<void> => {
|
||||||
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata);
|
await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
|
||||||
|
|
||||||
await expect(accessor.writeContainer({ path: `${base}resource/container` }, metadata))
|
await expect(accessor.writeContainer({ path: `${base}resource/container` }, metadata))
|
||||||
.rejects.toThrow(new Error('Invalid path.'));
|
.rejects.toThrow(new Error('Invalid path.'));
|
||||||
@ -141,7 +144,7 @@ describe('An InMemoryDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('removes the corresponding resource.', async(): Promise<void> => {
|
it('removes the corresponding resource.', async(): Promise<void> => {
|
||||||
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata);
|
await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
|
||||||
await accessor.writeContainer({ path: `${base}container/` }, metadata);
|
await accessor.writeContainer({ path: `${base}container/` }, metadata);
|
||||||
await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined();
|
await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined();
|
||||||
await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined();
|
await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined();
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import type { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import arrayifyStream from 'arrayify-stream';
|
import arrayifyStream from 'arrayify-stream';
|
||||||
import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint';
|
import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint';
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import type { Quad } from 'rdf-js';
|
import type { Quad } from 'rdf-js';
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||||
import { SparqlDataAccessor } from '../../../../src/storage/accessors/SparqlDataAccessor';
|
import { SparqlDataAccessor } from '../../../../src/storage/accessors/SparqlDataAccessor';
|
||||||
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
|
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
|
||||||
@ -11,6 +10,8 @@ import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError
|
|||||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||||
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
|
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
|
||||||
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
||||||
|
import type { Guarded } from '../../../../src/util/GuardedStream';
|
||||||
|
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
|
||||||
import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants';
|
import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants';
|
||||||
import { toNamedNode } from '../../../../src/util/UriUtil';
|
import { toNamedNode } from '../../../../src/util/UriUtil';
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ describe('A SparqlDataAccessor', (): void => {
|
|||||||
const base = 'http://test.com/';
|
const base = 'http://test.com/';
|
||||||
let accessor: SparqlDataAccessor;
|
let accessor: SparqlDataAccessor;
|
||||||
let metadata: RepresentationMetadata;
|
let metadata: RepresentationMetadata;
|
||||||
|
let data: Guarded<Readable>;
|
||||||
let fetchTriples: jest.Mock<Promise<Readable>>;
|
let fetchTriples: jest.Mock<Promise<Readable>>;
|
||||||
let fetchUpdate: jest.Mock<Promise<void>>;
|
let fetchUpdate: jest.Mock<Promise<void>>;
|
||||||
let triples: Quad[];
|
let triples: Quad[];
|
||||||
@ -38,6 +40,9 @@ describe('A SparqlDataAccessor', (): void => {
|
|||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
metadata = new RepresentationMetadata();
|
metadata = new RepresentationMetadata();
|
||||||
|
data = guardedStreamFrom(
|
||||||
|
[ quad(namedNode('http://name'), namedNode('http://pred'), literal('value')) ],
|
||||||
|
);
|
||||||
triples = [ quad(namedNode('this'), namedNode('a'), namedNode('triple')) ];
|
triples = [ quad(namedNode('this'), namedNode('a'), namedNode('triple')) ];
|
||||||
|
|
||||||
// Makes it so the `SparqlEndpointFetcher` will always return the contents of the `triples` array
|
// Makes it so the `SparqlEndpointFetcher` will always return the contents of the `triples` array
|
||||||
@ -45,7 +50,7 @@ describe('A SparqlDataAccessor', (): void => {
|
|||||||
if (fetchError) {
|
if (fetchError) {
|
||||||
throw fetchError;
|
throw fetchError;
|
||||||
}
|
}
|
||||||
return streamifyArray(triples);
|
return Readable.from(triples);
|
||||||
});
|
});
|
||||||
fetchUpdate = jest.fn(async(): Promise<void> => {
|
fetchUpdate = jest.fn(async(): Promise<void> => {
|
||||||
if (updateError) {
|
if (updateError) {
|
||||||
@ -62,7 +67,6 @@ describe('A SparqlDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can only handle quad data.', async(): Promise<void> => {
|
it('can only handle quad data.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([]);
|
|
||||||
await expect(accessor.canHandle({ binary: true, data, metadata })).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
await expect(accessor.canHandle({ binary: true, data, metadata })).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
||||||
metadata.contentType = 'newInternalType';
|
metadata.contentType = 'newInternalType';
|
||||||
await expect(accessor.canHandle({ binary: false, data, metadata })).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
await expect(accessor.canHandle({ binary: false, data, metadata })).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
||||||
@ -71,8 +75,8 @@ describe('A SparqlDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns the corresponding quads when data is requested.', async(): Promise<void> => {
|
it('returns the corresponding quads when data is requested.', async(): Promise<void> => {
|
||||||
const data = await accessor.getData({ path: 'http://identifier' });
|
const result = await accessor.getData({ path: 'http://identifier' });
|
||||||
await expect(arrayifyStream(data)).resolves.toBeRdfIsomorphic([
|
await expect(arrayifyStream(result)).resolves.toBeRdfIsomorphic([
|
||||||
quad(namedNode('this'), namedNode('a'), namedNode('triple')),
|
quad(namedNode('this'), namedNode('a'), namedNode('triple')),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -168,7 +172,6 @@ describe('A SparqlDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('overwrites the data and metadata when writing a resource and updates parent.', async(): Promise<void> => {
|
it('overwrites the data and metadata when writing a resource and updates parent.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([ quad(namedNode('http://name'), namedNode('http://pred'), literal('value')) ]);
|
|
||||||
metadata = new RepresentationMetadata('http://test.com/container/resource',
|
metadata = new RepresentationMetadata('http://test.com/container/resource',
|
||||||
{ [RDF.type]: [ toNamedNode(LDP.Resource) ]});
|
{ [RDF.type]: [ toNamedNode(LDP.Resource) ]});
|
||||||
await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata))
|
await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata))
|
||||||
@ -202,13 +205,12 @@ describe('A SparqlDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('errors when trying to write to a metadata document.', async(): Promise<void> => {
|
it('errors when trying to write to a metadata document.', async(): Promise<void> => {
|
||||||
const data = streamifyArray([ quad(namedNode('http://name'), namedNode('http://pred'), literal('value')) ]);
|
|
||||||
await expect(accessor.writeDocument({ path: 'meta:http://test.com/container/resource' }, data, metadata))
|
await expect(accessor.writeDocument({ path: 'meta:http://test.com/container/resource' }, data, metadata))
|
||||||
.rejects.toThrow(new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.'));
|
.rejects.toThrow(new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors when writing triples in a non-default graph.', async(): Promise<void> => {
|
it('errors when writing triples in a non-default graph.', async(): Promise<void> => {
|
||||||
const data = streamifyArray(
|
data = guardedStreamFrom(
|
||||||
[ quad(namedNode('http://name'), namedNode('http://pred'), literal('value'), namedNode('badGraph!')) ],
|
[ quad(namedNode('http://name'), namedNode('http://pred'), literal('value'), namedNode('badGraph!')) ],
|
||||||
);
|
);
|
||||||
await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata))
|
await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata))
|
||||||
|
@ -2,12 +2,12 @@ import { EventEmitter } from 'events';
|
|||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import type { IncomingHttpHeaders } from 'http';
|
import type { IncomingHttpHeaders } from 'http';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { Readable } from 'stream';
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
import type { MockResponse } from 'node-mocks-http';
|
import type { MockResponse } from 'node-mocks-http';
|
||||||
import { createResponse } from 'node-mocks-http';
|
import { createResponse } from 'node-mocks-http';
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import type { ResourceStore } from '../../index';
|
import type { ResourceStore } from '../../index';
|
||||||
import { RepresentationMetadata } from '../../index';
|
import { guardedStreamFrom, RepresentationMetadata } from '../../index';
|
||||||
import type { PermissionSet } from '../../src/ldp/permissions/PermissionSet';
|
import type { PermissionSet } from '../../src/ldp/permissions/PermissionSet';
|
||||||
import type { HttpHandler } from '../../src/server/HttpHandler';
|
import type { HttpHandler } from '../../src/server/HttpHandler';
|
||||||
import type { HttpRequest } from '../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../src/server/HttpRequest';
|
||||||
@ -52,7 +52,7 @@ export class AclTestHelper {
|
|||||||
|
|
||||||
const representation = {
|
const representation = {
|
||||||
binary: true,
|
binary: true,
|
||||||
data: streamifyArray(acl),
|
data: guardedStreamFrom(acl),
|
||||||
metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }),
|
metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ export class FileTestHelper {
|
|||||||
headers: IncomingHttpHeaders,
|
headers: IncomingHttpHeaders,
|
||||||
data: Buffer,
|
data: Buffer,
|
||||||
): Promise<MockResponse<any>> {
|
): Promise<MockResponse<any>> {
|
||||||
const request = streamifyArray([ data ]) as HttpRequest;
|
const request = Readable.from([ data ]) as HttpRequest;
|
||||||
|
|
||||||
request.url = requestUrl.pathname;
|
request.url = requestUrl.pathname;
|
||||||
request.method = method;
|
request.method = method;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user