mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add IndexRepresentationStore to support index resources
This commit is contained in:
parent
4e0c8291ca
commit
cc1e332394
@ -14,14 +14,17 @@
|
||||
"@id": "urn:solid-server:default:ResourceStore",
|
||||
"@type": "MonitoringStore",
|
||||
"MonitoringStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Routing",
|
||||
"comment": "A router rule will be defined when activating dynamic pods."
|
||||
"@id": "urn:solid-server:default:ResourceStore_Index",
|
||||
"IndexRepresentationStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Routing",
|
||||
"comment": "A router rule will be defined when activating dynamic pods."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,15 +13,18 @@
|
||||
"@id": "urn:solid-server:default:ResourceStore",
|
||||
"@type": "MonitoringStore",
|
||||
"MonitoringStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_DataAccessor",
|
||||
"DataAccessorBasedStore:_accessor": {
|
||||
"@id": "urn:solid-server:default:FileDataAccessor"
|
||||
"@id": "urn:solid-server:default:ResourceStore_Index",
|
||||
"IndexRepresentationStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_DataAccessor",
|
||||
"DataAccessorBasedStore:_accessor": {
|
||||
"@id": "urn:solid-server:default:FileDataAccessor"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,15 +13,18 @@
|
||||
"@id": "urn:solid-server:default:ResourceStore",
|
||||
"@type": "MonitoringStore",
|
||||
"MonitoringStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_DataAccessor",
|
||||
"DataAccessorBasedStore:_accessor": {
|
||||
"@id": "urn:solid-server:default:MemoryDataAccessor"
|
||||
"@id": "urn:solid-server:default:ResourceStore_Index",
|
||||
"IndexRepresentationStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_DataAccessor",
|
||||
"DataAccessorBasedStore:_accessor": {
|
||||
"@id": "urn:solid-server:default:MemoryDataAccessor"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,13 +15,16 @@
|
||||
"@id": "urn:solid-server:default:ResourceStore",
|
||||
"@type": "MonitoringStore",
|
||||
"MonitoringStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Routing"
|
||||
"@id": "urn:solid-server:default:ResourceStore_Index",
|
||||
"IndexRepresentationStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Routing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,18 +13,23 @@
|
||||
"@id": "urn:solid-server:default:ResourceStore",
|
||||
"@type": "MonitoringStore",
|
||||
"MonitoringStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"comment": "This makes it so all incoming data is converted to quad objects.",
|
||||
"RepresentationConvertingStore:_options_inConverter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
||||
"RepresentationConvertingStore:_options_inType": "internal/quads",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_DataAccessor",
|
||||
"DataAccessorBasedStore:_accessor": {
|
||||
"@id": "urn:solid-server:default:SparqlDataAccessor"
|
||||
"@id": "urn:solid-server:default:ResourceStore_Index",
|
||||
"IndexRepresentationStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Locking",
|
||||
"LockingResourceStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Patching",
|
||||
"PatchingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_Converting",
|
||||
"comment": "This makes it so all incoming data is converted to quad objects.",
|
||||
"RepresentationConvertingStore:_options_inConverter": {
|
||||
"@id": "urn:solid-server:default:RepresentationConverter"
|
||||
},
|
||||
"RepresentationConvertingStore:_options_inType": "internal/quads",
|
||||
"RepresentationConvertingStore:_source": {
|
||||
"@id": "urn:solid-server:default:ResourceStore_DataAccessor",
|
||||
"DataAccessorBasedStore:_accessor": {
|
||||
"@id": "urn:solid-server:default:SparqlDataAccessor"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "When a container with an index.html document is accessed, serve that HTML document instead of the container.",
|
||||
"@id": "urn:solid-server:default:ResourceStore_Index",
|
||||
"@type": "IndexRepresentationStore"
|
||||
},
|
||||
{
|
||||
"comment": "This value can be used to set a custom handler for index files. See the example file.",
|
||||
"@id": "urn:solid-server:default:DefaultUiConverter",
|
||||
|
@ -1,6 +1,11 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "This an example on how to disable the routing to index.html when accessing a container.",
|
||||
"@id": "urn:solid-server:default:ResourceStore_Index",
|
||||
"@type": "PassthroughStore"
|
||||
},
|
||||
{
|
||||
"comment": [
|
||||
"This converter replaces every browser request for text/html with the Databrowser UI (which then in turn loads a Turtle or other representation of the same resource).",
|
||||
|
@ -265,6 +265,7 @@ export * from './storage/AtomicResourceStore';
|
||||
export * from './storage/BaseResourceStore';
|
||||
export * from './storage/Conditions';
|
||||
export * from './storage/DataAccessorBasedStore';
|
||||
export * from './storage/IndexRepresentationStore';
|
||||
export * from './storage/LockingResourceStore';
|
||||
export * from './storage/MonitoringStore';
|
||||
export * from './storage/PassthroughStore';
|
||||
|
64
src/storage/IndexRepresentationStore.ts
Normal file
64
src/storage/IndexRepresentationStore.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import assert from 'assert';
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||
import { isContainerIdentifier } from '../util/PathUtil';
|
||||
import type { Conditions } from './Conditions';
|
||||
import { cleanPreferences, matchesMediaType } from './conversion/ConversionUtil';
|
||||
import { PassthroughStore } from './PassthroughStore';
|
||||
import type { ResourceStore } from './ResourceStore';
|
||||
|
||||
/**
|
||||
* Allow containers to have a custom representation.
|
||||
* The index representation will be returned when the following conditions are fulfilled:
|
||||
* * The request targets a container.
|
||||
* * A resource with the given `indexName` exists in the container. (default: "index.html")
|
||||
* * The highest weighted preference matches the `mediaRange` (default: "text/html")
|
||||
* Otherwise the request will be passed on to the source store.
|
||||
* In case the index representation should always be returned when it exists,
|
||||
* the `mediaRange` should be set to "\*∕\*".
|
||||
*
|
||||
* Note: this functionality is not yet part of the specification. Relevant issues are:
|
||||
* - https://github.com/solid/specification/issues/69
|
||||
* - https://github.com/solid/specification/issues/198
|
||||
* - https://github.com/solid/specification/issues/109
|
||||
* - https://github.com/solid/web-access-control-spec/issues/36
|
||||
*/
|
||||
export class IndexRepresentationStore extends PassthroughStore {
|
||||
private readonly indexName: string;
|
||||
private readonly mediaRange: string;
|
||||
|
||||
public constructor(source: ResourceStore, indexName = 'index.html', mediaRange = 'text/html') {
|
||||
super(source);
|
||||
assert(/^[\w.-]+$/u.test(indexName), 'Invalid index name');
|
||||
this.indexName = indexName;
|
||||
this.mediaRange = mediaRange;
|
||||
}
|
||||
|
||||
public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
|
||||
conditions?: Conditions): Promise<Representation> {
|
||||
if (isContainerIdentifier(identifier) && this.matchesPreferences(preferences)) {
|
||||
try {
|
||||
const indexIdentifier = { path: `${identifier.path}${this.indexName}` };
|
||||
return await this.source.getRepresentation(indexIdentifier, preferences, conditions);
|
||||
} catch (error: unknown) {
|
||||
if (!NotFoundHttpError.isInstance(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.source.getRepresentation(identifier, preferences, conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure the stored media range matches the highest weight preference.
|
||||
*/
|
||||
private matchesPreferences(preferences: RepresentationPreferences): boolean {
|
||||
const cleaned = cleanPreferences(preferences.type);
|
||||
const max = Math.max(...Object.values(cleaned));
|
||||
return Object.entries(cleaned).some(([ range, weight ]): boolean =>
|
||||
matchesMediaType(range, this.mediaRange) && weight === max);
|
||||
}
|
||||
}
|
75
test/unit/storage/IndexRepresentationstore.test.ts
Normal file
75
test/unit/storage/IndexRepresentationstore.test.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
|
||||
import { IndexRepresentationStore } from '../../../src/storage/IndexRepresentationStore';
|
||||
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
||||
import { ConflictHttpError } from '../../../src/util/errors/ConflictHttpError';
|
||||
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
||||
import { readableToString } from '../../../src/util/StreamUtil';
|
||||
|
||||
describe('An IndexRepresentationStore', (): void => {
|
||||
const baseUrl = 'http://test.com/';
|
||||
const emptyContainer = { path: `${baseUrl}/container/` };
|
||||
let source: jest.Mocked<ResourceStore>;
|
||||
let store: IndexRepresentationStore;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
source = {
|
||||
getRepresentation: jest.fn((identifier: ResourceIdentifier): any => {
|
||||
if (identifier.path === `${baseUrl}index.html`) {
|
||||
return new BasicRepresentation('index data', 'text/html');
|
||||
}
|
||||
if (identifier.path.endsWith('/')) {
|
||||
return new BasicRepresentation('container data', 'text/turtle');
|
||||
}
|
||||
throw new NotFoundHttpError();
|
||||
}),
|
||||
} as any;
|
||||
|
||||
store = new IndexRepresentationStore(source);
|
||||
});
|
||||
|
||||
it('errors on invalid index names.', async(): Promise<void> => {
|
||||
expect((): any => new IndexRepresentationStore(source, '../secretContainer/secret.key'))
|
||||
.toThrow('Invalid index name');
|
||||
});
|
||||
|
||||
it('retrieves the index resource if it exists.', async(): Promise<void> => {
|
||||
const result = await store.getRepresentation({ path: baseUrl }, {});
|
||||
await expect(readableToString(result.data)).resolves.toBe('index data');
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('errors if a non-404 error was thrown when accessing the index resource.', async(): Promise<void> => {
|
||||
source.getRepresentation.mockRejectedValueOnce(new ConflictHttpError('conflict!'));
|
||||
await expect(store.getRepresentation({ path: baseUrl }, {})).rejects.toThrow('conflict!');
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('requests the usual data if there is no index resource.', async(): Promise<void> => {
|
||||
const result = await store.getRepresentation(emptyContainer, {});
|
||||
await expect(readableToString(result.data)).resolves.toBe('container data');
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(2);
|
||||
expect(source.getRepresentation).toHaveBeenCalledWith({ path: `${emptyContainer.path}index.html` }, {}, undefined);
|
||||
expect(source.getRepresentation).toHaveBeenLastCalledWith(emptyContainer, {}, undefined);
|
||||
});
|
||||
|
||||
it('requests the usual data if the index media range is not the most preferred.', async(): Promise<void> => {
|
||||
const preferences = { type: { 'text/turtle': 0.8, 'text/html': 0.5 }};
|
||||
const result = await store.getRepresentation({ path: baseUrl }, preferences);
|
||||
await expect(readableToString(result.data)).resolves.toBe('container data');
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: baseUrl }, preferences, undefined);
|
||||
});
|
||||
|
||||
it('always returns the index resource if the media range is set to */*.', async(): Promise<void> => {
|
||||
store = new IndexRepresentationStore(source, 'base.html', '*/*');
|
||||
// Mocking because we also change the index name
|
||||
source.getRepresentation.mockResolvedValueOnce(new BasicRepresentation('index data', 'text/html'));
|
||||
|
||||
const preferences = { type: { 'text/turtle': 0.8, 'text/html': 0.5 }};
|
||||
const result = await store.getRepresentation({ path: baseUrl }, preferences);
|
||||
await expect(readableToString(result.data)).resolves.toBe('index data');
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: `${baseUrl}base.html` }, preferences, undefined);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user