fix: Allow deletion of root in InMemoryDataAccessor

This commit is contained in:
Joachim Van Herwegen 2020-12-11 17:19:37 +01:00
parent 90bdfb5583
commit 3e3dd7f5a9
2 changed files with 62 additions and 32 deletions

View File

@ -23,14 +23,16 @@ type CacheEntry = DataEntry | ContainerEntry;
export class InMemoryDataAccessor implements DataAccessor { export class InMemoryDataAccessor implements DataAccessor {
private readonly base: string; private readonly base: string;
private readonly store: ContainerEntry; // A dummy container with one entry which corresponds to the base
private readonly store: { entries: { ''?: ContainerEntry } };
public constructor(base: string) { public constructor(base: string) {
this.base = ensureTrailingSlash(base); this.base = ensureTrailingSlash(base);
const metadata = new RepresentationMetadata({ path: this.base }); const metadata = new RepresentationMetadata({ path: this.base });
metadata.addQuads(generateResourceQuads(DataFactory.namedNode(this.base), true)); metadata.addQuads(generateResourceQuads(DataFactory.namedNode(this.base), true));
this.store = { entries: {}, metadata }; const rootContainer = { entries: {}, metadata };
this.store = { entries: { '': rootContainer }};
} }
public async canHandle(): Promise<void> { public async canHandle(): Promise<void> {
@ -96,11 +98,15 @@ export class InMemoryDataAccessor implements DataAccessor {
} }
private getParentEntry(identifier: ResourceIdentifier): { parent: ContainerEntry; name: string } { private getParentEntry(identifier: ResourceIdentifier): { parent: ContainerEntry; name: string } {
const parts = identifier.path.slice(this.base.length).split('/').filter((part): boolean => part.length > 0); if (identifier.path === this.base) {
// Casting is fine here as the parent should never be used as a real container
if (parts.length === 0) { return { parent: this.store as any, name: '' };
throw new Error('Root container has no parent.');
} }
if (!this.store.entries['']) {
throw new NotFoundHttpError();
}
const parts = identifier.path.slice(this.base.length).split('/').filter((part): boolean => part.length > 0);
// Name of the resource will be the last entry in the path // Name of the resource will be the last entry in the path
const name = parts[parts.length - 1]; const name = parts[parts.length - 1];
@ -109,7 +115,8 @@ export class InMemoryDataAccessor implements DataAccessor {
const containers = parts.slice(0, -1); const containers = parts.slice(0, -1);
// Step through the parts of the path up to the end // Step through the parts of the path up to the end
let parent = this.store; // First entry is guaranteed to be a ContainerEntry
let parent = this.store.entries[''];
for (const container of containers) { for (const container of containers) {
const child = parent.entries[container]; const child = parent.entries[container];
if (!child) { if (!child) {
@ -124,9 +131,6 @@ export class InMemoryDataAccessor implements DataAccessor {
} }
private getEntry(identifier: ResourceIdentifier): CacheEntry { private getEntry(identifier: ResourceIdentifier): CacheEntry {
if (identifier.path === this.base) {
return this.store;
}
const { parent, name } = this.getParentEntry(identifier); const { parent, name } = this.getParentEntry(identifier);
const entry = parent.entries[name]; const entry = parent.entries[name];
if (!entry) { if (!entry) {

View File

@ -38,12 +38,12 @@ describe('An InMemoryDataAccessor', (): void => {
}); });
it('throws an error if part of the path matches a document.', async(): Promise<void> => { it('throws an error if part of the path matches a document.', async(): Promise<void> => {
await accessor.writeDocument({ path: `${base}resource` }, data, metadata); await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
await expect(accessor.getData({ path: `${base}resource/resource2` })).rejects.toThrow(new Error('Invalid path.')); await expect(accessor.getData({ path: `${base}resource/resource2` })).rejects.toThrow(new Error('Invalid path.'));
}); });
it('returns the corresponding data every time.', async(): Promise<void> => { it('returns the corresponding data every time.', async(): Promise<void> => {
await accessor.writeDocument({ path: `${base}resource` }, data, metadata); await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
// Run twice to make sure the data is stored correctly // Run twice to make sure the data is stored correctly
await expect(readableToString(await accessor.getData({ path: `${base}resource` }))).resolves.toBe('data'); await expect(readableToString(await accessor.getData({ path: `${base}resource` }))).resolves.toBe('data');
@ -56,29 +56,25 @@ describe('An InMemoryDataAccessor', (): void => {
await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError); await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError);
}); });
it('errors when trying to access the parent of root.', async(): Promise<void> => {
await expect(accessor.writeDocument({ path: `${base}` }, data, metadata))
.rejects.toThrow(new Error('Root container has no parent.'));
});
it('throws a 404 if the trailing slash does not match its type.', async(): Promise<void> => { it('throws a 404 if the trailing slash does not match its type.', async(): Promise<void> => {
await accessor.writeDocument({ path: `${base}resource` }, data, metadata); await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError); await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError);
await accessor.writeContainer({ path: `${base}container/` }, metadata); await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
await expect(accessor.getMetadata({ path: `${base}container` })).rejects.toThrow(NotFoundHttpError); await expect(accessor.getMetadata({ path: `${base}container` })).rejects.toThrow(NotFoundHttpError);
}); });
it('returns empty metadata if there was none stored.', async(): Promise<void> => { it('returns empty metadata if there was none stored.', async(): Promise<void> => {
metadata = new RepresentationMetadata(); metadata = new RepresentationMetadata();
await accessor.writeDocument({ path: `${base}resource` }, data, metadata); await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
metadata = await accessor.getMetadata({ path: `${base}resource` }); metadata = await accessor.getMetadata({ path: `${base}resource` });
expect(metadata.quads()).toHaveLength(0); expect(metadata.quads()).toHaveLength(0);
}); });
it('generates the containment metadata for a container.', async(): Promise<void> => { it('generates the containment metadata for a container.', async(): Promise<void> => {
await accessor.writeContainer({ path: `${base}container/` }, metadata); await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
await accessor.writeDocument({ path: `${base}container/resource` }, data, metadata); await expect(accessor.writeDocument({ path: `${base}container/resource` }, data, metadata))
await accessor.writeContainer({ path: `${base}container/container2` }, metadata); .resolves.toBeUndefined();
await expect(accessor.writeContainer({ path: `${base}container/container2` }, metadata)).resolves.toBeUndefined();
metadata = await accessor.getMetadata({ path: `${base}container/` }); metadata = await accessor.getMetadata({ path: `${base}container/` });
expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray( expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray(
[ toNamedNode(`${base}container/resource`), toNamedNode(`${base}container/container2/`) ], [ toNamedNode(`${base}container/resource`), toNamedNode(`${base}container/container2/`) ],
@ -88,7 +84,7 @@ describe('An InMemoryDataAccessor', (): void => {
it('adds stored metadata when requesting document metadata.', async(): Promise<void> => { it('adds stored metadata when requesting document metadata.', async(): Promise<void> => {
const identifier = { path: `${base}resource` }; const identifier = { path: `${base}resource` };
const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Resource) }); const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Resource) });
await accessor.writeDocument(identifier, data, inputMetadata); await expect(accessor.writeDocument(identifier, data, inputMetadata)).resolves.toBeUndefined();
metadata = await accessor.getMetadata(identifier); metadata = await accessor.getMetadata(identifier);
expect(metadata.identifier.value).toBe(`${base}resource`); expect(metadata.identifier.value).toBe(`${base}resource`);
const quads = metadata.quads(); const quads = metadata.quads();
@ -99,7 +95,7 @@ describe('An InMemoryDataAccessor', (): void => {
it('adds stored metadata when requesting container metadata.', async(): Promise<void> => { it('adds stored metadata when requesting container metadata.', async(): Promise<void> => {
const identifier = { path: `${base}container/` }; const identifier = { path: `${base}container/` };
const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) }); const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) });
await accessor.writeContainer(identifier, inputMetadata); await expect(accessor.writeContainer(identifier, inputMetadata)).resolves.toBeUndefined();
metadata = await accessor.getMetadata(identifier); metadata = await accessor.getMetadata(identifier);
expect(metadata.identifier.value).toBe(`${base}container/`); expect(metadata.identifier.value).toBe(`${base}container/`);
@ -111,15 +107,15 @@ describe('An InMemoryDataAccessor', (): void => {
it('can overwrite the metadata of an existing container without overwriting children.', async(): Promise<void> => { it('can overwrite the metadata of an existing container without overwriting children.', async(): Promise<void> => {
const identifier = { path: `${base}container/` }; const identifier = { path: `${base}container/` };
const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) }); const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) });
await accessor.writeContainer(identifier, inputMetadata); await expect(accessor.writeContainer(identifier, inputMetadata)).resolves.toBeUndefined();
const resourceMetadata = new RepresentationMetadata(); const resourceMetadata = new RepresentationMetadata();
await accessor.writeDocument( await expect(accessor.writeDocument(
{ path: `${base}container/resource` }, data, resourceMetadata, { path: `${base}container/resource` }, data, resourceMetadata,
); )).resolves.toBeUndefined();
const newMetadata = new RepresentationMetadata(inputMetadata); const newMetadata = new RepresentationMetadata(inputMetadata);
newMetadata.add(RDF.type, toNamedNode(LDP.BasicContainer)); newMetadata.add(RDF.type, toNamedNode(LDP.BasicContainer));
await accessor.writeContainer(identifier, newMetadata); await expect(accessor.writeContainer(identifier, newMetadata)).resolves.toBeUndefined();
metadata = await accessor.getMetadata(identifier); metadata = await accessor.getMetadata(identifier);
expect(metadata.identifier.value).toBe(`${base}container/`); expect(metadata.identifier.value).toBe(`${base}container/`);
@ -134,8 +130,29 @@ describe('An InMemoryDataAccessor', (): void => {
expect(await readableToString(await accessor.getData({ path: `${base}container/resource` }))).toBe('data'); expect(await readableToString(await accessor.getData({ path: `${base}container/resource` }))).toBe('data');
}); });
it('can write to the root container without overriding its children.', async(): Promise<void> => {
const identifier = { path: `${base}` };
const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: toNamedNode(LDP.Container) });
await expect(accessor.writeContainer(identifier, inputMetadata)).resolves.toBeUndefined();
const resourceMetadata = new RepresentationMetadata();
await expect(accessor.writeDocument(
{ path: `${base}resource` }, data, resourceMetadata,
)).resolves.toBeUndefined();
metadata = await accessor.getMetadata(identifier);
expect(metadata.identifier.value).toBe(`${base}`);
const quads = metadata.quads();
expect(quads).toHaveLength(2);
expect(metadata.getAll(RDF.type)).toHaveLength(1);
expect(metadata.getAll(LDP.contains)).toHaveLength(1);
await expect(accessor.getMetadata({ path: `${base}resource` }))
.resolves.toBeInstanceOf(RepresentationMetadata);
expect(await readableToString(await accessor.getData({ path: `${base}resource` }))).toBe('data');
});
it('errors when writing to an invalid container path..', async(): Promise<void> => { it('errors when writing to an invalid container path..', async(): Promise<void> => {
await accessor.writeDocument({ path: `${base}resource` }, data, metadata); await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
await expect(accessor.writeContainer({ path: `${base}resource/container` }, metadata)) await expect(accessor.writeContainer({ path: `${base}resource/container` }, metadata))
.rejects.toThrow(new Error('Invalid path.')); .rejects.toThrow(new Error('Invalid path.'));
@ -148,12 +165,21 @@ describe('An InMemoryDataAccessor', (): void => {
}); });
it('removes the corresponding resource.', async(): Promise<void> => { it('removes the corresponding resource.', async(): Promise<void> => {
await accessor.writeDocument({ path: `${base}resource` }, data, metadata); await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
await accessor.writeContainer({ path: `${base}container/` }, metadata); await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined(); await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined();
await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined(); await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined();
await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError); await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError);
await expect(accessor.getMetadata({ path: `${base}container/` })).rejects.toThrow(NotFoundHttpError); await expect(accessor.getMetadata({ path: `${base}container/` })).rejects.toThrow(NotFoundHttpError);
}); });
it('can delete the root container and write to it again.', async(): Promise<void> => {
await expect(accessor.deleteResource({ path: `${base}` })).resolves.toBeUndefined();
await expect(accessor.getMetadata({ path: `${base}` })).rejects.toThrow(NotFoundHttpError);
await expect(accessor.getMetadata({ path: `${base}test/` })).rejects.toThrow(NotFoundHttpError);
await expect(accessor.writeContainer({ path: `${base}` }, metadata)).resolves.toBeUndefined();
const resultMetadata = await accessor.getMetadata({ path: `${base}` });
expect(resultMetadata.quads()).toBeRdfIsomorphic(metadata.quads());
});
}); });
}); });