mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add ConstantConverter.
This commit is contained in:
parent
a21532ebf8
commit
5416d66a31
@ -131,6 +131,7 @@ export * from './storage/accessors/SparqlDataAccessor';
|
|||||||
|
|
||||||
// Storage/Conversion
|
// Storage/Conversion
|
||||||
export * from './storage/conversion/ChainedConverter';
|
export * from './storage/conversion/ChainedConverter';
|
||||||
|
export * from './storage/conversion/ConstantConverter';
|
||||||
export * from './storage/conversion/ContentTypeReplacer';
|
export * from './storage/conversion/ContentTypeReplacer';
|
||||||
export * from './storage/conversion/IfNeededConverter';
|
export * from './storage/conversion/IfNeededConverter';
|
||||||
export * from './storage/conversion/PassthroughConverter';
|
export * from './storage/conversion/PassthroughConverter';
|
||||||
|
58
src/storage/conversion/ConstantConverter.ts
Normal file
58
src/storage/conversion/ConstantConverter.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
|
||||||
|
import type { Representation } from '../../ldp/representation/Representation';
|
||||||
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
|
import { hasMatchingMediaTypes, matchesMediaType } from './ConversionUtil';
|
||||||
|
import { RepresentationConverter } from './RepresentationConverter';
|
||||||
|
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RepresentationConverter} that ensures
|
||||||
|
* a representation for a certain content type is available.
|
||||||
|
*
|
||||||
|
* Representations of the same content type are served as is;
|
||||||
|
* others are replaced by a constant document.
|
||||||
|
*
|
||||||
|
* This can for example be used to serve an index.html file,
|
||||||
|
* which could then interactively load another representation.
|
||||||
|
*/
|
||||||
|
export class ConstantConverter extends RepresentationConverter {
|
||||||
|
private readonly filePath: string;
|
||||||
|
private readonly contentType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new constant converter.
|
||||||
|
*
|
||||||
|
* @param filePath - The path to the constant representation.
|
||||||
|
* @param contentType - The content type of the constant representation.
|
||||||
|
*/
|
||||||
|
public constructor(filePath: string, contentType: string) {
|
||||||
|
super();
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async canHandle({ preferences, representation }: RepresentationConverterArgs): Promise<void> {
|
||||||
|
// Do not replace the representation if there is no preference for our content type
|
||||||
|
if (!preferences.type) {
|
||||||
|
throw new NotImplementedHttpError('No content type preferences specified');
|
||||||
|
}
|
||||||
|
if (!hasMatchingMediaTypes({ ...preferences.type, '*/*': 0 }, { [this.contentType]: 1 })) {
|
||||||
|
throw new NotImplementedHttpError(`No preference for ${this.contentType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not replace the representation if it already has our content type
|
||||||
|
if (matchesMediaType(representation.metadata.contentType ?? '', this.contentType)) {
|
||||||
|
throw new NotImplementedHttpError(`Representation is already ${this.contentType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||||
|
// Ignore the original representation
|
||||||
|
representation.data.destroy();
|
||||||
|
|
||||||
|
// Create a new representation from the constant file
|
||||||
|
const data = await fs.readFile(this.filePath, 'utf8');
|
||||||
|
return new BasicRepresentation(data, representation.metadata, this.contentType);
|
||||||
|
}
|
||||||
|
}
|
@ -55,6 +55,21 @@ string[] {
|
|||||||
.map(([ type ]): string => type);
|
.map(([ type ]): string => type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether any available type satisfies the preferences.
|
||||||
|
*
|
||||||
|
* @param preferredTypes - Preferences for output type.
|
||||||
|
* @param availableTypes - Media types to compare to the preferences.
|
||||||
|
*
|
||||||
|
* @throws BadRequestHttpError
|
||||||
|
* If the type preferences are undefined or if there are duplicate preferences.
|
||||||
|
*
|
||||||
|
* @returns Whether there is at least one preference match.
|
||||||
|
*/
|
||||||
|
export function hasMatchingMediaTypes(preferredTypes?: ValuePreferences, availableTypes?: ValuePreferences): boolean {
|
||||||
|
return matchingMediaTypes(preferredTypes, availableTypes).length !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given two media types/ranges match each other.
|
* Checks if the given two media types/ranges match each other.
|
||||||
* Takes wildcards into account.
|
* Takes wildcards into account.
|
||||||
|
@ -2,7 +2,7 @@ import type { Representation } from '../../ldp/representation/Representation';
|
|||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||||
import { UnsupportedAsyncHandler } from '../../util/UnsupportedAsyncHandler';
|
import { UnsupportedAsyncHandler } from '../../util/UnsupportedAsyncHandler';
|
||||||
import { matchingMediaTypes } from './ConversionUtil';
|
import { hasMatchingMediaTypes } from './ConversionUtil';
|
||||||
import { RepresentationConverter } from './RepresentationConverter';
|
import { RepresentationConverter } from './RepresentationConverter';
|
||||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ export class IfNeededConverter extends RepresentationConverter {
|
|||||||
if (!contentType) {
|
if (!contentType) {
|
||||||
throw new InternalServerError('Content-Type is required for data conversion.');
|
throw new InternalServerError('Content-Type is required for data conversion.');
|
||||||
}
|
}
|
||||||
const noMatchingMediaType = matchingMediaTypes(preferences.type, { [contentType]: 1 }).length === 0;
|
const noMatchingMediaType = !hasMatchingMediaTypes(preferences.type, { [contentType]: 1 });
|
||||||
if (noMatchingMediaType) {
|
if (noMatchingMediaType) {
|
||||||
this.logger.debug(`Conversion needed for ${identifier
|
this.logger.debug(`Conversion needed for ${identifier
|
||||||
.path} from ${representation.metadata.contentType} to satisfy ${Object.entries(preferences.type)
|
.path} from ${representation.metadata.contentType} to satisfy ${Object.entries(preferences.type)
|
||||||
|
67
test/unit/storage/conversion/ConstantConverter.test.ts
Normal file
67
test/unit/storage/conversion/ConstantConverter.test.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import arrayifyStream from 'arrayify-stream';
|
||||||
|
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||||
|
import { ConstantConverter } from '../../../../src/storage/conversion/ConstantConverter';
|
||||||
|
|
||||||
|
const readFile = jest.spyOn(fs, 'readFile').mockResolvedValue('file contents');
|
||||||
|
|
||||||
|
describe('A ConstantConverter', (): void => {
|
||||||
|
const identifier = { path: 'identifier' };
|
||||||
|
|
||||||
|
const converter = new ConstantConverter('abc/def/index.html', 'text/html');
|
||||||
|
|
||||||
|
it('does not support requests without content type preferences.', async(): Promise<void> => {
|
||||||
|
const preferences = {};
|
||||||
|
const representation = {} as any;
|
||||||
|
const args = { identifier, representation, preferences };
|
||||||
|
|
||||||
|
await expect(converter.canHandle(args)).rejects
|
||||||
|
.toThrow('No content type preferences specified');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not support requests without matching content type preference.', async(): Promise<void> => {
|
||||||
|
const preferences = { type: { 'text/turtle': 1 }};
|
||||||
|
const representation = {} as any;
|
||||||
|
const args = { identifier, representation, preferences };
|
||||||
|
|
||||||
|
await expect(converter.canHandle(args)).rejects
|
||||||
|
.toThrow('No preference for text/html');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not support representations that are already in the right format.', async(): Promise<void> => {
|
||||||
|
const preferences = { type: { 'text/html': 1 }};
|
||||||
|
const metadata = new RepresentationMetadata({ contentType: 'text/html' });
|
||||||
|
const representation = { metadata } as any;
|
||||||
|
const args = { identifier, representation, preferences };
|
||||||
|
|
||||||
|
await expect(converter.canHandle(args)).rejects
|
||||||
|
.toThrow('Representation is already text/html');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports representations with an unknown content type.', async(): Promise<void> => {
|
||||||
|
const preferences = { type: { 'text/html': 1 }};
|
||||||
|
const metadata = new RepresentationMetadata();
|
||||||
|
const representation = { metadata } as any;
|
||||||
|
const args = { identifier, representation, preferences };
|
||||||
|
|
||||||
|
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces the representation of a supported request.', async(): Promise<void> => {
|
||||||
|
const preferences = { type: { 'text/html': 1 }};
|
||||||
|
const metadata = new RepresentationMetadata({ contentType: 'text/turtle' });
|
||||||
|
const representation = { metadata, data: { destroy: jest.fn() }} as any;
|
||||||
|
const args = { identifier, representation, preferences };
|
||||||
|
|
||||||
|
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
||||||
|
const converted = await converter.handle(args);
|
||||||
|
|
||||||
|
expect(representation.data.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(readFile).toHaveBeenCalledTimes(1);
|
||||||
|
expect(readFile).toHaveBeenCalledWith('abc/def/index.html', 'utf8');
|
||||||
|
|
||||||
|
expect(converted.metadata.contentType).toBe('text/html');
|
||||||
|
expect(await arrayifyStream(converted.data)).toEqual([ 'file contents' ]);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user