mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Allow deletion of root in InMemoryDataAccessor
This commit is contained in:
parent
90bdfb5583
commit
3e3dd7f5a9
@ -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) {
|
||||||
|
@ -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());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user