Skip to content

Commit

Permalink
feat: Create store that converts incoming data when required
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Oct 21, 2020
1 parent af6fc42 commit c86eae9
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 4 deletions.
1 change: 1 addition & 0 deletions index.ts
Expand Up @@ -103,6 +103,7 @@ export * from './src/storage/Conditions';
export * from './src/storage/ContainerManager';
export * from './src/storage/DataAccessorBasedStore';
export * from './src/storage/ExtensionBasedMapper';
export * from './src/storage/FixedConvertingStore';
export * from './src/storage/Lock';
export * from './src/storage/LockingResourceStore';
export * from './src/storage/PassthroughStore';
Expand Down
45 changes: 45 additions & 0 deletions src/storage/FixedConvertingStore.ts
@@ -0,0 +1,45 @@
import type { Representation } from '../ldp/representation/Representation';
import type { RepresentationPreference } from '../ldp/representation/RepresentationPreference';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import type { Conditions } from './Conditions';
import type { RepresentationConverter } from './conversion/RepresentationConverter';
import { PassthroughStore } from './PassthroughStore';
import type { ResourceStore } from './ResourceStore';

/**
* Store that converts incoming data when required.
* If the content-type of an incoming representation does not match one of the stored types it will be converted.
*/
export class FixedConvertingStore extends PassthroughStore {
private readonly types: string[];
private readonly converter: RepresentationConverter;

public constructor(source: ResourceStore, converter: RepresentationConverter, types: string[]) {
super(source);
this.converter = converter;
this.types = types;
}

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 async convertRepresentation(identifier: ResourceIdentifier, representation: Representation):
Promise<Representation> {
if (this.types.includes(representation.metadata.contentType!)) {
return representation;
}
const preferences = this.types.map((type): RepresentationPreference => ({ value: type, weight: 1 }));
return this.converter.handleSafe({ identifier, representation, preferences: { type: preferences }});
}
}
11 changes: 9 additions & 2 deletions src/storage/accessors/SparqlDataAccessor.ts
Expand Up @@ -20,6 +20,7 @@ import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdenti
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
import type { MetadataController } from '../../util/MetadataController';
import { CONTENT_TYPE, LDP } from '../../util/UriConstants';
Expand All @@ -28,7 +29,7 @@ import { ensureTrailingSlash } from '../../util/Util';
import type { ContainerManager } from '../ContainerManager';
import type { DataAccessor } from './DataAccessor';

const { quad, namedNode, variable } = DataFactory;
const { defaultGraph, namedNode, quad, variable } = DataFactory;

/**
* Stores all data and metadata of resources in a SPARQL backend.
Expand Down Expand Up @@ -124,10 +125,16 @@ export class SparqlDataAccessor implements DataAccessor {
}
const { name, parent } = await this.getRelevantNames(identifier);

const triples = await arrayifyStream(data) as Quad[];
const def = defaultGraph();
if (triples.some((triple): boolean => !triple.graph.equals(def))) {
throw new UnsupportedHttpError('Only triples in the default graph are supported.');
}

// Not relevant since all content is triples
metadata.removeAll(CONTENT_TYPE);

return this.sendSparqlUpdate(this.sparqlInsert(name, parent, metadata, await arrayifyStream(data)));
return this.sendSparqlUpdate(this.sparqlInsert(name, parent, metadata, triples));
}

/**
Expand Down
51 changes: 51 additions & 0 deletions test/unit/storage/FixedConvertingStore.test.ts
@@ -0,0 +1,51 @@
import type { Representation } from '../../../src/ldp/representation/Representation';
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
import type { RepresentationConverter } from '../../../src/storage/conversion/RepresentationConverter';
import { FixedConvertingStore } from '../../../src/storage/FixedConvertingStore';
import type { ResourceStore } from '../../../src/storage/ResourceStore';
import { StaticAsyncHandler } from '../../util/StaticAsyncHandler';

describe('A FixedConvertingStore', (): void => {
let store: FixedConvertingStore;
let source: ResourceStore;
let converter: RepresentationConverter;
const types = [ 'text/turtle' ];
let metadata: RepresentationMetadata;
let representation: Representation;

beforeEach(async(): Promise<void> => {
source = {
addResource: jest.fn(),
setRepresentation: jest.fn(),
} as any;

converter = new StaticAsyncHandler(true, 'converted') as any;

store = new FixedConvertingStore(source, converter, types);

metadata = new RepresentationMetadata();
representation = { binary: true, data: 'data', metadata } as any;
});

it('keeps the representation if the content-type is supported.', async(): Promise<void> => {
metadata.contentType = types[0];
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(source.setRepresentation).toHaveBeenLastCalledWith(id, representation, 'conditions');
});

it('converts the data if the content-type is not supported.', async(): Promise<void> => {
metadata.contentType = 'text/plain';
const id = { path: 'identifier' };

await expect(store.addResource(id, representation, 'conditions' as any)).resolves.toBeUndefined();
expect(source.addResource).toHaveBeenLastCalledWith(id, 'converted', 'conditions');

await expect(store.setRepresentation(id, representation, 'conditions' as any)).resolves.toBeUndefined();
expect(source.setRepresentation).toHaveBeenLastCalledWith(id, 'converted', 'conditions');
});
});
11 changes: 9 additions & 2 deletions test/unit/storage/accessors/SparqlDataAccessor.test.ts
Expand Up @@ -10,6 +10,7 @@ import { UrlContainerManager } from '../../../../src/storage/UrlContainerManager
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError';
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { MetadataController } from '../../../../src/util/MetadataController';
import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants';
Expand Down Expand Up @@ -193,9 +194,15 @@ describe('A SparqlDataAccessor', (): 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')) ]);
metadata = new RepresentationMetadata('http://test.com/container/resource',
{ [RDF.type]: [ toNamedNode(LDP.Resource) ]});
await expect(accessor.writeDocument({ path: 'http://test.com/container/resource.meta' }, data, metadata))
.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> => {
const data = streamifyArray(
[ quad(namedNode('http://name'), namedNode('http://pred'), literal('value'), namedNode('badGraph!')) ],
);
await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata))
.rejects.toThrow(new UnsupportedHttpError('Only triples in the default graph are supported.'));
});
});

0 comments on commit c86eae9

Please sign in to comment.