mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Integrate MetadataHandler
This commit is contained in:
parent
7dcb3eaa84
commit
31844a4f40
@ -5,6 +5,7 @@
|
|||||||
"files-scs:config/presets/http.json",
|
"files-scs:config/presets/http.json",
|
||||||
"files-scs:config/presets/ldp.json",
|
"files-scs:config/presets/ldp.json",
|
||||||
"files-scs:config/presets/ldp/credentials-extractor.json",
|
"files-scs:config/presets/ldp/credentials-extractor.json",
|
||||||
|
"files-scs:config/presets/ldp/metadata-handler.json",
|
||||||
"files-scs:config/presets/ldp/operation-handler.json",
|
"files-scs:config/presets/ldp/operation-handler.json",
|
||||||
"files-scs:config/presets/ldp/permissions-extractor.json",
|
"files-scs:config/presets/ldp/permissions-extractor.json",
|
||||||
"files-scs:config/presets/ldp/request-parser.json",
|
"files-scs:config/presets/ldp/request-parser.json",
|
||||||
|
20
config/presets/ldp/metadata-handler.json
Normal file
20
config/presets/ldp/metadata-handler.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"@id": "urn:solid-server:default:MetadataExtractor",
|
||||||
|
"@type": "BasicMetadataExtractor",
|
||||||
|
"BasicMetadataExtractor:_parsers": [
|
||||||
|
{
|
||||||
|
"@type": "ContentTypeParser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "LinkTypeParser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "SlugParser"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -10,6 +10,9 @@
|
|||||||
"BasicRequestParser:_preferenceParser": {
|
"BasicRequestParser:_preferenceParser": {
|
||||||
"@type": "AcceptPreferenceParser"
|
"@type": "AcceptPreferenceParser"
|
||||||
},
|
},
|
||||||
|
"BasicRequestParser:_metadataExtractor": {
|
||||||
|
"@id": "urn:solid-server:default:MetadataExtractor"
|
||||||
|
},
|
||||||
"BasicRequestParser:_bodyParser": {
|
"BasicRequestParser:_bodyParser": {
|
||||||
"@type": "CompositeAsyncHandler",
|
"@type": "CompositeAsyncHandler",
|
||||||
"CompositeAsyncHandler:_handlers": [
|
"CompositeAsyncHandler:_handlers": [
|
||||||
|
8
index.ts
8
index.ts
@ -14,6 +14,14 @@ export * from './src/authorization/WebAclAuthorizer';
|
|||||||
export * from './src/init/CliRunner';
|
export * from './src/init/CliRunner';
|
||||||
export * from './src/init/Setup';
|
export * from './src/init/Setup';
|
||||||
|
|
||||||
|
// LDP/HTTP/Metadata
|
||||||
|
export * from './src/ldp/http/metadata/BasicMetadataExtractor';
|
||||||
|
export * from './src/ldp/http/metadata/ContentTypeParser';
|
||||||
|
export * from './src/ldp/http/metadata/LinkTypeParser';
|
||||||
|
export * from './src/ldp/http/metadata/MetadataExtractor';
|
||||||
|
export * from './src/ldp/http/metadata/MetadataParser';
|
||||||
|
export * from './src/ldp/http/metadata/SlugParser';
|
||||||
|
|
||||||
// LDP/HTTP
|
// LDP/HTTP
|
||||||
export * from './src/ldp/http/AcceptPreferenceParser';
|
export * from './src/ldp/http/AcceptPreferenceParser';
|
||||||
export * from './src/ldp/http/BasicRequestParser';
|
export * from './src/ldp/http/BasicRequestParser';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { HttpRequest } from '../../server/HttpRequest';
|
import type { HttpRequest } from '../../server/HttpRequest';
|
||||||
import type { Operation } from '../operations/Operation';
|
import type { Operation } from '../operations/Operation';
|
||||||
import type { BodyParser } from './BodyParser';
|
import type { BodyParser } from './BodyParser';
|
||||||
|
import type { MetadataExtractor } from './metadata/MetadataExtractor';
|
||||||
import type { PreferenceParser } from './PreferenceParser';
|
import type { PreferenceParser } from './PreferenceParser';
|
||||||
import { RequestParser } from './RequestParser';
|
import { RequestParser } from './RequestParser';
|
||||||
import type { TargetExtractor } from './TargetExtractor';
|
import type { TargetExtractor } from './TargetExtractor';
|
||||||
@ -11,16 +12,18 @@ import type { TargetExtractor } from './TargetExtractor';
|
|||||||
export interface SimpleRequestParserArgs {
|
export interface SimpleRequestParserArgs {
|
||||||
targetExtractor: TargetExtractor;
|
targetExtractor: TargetExtractor;
|
||||||
preferenceParser: PreferenceParser;
|
preferenceParser: PreferenceParser;
|
||||||
|
metadataExtractor: MetadataExtractor;
|
||||||
bodyParser: BodyParser;
|
bodyParser: BodyParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an {@link Operation} from an incoming {@link HttpRequest} by aggregating the results
|
* Creates an {@link Operation} from an incoming {@link HttpRequest} by aggregating the results
|
||||||
* of a {@link TargetExtractor}, {@link PreferenceParser}, and {@link BodyParser}.
|
* of a {@link TargetExtractor}, {@link PreferenceParser}, {@link MetadataExtractor}, and {@link BodyParser}.
|
||||||
*/
|
*/
|
||||||
export class BasicRequestParser extends RequestParser {
|
export class BasicRequestParser extends RequestParser {
|
||||||
private readonly targetExtractor!: TargetExtractor;
|
private readonly targetExtractor!: TargetExtractor;
|
||||||
private readonly preferenceParser!: PreferenceParser;
|
private readonly preferenceParser!: PreferenceParser;
|
||||||
|
private readonly metadataExtractor!: MetadataExtractor;
|
||||||
private readonly bodyParser!: BodyParser;
|
private readonly bodyParser!: BodyParser;
|
||||||
|
|
||||||
public constructor(args: SimpleRequestParserArgs) {
|
public constructor(args: SimpleRequestParserArgs) {
|
||||||
@ -38,7 +41,8 @@ export class BasicRequestParser extends RequestParser {
|
|||||||
}
|
}
|
||||||
const target = await this.targetExtractor.handleSafe(input);
|
const target = await this.targetExtractor.handleSafe(input);
|
||||||
const preferences = await this.preferenceParser.handleSafe(input);
|
const preferences = await this.preferenceParser.handleSafe(input);
|
||||||
const body = await this.bodyParser.handleSafe(input);
|
const metadata = await this.metadataExtractor.handleSafe(input);
|
||||||
|
const body = await this.bodyParser.handleSafe({ request: input, metadata });
|
||||||
|
|
||||||
return { method: input.method, target, preferences, body };
|
return { method: input.method, target, preferences, body };
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,22 @@
|
|||||||
import type { HttpRequest } from '../../server/HttpRequest';
|
import type { HttpRequest } from '../../server/HttpRequest';
|
||||||
import { AsyncHandler } from '../../util/AsyncHandler';
|
import { AsyncHandler } from '../../util/AsyncHandler';
|
||||||
import type { Representation } from '../representation/Representation';
|
import type { Representation } from '../representation/Representation';
|
||||||
|
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
||||||
|
|
||||||
|
export interface BodyParserArgs {
|
||||||
|
/**
|
||||||
|
* Request that contains the (potential) body.
|
||||||
|
*/
|
||||||
|
request: HttpRequest;
|
||||||
|
/**
|
||||||
|
* Metadata that has already been parsed from the request.
|
||||||
|
* Can be updated by the BodyParser with extra metadata.
|
||||||
|
*/
|
||||||
|
metadata: RepresentationMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the body of an incoming {@link HttpRequest} and converts it to a {@link Representation}.
|
* Parses the body of an incoming {@link HttpRequest} and converts it to a {@link Representation}.
|
||||||
*/
|
*/
|
||||||
export abstract class BodyParser extends AsyncHandler<HttpRequest, Representation | undefined> {}
|
export abstract class BodyParser extends
|
||||||
|
AsyncHandler<BodyParserArgs, Representation | undefined> {}
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import type { HttpRequest } from '../../server/HttpRequest';
|
|
||||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||||
import { CONTENT_TYPE, HTTP, RDF } from '../../util/UriConstants';
|
|
||||||
import type { Representation } from '../representation/Representation';
|
import type { Representation } from '../representation/Representation';
|
||||||
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
import type { BodyParserArgs } from './BodyParser';
|
||||||
import { BodyParser } from './BodyParser';
|
import { BodyParser } from './BodyParser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts incoming {@link HttpRequest} to a Representation without any further parsing.
|
* Converts incoming {@link HttpRequest} to a Representation without any further parsing.
|
||||||
* Naively parses the mediatype from the content-type header.
|
|
||||||
* Some other metadata is also generated, but this should probably be done in an external handler.
|
|
||||||
*/
|
*/
|
||||||
export class RawBodyParser extends BodyParser {
|
export class RawBodyParser extends BodyParser {
|
||||||
public async canHandle(): Promise<void> {
|
public async canHandle(): Promise<void> {
|
||||||
@ -17,56 +13,23 @@ export class RawBodyParser extends BodyParser {
|
|||||||
|
|
||||||
// Note that the only reason this is a union is in case the body is empty.
|
// Note that the only reason this is a union is in case the body is empty.
|
||||||
// If this check gets moved away from the BodyParsers this union could be removed
|
// If this check gets moved away from the BodyParsers this union could be removed
|
||||||
public async handle(input: HttpRequest): Promise<Representation | undefined> {
|
public async handle({ request, metadata }: BodyParserArgs): Promise<Representation | undefined> {
|
||||||
// RFC7230, §3.3: The presence of a message body in a request
|
// RFC7230, §3.3: The presence of a message body in a request
|
||||||
// is signaled by a Content-Length or Transfer-Encoding header field.
|
// is signaled by a Content-Length or Transfer-Encoding header field.
|
||||||
if (!input.headers['content-length'] && !input.headers['transfer-encoding']) {
|
if (!request.headers['content-length'] && !request.headers['transfer-encoding']) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// While RFC7231 allows treating a body without content type as an octet stream,
|
// While RFC7231 allows treating a body without content type as an octet stream,
|
||||||
// such an omission likely signals a mistake, so force clients to make this explicit.
|
// such an omission likely signals a mistake, so force clients to make this explicit.
|
||||||
if (!input.headers['content-type']) {
|
if (!request.headers['content-type']) {
|
||||||
throw new UnsupportedHttpError('An HTTP request body was passed without Content-Type header');
|
throw new UnsupportedHttpError('An HTTP request body was passed without Content-Type header');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
binary: true,
|
binary: true,
|
||||||
data: input,
|
data: request,
|
||||||
metadata: this.parseMetadata(input),
|
metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseMetadata(input: HttpRequest): RepresentationMetadata {
|
|
||||||
const contentType = /^[^;]*/u.exec(input.headers['content-type']!)![0];
|
|
||||||
|
|
||||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: contentType });
|
|
||||||
|
|
||||||
const { link, slug } = input.headers;
|
|
||||||
|
|
||||||
if (slug) {
|
|
||||||
if (Array.isArray(slug)) {
|
|
||||||
throw new UnsupportedHttpError('At most 1 slug header is allowed.');
|
|
||||||
}
|
|
||||||
metadata.set(HTTP.slug, slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
// There are similarities here to Accept header parsing so that library should become more generic probably
|
|
||||||
if (link) {
|
|
||||||
const linkArray = Array.isArray(link) ? link : [ link ];
|
|
||||||
const parsedLinks = linkArray.map((entry): { url: string; rel: string } => {
|
|
||||||
const [ , url, rest ] = /^<([^>]*)>(.*)$/u.exec(entry) ?? [];
|
|
||||||
const [ , rel ] = /^ *; *rel="(.*)"$/u.exec(rest) ?? [];
|
|
||||||
return { url, rel };
|
|
||||||
});
|
|
||||||
for (const entry of parsedLinks) {
|
|
||||||
if (entry.rel === 'type') {
|
|
||||||
metadata.set(RDF.type, entry.url);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,50 @@
|
|||||||
import { PassThrough } from 'stream';
|
import { PassThrough } from 'stream';
|
||||||
|
import type { Algebra } from 'sparqlalgebrajs';
|
||||||
import { translate } from 'sparqlalgebrajs';
|
import { translate } from 'sparqlalgebrajs';
|
||||||
import type { HttpRequest } from '../../server/HttpRequest';
|
|
||||||
import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes';
|
import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes';
|
||||||
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 { CONTENT_TYPE } from '../../util/UriConstants';
|
import { pipeStreamsAndErrors, readableToString } from '../../util/Util';
|
||||||
import { readableToString } from '../../util/Util';
|
import type { BodyParserArgs } from './BodyParser';
|
||||||
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
|
||||||
import { BodyParser } from './BodyParser';
|
import { BodyParser } from './BodyParser';
|
||||||
import type { SparqlUpdatePatch } from './SparqlUpdatePatch';
|
import type { SparqlUpdatePatch } from './SparqlUpdatePatch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link BodyParser} that supports `application/sparql-update` content.
|
* {@link BodyParser} that supports `application/sparql-update` content.
|
||||||
* Will convert the incoming update string to algebra in a {@link SparqlUpdatePatch}.
|
* Will convert the incoming update string to algebra in a {@link SparqlUpdatePatch}.
|
||||||
* Still needs access to a handler for parsing metadata.
|
|
||||||
*/
|
*/
|
||||||
export class SparqlUpdateBodyParser extends BodyParser {
|
export class SparqlUpdateBodyParser extends BodyParser {
|
||||||
public async canHandle(input: HttpRequest): Promise<void> {
|
public async canHandle({ request }: BodyParserArgs): Promise<void> {
|
||||||
if (input.headers['content-type'] !== APPLICATION_SPARQL_UPDATE) {
|
if (request.headers['content-type'] !== APPLICATION_SPARQL_UPDATE) {
|
||||||
throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.');
|
throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(input: HttpRequest): Promise<SparqlUpdatePatch> {
|
public async handle({ request, metadata }: BodyParserArgs): Promise<SparqlUpdatePatch> {
|
||||||
|
// Note that readableObjectMode is only defined starting from Node 12
|
||||||
|
// It is impossible to check if object mode is enabled in Node 10 (without accessing private variables)
|
||||||
|
const options = { objectMode: request.readableObjectMode };
|
||||||
|
const toAlgebraStream = new PassThrough(options);
|
||||||
|
const dataCopy = new PassThrough(options);
|
||||||
|
pipeStreamsAndErrors(request, toAlgebraStream);
|
||||||
|
pipeStreamsAndErrors(request, dataCopy);
|
||||||
|
let algebra: Algebra.Operation;
|
||||||
try {
|
try {
|
||||||
// Note that readableObjectMode is only defined starting from Node 12
|
|
||||||
// It is impossible to check if object mode is enabled in Node 10 (without accessing private variables)
|
|
||||||
const options = { objectMode: input.readableObjectMode };
|
|
||||||
const toAlgebraStream = new PassThrough(options);
|
|
||||||
const dataCopy = new PassThrough(options);
|
|
||||||
input.pipe(toAlgebraStream);
|
|
||||||
input.pipe(dataCopy);
|
|
||||||
const sparql = await readableToString(toAlgebraStream);
|
const sparql = await readableToString(toAlgebraStream);
|
||||||
const algebra = translate(sparql, { quads: true });
|
algebra = translate(sparql, { quads: true });
|
||||||
|
|
||||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_SPARQL_UPDATE });
|
|
||||||
|
|
||||||
// Prevent body from being requested again
|
|
||||||
return {
|
|
||||||
algebra,
|
|
||||||
binary: true,
|
|
||||||
data: dataCopy,
|
|
||||||
metadata,
|
|
||||||
};
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
throw new UnsupportedHttpError(error.message);
|
throw new UnsupportedHttpError(error.message);
|
||||||
}
|
}
|
||||||
throw new UnsupportedHttpError();
|
throw new UnsupportedHttpError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent body from being requested again
|
||||||
|
return {
|
||||||
|
algebra,
|
||||||
|
binary: true,
|
||||||
|
data: dataCopy,
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,13 @@ import {
|
|||||||
} from '../../index';
|
} from '../../index';
|
||||||
|
|
||||||
import type { ServerConfig } from './ServerConfig';
|
import type { ServerConfig } from './ServerConfig';
|
||||||
import { getInMemoryResourceStore,
|
import {
|
||||||
|
getInMemoryResourceStore,
|
||||||
getOperationHandler,
|
getOperationHandler,
|
||||||
getConvertingStore,
|
getConvertingStore,
|
||||||
getPatchingStore, getBasicRequestParser } from './Util';
|
getPatchingStore,
|
||||||
|
getBasicRequestParser,
|
||||||
|
} from './Util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BasicHandlersConfig works with
|
* BasicHandlersConfig works with
|
||||||
|
@ -12,7 +12,12 @@ import {
|
|||||||
UnsecureWebIdExtractor,
|
UnsecureWebIdExtractor,
|
||||||
} from '../../index';
|
} from '../../index';
|
||||||
import type { ServerConfig } from './ServerConfig';
|
import type { ServerConfig } from './ServerConfig';
|
||||||
import { getFileResourceStore, getOperationHandler, getConvertingStore, getBasicRequestParser } from './Util';
|
import {
|
||||||
|
getFileResourceStore,
|
||||||
|
getOperationHandler,
|
||||||
|
getConvertingStore,
|
||||||
|
getBasicRequestParser,
|
||||||
|
} from './Util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileResourceStoreConfig works with
|
* FileResourceStoreConfig works with
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import type { BodyParser,
|
import type { BodyParser,
|
||||||
HttpRequest,
|
|
||||||
Operation,
|
Operation,
|
||||||
Representation,
|
|
||||||
RepresentationConverter,
|
RepresentationConverter,
|
||||||
ResourceStore,
|
ResourceStore,
|
||||||
ResponseDescription } from '../../index';
|
ResponseDescription } from '../../index';
|
||||||
import {
|
import {
|
||||||
AcceptPreferenceParser,
|
AcceptPreferenceParser,
|
||||||
|
BasicMetadataExtractor,
|
||||||
BasicRequestParser,
|
BasicRequestParser,
|
||||||
BasicTargetExtractor,
|
BasicTargetExtractor,
|
||||||
CompositeAsyncHandler,
|
CompositeAsyncHandler,
|
||||||
|
ContentTypeParser,
|
||||||
DeleteOperationHandler,
|
DeleteOperationHandler,
|
||||||
FileResourceStore,
|
FileResourceStore,
|
||||||
GetOperationHandler,
|
GetOperationHandler,
|
||||||
HeadOperationHandler,
|
HeadOperationHandler,
|
||||||
InMemoryResourceStore,
|
InMemoryResourceStore,
|
||||||
InteractionController,
|
InteractionController,
|
||||||
|
LinkTypeParser,
|
||||||
MetadataController,
|
MetadataController,
|
||||||
PatchingStore,
|
PatchingStore,
|
||||||
PatchOperationHandler,
|
PatchOperationHandler,
|
||||||
@ -25,6 +26,7 @@ import {
|
|||||||
RawBodyParser,
|
RawBodyParser,
|
||||||
RepresentationConvertingStore,
|
RepresentationConvertingStore,
|
||||||
SingleThreadedResourceLocker,
|
SingleThreadedResourceLocker,
|
||||||
|
SlugParser,
|
||||||
SparqlUpdatePatchHandler,
|
SparqlUpdatePatchHandler,
|
||||||
UrlBasedAclManager,
|
UrlBasedAclManager,
|
||||||
UrlContainerManager,
|
UrlContainerManager,
|
||||||
@ -104,6 +106,15 @@ export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler
|
|||||||
return new CompositeAsyncHandler<Operation, ResponseDescription>(handlers);
|
return new CompositeAsyncHandler<Operation, ResponseDescription>(handlers);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BasicMetadataExtractor with parsers for content-type, slugs and link types.
|
||||||
|
*/
|
||||||
|
export const getBasicMetadataExtractor = (): BasicMetadataExtractor => new BasicMetadataExtractor([
|
||||||
|
new ContentTypeParser(),
|
||||||
|
new SlugParser(),
|
||||||
|
new LinkTypeParser(),
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives a basic request parser based on some body parses.
|
* Gives a basic request parser based on some body parses.
|
||||||
* @param bodyParsers - Optional list of body parsers, default is RawBodyParser.
|
* @param bodyParsers - Optional list of body parsers, default is RawBodyParser.
|
||||||
@ -118,11 +129,12 @@ export const getBasicRequestParser = (bodyParsers: BodyParser[] = []): BasicRequ
|
|||||||
// If no body parser is given (array is empty), default to RawBodyParser
|
// If no body parser is given (array is empty), default to RawBodyParser
|
||||||
bodyParser = new RawBodyParser();
|
bodyParser = new RawBodyParser();
|
||||||
} else {
|
} else {
|
||||||
bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>(bodyParsers);
|
bodyParser = new CompositeAsyncHandler(bodyParsers);
|
||||||
}
|
}
|
||||||
return new BasicRequestParser({
|
return new BasicRequestParser({
|
||||||
targetExtractor: new BasicTargetExtractor(),
|
targetExtractor: new BasicTargetExtractor(),
|
||||||
preferenceParser: new AcceptPreferenceParser(),
|
preferenceParser: new AcceptPreferenceParser(),
|
||||||
|
metadataExtractor: getBasicMetadataExtractor(),
|
||||||
bodyParser,
|
bodyParser,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -4,15 +4,18 @@ import streamifyArray from 'streamify-array';
|
|||||||
import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
|
import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
|
||||||
import { BasicRequestParser } from '../../src/ldp/http/BasicRequestParser';
|
import { BasicRequestParser } from '../../src/ldp/http/BasicRequestParser';
|
||||||
import { BasicTargetExtractor } from '../../src/ldp/http/BasicTargetExtractor';
|
import { BasicTargetExtractor } from '../../src/ldp/http/BasicTargetExtractor';
|
||||||
|
import { BasicMetadataExtractor } from '../../src/ldp/http/metadata/BasicMetadataExtractor';
|
||||||
|
import { ContentTypeParser } from '../../src/ldp/http/metadata/ContentTypeParser';
|
||||||
import { RawBodyParser } from '../../src/ldp/http/RawBodyParser';
|
import { RawBodyParser } from '../../src/ldp/http/RawBodyParser';
|
||||||
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
|
||||||
import type { HttpRequest } from '../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../src/server/HttpRequest';
|
||||||
|
|
||||||
describe('A BasicRequestParser with simple input parsers', (): void => {
|
describe('A BasicRequestParser with simple input parsers', (): void => {
|
||||||
const targetExtractor = new BasicTargetExtractor();
|
const targetExtractor = new BasicTargetExtractor();
|
||||||
const bodyParser = new RawBodyParser();
|
|
||||||
const preferenceParser = new AcceptPreferenceParser();
|
const preferenceParser = new AcceptPreferenceParser();
|
||||||
const requestParser = new BasicRequestParser({ targetExtractor, bodyParser, preferenceParser });
|
const metadataExtractor = new BasicMetadataExtractor([ new ContentTypeParser() ]);
|
||||||
|
const bodyParser = new RawBodyParser();
|
||||||
|
const requestParser = new BasicRequestParser({ targetExtractor, preferenceParser, metadataExtractor, bodyParser });
|
||||||
|
|
||||||
it('can parse an incoming request.', async(): Promise<void> => {
|
it('can parse an incoming request.', async(): Promise<void> => {
|
||||||
const request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
|
const request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
import { BasicRequestParser } from '../../../../src/ldp/http/BasicRequestParser';
|
import { BasicRequestParser } from '../../../../src/ldp/http/BasicRequestParser';
|
||||||
import type { BodyParser } from '../../../../src/ldp/http/BodyParser';
|
import type { BodyParser } from '../../../../src/ldp/http/BodyParser';
|
||||||
|
import type { MetadataExtractor } from '../../../../src/ldp/http/metadata/MetadataExtractor';
|
||||||
import type { PreferenceParser } from '../../../../src/ldp/http/PreferenceParser';
|
import type { PreferenceParser } from '../../../../src/ldp/http/PreferenceParser';
|
||||||
import type { TargetExtractor } from '../../../../src/ldp/http/TargetExtractor';
|
import type { TargetExtractor } from '../../../../src/ldp/http/TargetExtractor';
|
||||||
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
|
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
|
||||||
|
|
||||||
describe('A BasicRequestParser', (): void => {
|
describe('A BasicRequestParser', (): void => {
|
||||||
let targetExtractor: TargetExtractor;
|
let targetExtractor: TargetExtractor;
|
||||||
let bodyParser: BodyParser;
|
|
||||||
let preferenceParser: PreferenceParser;
|
let preferenceParser: PreferenceParser;
|
||||||
|
let metadataExtractor: MetadataExtractor;
|
||||||
|
let bodyParser: BodyParser;
|
||||||
let requestParser: BasicRequestParser;
|
let requestParser: BasicRequestParser;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
targetExtractor = new StaticAsyncHandler(true, 'target' as any);
|
targetExtractor = new StaticAsyncHandler(true, 'target' as any);
|
||||||
bodyParser = new StaticAsyncHandler(true, 'body' as any);
|
|
||||||
preferenceParser = new StaticAsyncHandler(true, 'preference' as any);
|
preferenceParser = new StaticAsyncHandler(true, 'preference' as any);
|
||||||
requestParser = new BasicRequestParser({ targetExtractor, bodyParser, preferenceParser });
|
metadataExtractor = new StaticAsyncHandler(true, 'metadata' as any);
|
||||||
|
bodyParser = new StaticAsyncHandler(true, 'body' as any);
|
||||||
|
requestParser = new BasicRequestParser({ targetExtractor, preferenceParser, metadataExtractor, bodyParser });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle any input.', async(): Promise<void> => {
|
it('can handle any input.', async(): Promise<void> => {
|
||||||
@ -26,11 +29,12 @@ describe('A BasicRequestParser', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns the output of all input parsers after calling handle.', async(): Promise<void> => {
|
it('returns the output of all input parsers after calling handle.', async(): Promise<void> => {
|
||||||
|
bodyParser.handle = ({ metadata }): any => ({ data: 'body', metadata });
|
||||||
await expect(requestParser.handle({ url: 'url', method: 'GET' } as any)).resolves.toEqual({
|
await expect(requestParser.handle({ url: 'url', method: 'GET' } as any)).resolves.toEqual({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
target: 'target',
|
target: 'target',
|
||||||
preferences: 'preference',
|
preferences: 'preference',
|
||||||
body: 'body',
|
body: { data: 'body', metadata: 'metadata' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,87 +1,54 @@
|
|||||||
import arrayifyStream from 'arrayify-stream';
|
import arrayifyStream from 'arrayify-stream';
|
||||||
import streamifyArray from 'streamify-array';
|
import streamifyArray from 'streamify-array';
|
||||||
|
import type { BodyParserArgs } from '../../../../src/ldp/http/BodyParser';
|
||||||
import { RawBodyParser } from '../../../../src/ldp/http/RawBodyParser';
|
import { RawBodyParser } from '../../../../src/ldp/http/RawBodyParser';
|
||||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||||
import type { HttpRequest } from '../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../src/server/HttpRequest';
|
||||||
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
|
|
||||||
import 'jest-rdf';
|
import 'jest-rdf';
|
||||||
import { HTTP, RDF } from '../../../../src/util/UriConstants';
|
|
||||||
|
|
||||||
describe('A RawBodyparser', (): void => {
|
describe('A RawBodyparser', (): void => {
|
||||||
const bodyParser = new RawBodyParser();
|
const bodyParser = new RawBodyParser();
|
||||||
|
let input: BodyParserArgs;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
input = { request: { headers: {}} as HttpRequest, metadata: new RepresentationMetadata() };
|
||||||
|
});
|
||||||
|
|
||||||
it('accepts all input.', async(): Promise<void> => {
|
it('accepts all input.', async(): Promise<void> => {
|
||||||
await expect(bodyParser.canHandle()).resolves.toBeUndefined();
|
await expect(bodyParser.canHandle()).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty output if there was no content length or transfer encoding.', async(): Promise<void> => {
|
it('returns empty output if there was no content length or transfer encoding.', async(): Promise<void> => {
|
||||||
const input = streamifyArray([ '' ]) as HttpRequest;
|
input.request = streamifyArray([ '' ]) as HttpRequest;
|
||||||
input.headers = {};
|
input.request.headers = {};
|
||||||
await expect(bodyParser.handle(input)).resolves.toBeUndefined();
|
await expect(bodyParser.handle(input)).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors when a content length was specified without content type.', async(): Promise<void> => {
|
it('errors when a content length was specified without content type.', async(): Promise<void> => {
|
||||||
const input = streamifyArray([ 'abc' ]) as HttpRequest;
|
input.request = streamifyArray([ 'abc' ]) as HttpRequest;
|
||||||
input.headers = { 'content-length': '0' };
|
input.request.headers = { 'content-length': '0' };
|
||||||
await expect(bodyParser.handle(input)).rejects
|
await expect(bodyParser.handle(input)).rejects
|
||||||
.toThrow('An HTTP request body was passed without Content-Type header');
|
.toThrow('An HTTP request body was passed without Content-Type header');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors when a transfer encoding was specified without content type.', async(): Promise<void> => {
|
it('errors when a transfer encoding was specified without content type.', async(): Promise<void> => {
|
||||||
const input = streamifyArray([ 'abc' ]) as HttpRequest;
|
input.request = streamifyArray([ 'abc' ]) as HttpRequest;
|
||||||
input.headers = { 'transfer-encoding': 'chunked' };
|
input.request.headers = { 'transfer-encoding': 'chunked' };
|
||||||
await expect(bodyParser.handle(input)).rejects
|
await expect(bodyParser.handle(input)).rejects
|
||||||
.toThrow('An HTTP request body was passed without Content-Type header');
|
.toThrow('An HTTP request body was passed without Content-Type header');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a Representation if there was data.', async(): Promise<void> => {
|
it('returns a Representation if there was data.', async(): Promise<void> => {
|
||||||
const input = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
|
input.request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
|
||||||
input.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle' };
|
input.request.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle' };
|
||||||
const result = (await bodyParser.handle(input))!;
|
const result = (await bodyParser.handle(input))!;
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
binary: true,
|
binary: true,
|
||||||
data: input,
|
data: input.request,
|
||||||
metadata: expect.any(RepresentationMetadata),
|
metadata: input.metadata,
|
||||||
});
|
});
|
||||||
expect(result.metadata.contentType).toEqual('text/turtle');
|
|
||||||
await expect(arrayifyStream(result.data)).resolves.toEqual(
|
await expect(arrayifyStream(result.data)).resolves.toEqual(
|
||||||
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
|
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds the slug header to the metadata.', async(): Promise<void> => {
|
|
||||||
const input = {} as HttpRequest;
|
|
||||||
input.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle', slug: 'slugText' };
|
|
||||||
const result = (await bodyParser.handle(input))!;
|
|
||||||
expect(result.metadata.contentType).toEqual('text/turtle');
|
|
||||||
expect(result.metadata.get(HTTP.slug)?.value).toEqual('slugText');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors if there are multiple slugs.', async(): Promise<void> => {
|
|
||||||
const input = {} as HttpRequest;
|
|
||||||
input.headers = { 'transfer-encoding': 'chunked',
|
|
||||||
'content-type': 'text/turtle',
|
|
||||||
slug: [ 'slugTextA', 'slugTextB' ]};
|
|
||||||
await expect(bodyParser.handle(input)).rejects.toThrow(UnsupportedHttpError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds the link type headers to the metadata.', async(): Promise<void> => {
|
|
||||||
const input = {} as HttpRequest;
|
|
||||||
input.headers = { 'transfer-encoding': 'chunked',
|
|
||||||
'content-type': 'text/turtle',
|
|
||||||
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"' };
|
|
||||||
const result = (await bodyParser.handle(input))!;
|
|
||||||
expect(result.metadata.contentType).toEqual('text/turtle');
|
|
||||||
expect(result.metadata.get(RDF.type)?.value).toEqual('http://www.w3.org/ns/ldp#Container');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores unknown link headers.', async(): Promise<void> => {
|
|
||||||
const input = {} as HttpRequest;
|
|
||||||
input.headers = { 'transfer-encoding': 'chunked',
|
|
||||||
'content-type': 'text/turtle',
|
|
||||||
link: [ '<unrelatedLink>', 'badLink' ]};
|
|
||||||
const result = (await bodyParser.handle(input))!;
|
|
||||||
expect(result.metadata.quads()).toHaveLength(1);
|
|
||||||
expect(result.metadata.contentType).toEqual('text/turtle');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,43 +1,52 @@
|
|||||||
import { namedNode, quad } from '@rdfjs/data-model';
|
import { namedNode, quad } from '@rdfjs/data-model';
|
||||||
import arrayifyStream from 'arrayify-stream';
|
|
||||||
import { Algebra } from 'sparqlalgebrajs';
|
import { Algebra } from 'sparqlalgebrajs';
|
||||||
import * as algebra from 'sparqlalgebrajs';
|
import * as algebra from 'sparqlalgebrajs';
|
||||||
import streamifyArray from 'streamify-array';
|
import streamifyArray from 'streamify-array';
|
||||||
|
import type { BodyParserArgs } from '../../../../src/ldp/http/BodyParser';
|
||||||
import { SparqlUpdateBodyParser } from '../../../../src/ldp/http/SparqlUpdateBodyParser';
|
import { SparqlUpdateBodyParser } from '../../../../src/ldp/http/SparqlUpdateBodyParser';
|
||||||
|
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||||
import type { HttpRequest } from '../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../src/server/HttpRequest';
|
||||||
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 { readableToString } from '../../../../src/util/Util';
|
||||||
|
|
||||||
describe('A SparqlUpdateBodyParser', (): void => {
|
describe('A SparqlUpdateBodyParser', (): void => {
|
||||||
const bodyParser = new SparqlUpdateBodyParser();
|
const bodyParser = new SparqlUpdateBodyParser();
|
||||||
|
let input: BodyParserArgs;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
input = { request: { headers: {}} as HttpRequest, metadata: new RepresentationMetadata() };
|
||||||
|
});
|
||||||
|
|
||||||
it('only accepts application/sparql-update content.', async(): Promise<void> => {
|
it('only accepts application/sparql-update content.', async(): Promise<void> => {
|
||||||
await expect(bodyParser.canHandle({ headers: {}} as HttpRequest)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
||||||
await expect(bodyParser.canHandle({ headers: { 'content-type': 'text/plain' }} as HttpRequest))
|
input.request.headers = { 'content-type': 'text/plain' };
|
||||||
.rejects.toThrow(UnsupportedMediaTypeHttpError);
|
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
||||||
await expect(bodyParser.canHandle({ headers: { 'content-type': 'application/sparql-update' }} as HttpRequest))
|
input.request.headers = { 'content-type': 'application/sparql-update' };
|
||||||
.resolves.toBeUndefined();
|
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors when handling invalid SPARQL updates.', async(): Promise<void> => {
|
it('errors when handling invalid SPARQL updates.', async(): Promise<void> => {
|
||||||
await expect(bodyParser.handle(streamifyArray([ 'VERY INVALID UPDATE' ]) as HttpRequest))
|
input.request = streamifyArray([ 'VERY INVALID UPDATE' ]) as HttpRequest;
|
||||||
.rejects.toThrow(UnsupportedHttpError);
|
await expect(bodyParser.handle(input)).rejects.toThrow(UnsupportedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors when receiving an unexpected error.', async(): Promise<void> => {
|
it('errors when receiving an unexpected error.', async(): Promise<void> => {
|
||||||
const mock = jest.spyOn(algebra, 'translate').mockImplementationOnce((): any => {
|
const mock = jest.spyOn(algebra, 'translate').mockImplementationOnce((): any => {
|
||||||
throw 'apple';
|
throw 'apple';
|
||||||
});
|
});
|
||||||
await expect(bodyParser.handle(streamifyArray(
|
input.request = streamifyArray(
|
||||||
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}' ],
|
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }' ],
|
||||||
) as HttpRequest)).rejects.toThrow(UnsupportedHttpError);
|
) as HttpRequest;
|
||||||
|
await expect(bodyParser.handle(input)).rejects.toThrow(UnsupportedHttpError);
|
||||||
mock.mockRestore();
|
mock.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('converts SPARQL updates to algebra.', async(): Promise<void> => {
|
it('converts SPARQL updates to algebra.', async(): Promise<void> => {
|
||||||
const result = await bodyParser.handle(streamifyArray(
|
input.request = streamifyArray(
|
||||||
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}' ],
|
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }' ],
|
||||||
) as HttpRequest);
|
) as HttpRequest;
|
||||||
|
const result = await bodyParser.handle(input);
|
||||||
expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT);
|
expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT);
|
||||||
expect((result.algebra as Algebra.DeleteInsert).delete).toBeRdfIsomorphic([ quad(
|
expect((result.algebra as Algebra.DeleteInsert).delete).toBeRdfIsomorphic([ quad(
|
||||||
namedNode('http://test.com/s'),
|
namedNode('http://test.com/s'),
|
||||||
@ -45,11 +54,11 @@ describe('A SparqlUpdateBodyParser', (): void => {
|
|||||||
namedNode('http://test.com/o'),
|
namedNode('http://test.com/o'),
|
||||||
) ]);
|
) ]);
|
||||||
expect(result.binary).toBe(true);
|
expect(result.binary).toBe(true);
|
||||||
expect(result.metadata.contentType).toEqual('application/sparql-update');
|
expect(result.metadata).toBe(input.metadata);
|
||||||
|
|
||||||
// Workaround for Node 10 not exposing objectMode
|
// Workaround for Node 10 not exposing objectMode
|
||||||
expect((await arrayifyStream(result.data)).join('')).toEqual(
|
expect(await readableToString(result.data)).toEqual(
|
||||||
'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o>}',
|
'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user