mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Update RepresentationConvertingStore to convert incoming data
This commit is contained in:
parent
9f7c246104
commit
712a690904
@ -9,9 +9,15 @@ import { PassthroughStore } from './PassthroughStore';
|
|||||||
import type { ResourceStore } from './ResourceStore';
|
import type { ResourceStore } from './ResourceStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store that overrides the `getRepresentation` function.
|
* Store that overrides all functions that take or output a {@link Representation},
|
||||||
* Tries to convert the {@link Representation} it got from the source store
|
* so `getRepresentation`, `addResource`, and `setRepresentation`.
|
||||||
* so it matches one of the given type preferences.
|
*
|
||||||
|
* For incoming representations, they will be converted if an incoming converter and preferences have been set.
|
||||||
|
* The converted Representation will be passed along.
|
||||||
|
*
|
||||||
|
* For outgoing representations, they will be converted if there is an outgoing converter.
|
||||||
|
*
|
||||||
|
* Conversions will only happen if required and will not happen if the Representation is already in the correct format.
|
||||||
*
|
*
|
||||||
* In the future this class should take the preferences of the request into account.
|
* In the future this class should take the preferences of the request into account.
|
||||||
* Even if there is a match with the output from the store,
|
* Even if there is a match with the output from the store,
|
||||||
@ -20,21 +26,44 @@ import type { ResourceStore } from './ResourceStore';
|
|||||||
export class RepresentationConvertingStore<T extends ResourceStore = ResourceStore> extends PassthroughStore<T> {
|
export class RepresentationConvertingStore<T extends ResourceStore = ResourceStore> extends PassthroughStore<T> {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly converter: RepresentationConverter;
|
private readonly inConverter?: RepresentationConverter;
|
||||||
|
private readonly outConverter?: RepresentationConverter;
|
||||||
|
|
||||||
public constructor(source: T, converter: RepresentationConverter) {
|
private readonly inPreferences?: RepresentationPreferences;
|
||||||
|
|
||||||
|
public constructor(source: T, options: {
|
||||||
|
outConverter?: RepresentationConverter;
|
||||||
|
inConverter?: RepresentationConverter;
|
||||||
|
inPreferences?: RepresentationPreferences;
|
||||||
|
}) {
|
||||||
super(source);
|
super(source);
|
||||||
this.converter = converter;
|
this.inConverter = options.inConverter;
|
||||||
|
this.outConverter = options.outConverter;
|
||||||
|
this.inPreferences = options.inPreferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
|
public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
|
||||||
conditions?: Conditions): Promise<Representation> {
|
conditions?: Conditions): Promise<Representation> {
|
||||||
const representation = await super.getRepresentation(identifier, preferences, conditions);
|
const representation = await super.getRepresentation(identifier, preferences, conditions);
|
||||||
if (this.matchesPreferences(representation, preferences)) {
|
if (!this.outConverter || this.matchesPreferences(representation, preferences)) {
|
||||||
return representation;
|
return representation;
|
||||||
}
|
}
|
||||||
this.logger.info(`Convert ${identifier.path} from ${representation.metadata.contentType} to ${preferences.type}`);
|
this.logger.info(`Convert ${identifier.path} from ${representation.metadata.contentType} to ${preferences.type}`);
|
||||||
return this.converter.handleSafe({ identifier, representation, preferences });
|
return this.outConverter.handleSafe({ identifier, representation, preferences });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addResource(container: ResourceIdentifier, representation: Representation,
|
||||||
|
conditions?: Conditions): Promise<ResourceIdentifier> {
|
||||||
|
// We can potentially run into problems here if we convert a turtle document where the base IRI is required,
|
||||||
|
// since we don't know the resource IRI yet at this point.
|
||||||
|
representation = await this.convertRepresentation(container, representation);
|
||||||
|
return this.source.addResource(container, representation, conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
|
||||||
|
conditions?: Conditions): Promise<void> {
|
||||||
|
representation = await this.convertRepresentation(identifier, representation);
|
||||||
|
return this.source.setRepresentation(identifier, representation, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private matchesPreferences(representation: Representation, preferences: RepresentationPreferences): boolean {
|
private matchesPreferences(representation: Representation, preferences: RepresentationPreferences): boolean {
|
||||||
@ -49,4 +78,12 @@ export class RepresentationConvertingStore<T extends ResourceStore = ResourceSto
|
|||||||
matchingMediaType(type.value, contentType)),
|
matchingMediaType(type.value, contentType)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async convertRepresentation(identifier: ResourceIdentifier, representation: Representation):
|
||||||
|
Promise<Representation> {
|
||||||
|
if (!this.inPreferences || !this.inConverter || this.matchesPreferences(representation, this.inPreferences)) {
|
||||||
|
return representation;
|
||||||
|
}
|
||||||
|
return this.inConverter.handleSafe({ identifier, representation, preferences: this.inPreferences });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,11 +76,11 @@ export const getInMemoryResourceStore = (base = BASE): DataAccessorBasedStore =>
|
|||||||
*/
|
*/
|
||||||
export const getConvertingStore =
|
export const getConvertingStore =
|
||||||
(store: ResourceStore, converters: RepresentationConverter[]): RepresentationConvertingStore =>
|
(store: ResourceStore, converters: RepresentationConverter[]): RepresentationConvertingStore =>
|
||||||
new RepresentationConvertingStore(store, new CompositeAsyncHandler(converters));
|
new RepresentationConvertingStore(store, { outConverter: new CompositeAsyncHandler(converters) });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives a patching store based on initial store.
|
* Gives a patching store based on initial store.
|
||||||
* @param store - Inital resource store.
|
* @param store - Initial resource store.
|
||||||
*
|
*
|
||||||
* @returns The patching store.
|
* @returns The patching store.
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import type { Representation } from '../../../src/ldp/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
||||||
|
import type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences';
|
||||||
import type { RepresentationConverter } from '../../../src/storage/conversion/RepresentationConverter';
|
import type { RepresentationConverter } from '../../../src/storage/conversion/RepresentationConverter';
|
||||||
import { RepresentationConvertingStore } from '../../../src/storage/RepresentationConvertingStore';
|
import { RepresentationConvertingStore } from '../../../src/storage/RepresentationConvertingStore';
|
||||||
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
||||||
@ -7,19 +9,24 @@ import { CONTENT_TYPE } from '../../../src/util/UriConstants';
|
|||||||
describe('A RepresentationConvertingStore', (): void => {
|
describe('A RepresentationConvertingStore', (): void => {
|
||||||
let store: RepresentationConvertingStore;
|
let store: RepresentationConvertingStore;
|
||||||
let source: ResourceStore;
|
let source: ResourceStore;
|
||||||
let handleSafeFn: jest.Mock<Promise<void>, []>;
|
let inConverter: RepresentationConverter;
|
||||||
let converter: RepresentationConverter;
|
let outConverter: RepresentationConverter;
|
||||||
|
const inPreferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]};
|
||||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||||
|
let representation: Representation;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
source = {
|
source = {
|
||||||
getRepresentation: jest.fn(async(): Promise<any> => ({ data: 'data', metadata })),
|
getRepresentation: jest.fn(async(): Promise<any> => ({ data: 'data', metadata })),
|
||||||
} as unknown as ResourceStore;
|
addResource: jest.fn(),
|
||||||
|
setRepresentation: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
handleSafeFn = jest.fn(async(): Promise<any> => 'converter');
|
inConverter = { handleSafe: jest.fn(async(): Promise<any> => 'inConvert') } as any;
|
||||||
converter = { handleSafe: handleSafeFn } as unknown as RepresentationConverter;
|
outConverter = { handleSafe: jest.fn(async(): Promise<any> => 'outConvert') } as any;
|
||||||
|
|
||||||
store = new RepresentationConvertingStore(source, converter);
|
store = new RepresentationConvertingStore(source, { inPreferences, inConverter, outConverter });
|
||||||
|
representation = { binary: true, data: 'data', metadata } as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the Representation from the source if no changes are required.', async(): Promise<void> => {
|
it('returns the Representation from the source if no changes are required.', async(): Promise<void> => {
|
||||||
@ -35,7 +42,7 @@ describe('A RepresentationConvertingStore', (): void => {
|
|||||||
expect(source.getRepresentation).toHaveBeenLastCalledWith(
|
expect(source.getRepresentation).toHaveBeenLastCalledWith(
|
||||||
{ path: 'path' }, { type: [{ value: 'text/*', weight: 0 }, { value: 'text/turtle', weight: 1 }]}, undefined,
|
{ path: 'path' }, { type: [{ value: 'text/*', weight: 0 }, { value: 'text/turtle', weight: 1 }]}, undefined,
|
||||||
);
|
);
|
||||||
expect(handleSafeFn).toHaveBeenCalledTimes(0);
|
expect(outConverter.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the Representation from the source if there are no preferences.', async(): Promise<void> => {
|
it('returns the Representation from the source if there are no preferences.', async(): Promise<void> => {
|
||||||
@ -49,19 +56,43 @@ describe('A RepresentationConvertingStore', (): void => {
|
|||||||
expect(source.getRepresentation).toHaveBeenLastCalledWith(
|
expect(source.getRepresentation).toHaveBeenLastCalledWith(
|
||||||
{ path: 'path' }, {}, undefined,
|
{ path: 'path' }, {}, undefined,
|
||||||
);
|
);
|
||||||
expect(handleSafeFn).toHaveBeenCalledTimes(0);
|
expect(outConverter.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the converter if another output is preferred.', async(): Promise<void> => {
|
it('calls the converter if another output is preferred.', async(): Promise<void> => {
|
||||||
await expect(store.getRepresentation({ path: 'path' }, { type: [
|
await expect(store.getRepresentation({ path: 'path' }, { type: [
|
||||||
{ value: 'text/plain', weight: 1 }, { value: 'text/turtle', weight: 0 },
|
{ value: 'text/plain', weight: 1 }, { value: 'text/turtle', weight: 0 },
|
||||||
]})).resolves.toEqual('converter');
|
]})).resolves.toEqual('outConvert');
|
||||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||||
expect(handleSafeFn).toHaveBeenCalledTimes(1);
|
expect(outConverter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(handleSafeFn).toHaveBeenLastCalledWith({
|
expect(outConverter.handleSafe).toHaveBeenLastCalledWith({
|
||||||
identifier: { path: 'path' },
|
identifier: { path: 'path' },
|
||||||
representation: { data: 'data', metadata },
|
representation: { data: 'data', metadata },
|
||||||
preferences: { type: [{ value: 'text/plain', weight: 1 }, { value: 'text/turtle', weight: 0 }]},
|
preferences: { type: [{ value: 'text/plain', weight: 1 }, { value: 'text/turtle', weight: 0 }]},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the representation if the conversion is not required.', async(): Promise<void> => {
|
||||||
|
const id = { path: 'identifier' };
|
||||||
|
|
||||||
|
await expect(store.addResource(id, representation, 'conditions' as any)).resolves.toBeUndefined();
|
||||||
|
expect(source.addResource).toHaveBeenLastCalledWith(id, representation, 'conditions');
|
||||||
|
|
||||||
|
await expect(store.setRepresentation(id, representation, 'conditions' as any)).resolves.toBeUndefined();
|
||||||
|
expect(inConverter.handleSafe).toHaveBeenCalledTimes(0);
|
||||||
|
expect(source.setRepresentation).toHaveBeenLastCalledWith(id, representation, 'conditions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts the data if it is required.', async(): Promise<void> => {
|
||||||
|
metadata.contentType = 'text/plain';
|
||||||
|
const id = { path: 'identifier' };
|
||||||
|
|
||||||
|
await expect(store.addResource(id, representation, 'conditions' as any)).resolves.toBeUndefined();
|
||||||
|
expect(inConverter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(source.addResource).toHaveBeenLastCalledWith(id, 'inConvert', 'conditions');
|
||||||
|
|
||||||
|
await expect(store.setRepresentation(id, representation, 'conditions' as any)).resolves.toBeUndefined();
|
||||||
|
expect(inConverter.handleSafe).toHaveBeenCalledTimes(2);
|
||||||
|
expect(source.setRepresentation).toHaveBeenLastCalledWith(id, 'inConvert', 'conditions');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user