mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add getChildren function to DataAccessor interface
DataAccessors are now no longer responsible for generating ldp:contains triples.
This commit is contained in:
@@ -19,6 +19,7 @@ import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
||||
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
|
||||
import type { Guarded } from '../../../src/util/GuardedStream';
|
||||
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
|
||||
import { trimTrailingSlashes } from '../../../src/util/PathUtil';
|
||||
import * as quadUtil from '../../../src/util/QuadUtil';
|
||||
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
|
||||
import { CONTENT_TYPE, HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies';
|
||||
@@ -56,6 +57,15 @@ class SimpleDataAccessor implements DataAccessor {
|
||||
return this.data[identifier.path].metadata;
|
||||
}
|
||||
|
||||
public async* getChildren(identifier: ResourceIdentifier): AsyncIterableIterator<RepresentationMetadata> {
|
||||
// Find all keys that look like children of the container
|
||||
const children = Object.keys(this.data).filter((name): boolean =>
|
||||
name.startsWith(identifier.path) &&
|
||||
name.length > identifier.path.length &&
|
||||
!trimTrailingSlashes(name.slice(identifier.path.length)).includes('/'));
|
||||
yield* children.map((name): RepresentationMetadata => new RepresentationMetadata({ path: name }));
|
||||
}
|
||||
|
||||
public async modifyResource(): Promise<void> {
|
||||
throw new Error('modify');
|
||||
}
|
||||
@@ -100,7 +110,7 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
|
||||
public async addMetadata(metadata: RepresentationMetadata): Promise<void> {
|
||||
const identifier = { path: metadata.identifier.value };
|
||||
// Random triple to test on
|
||||
metadata.add(identifier.path, this.getAuxiliaryIdentifier(identifier).path);
|
||||
metadata.add(namedNode('AUXILIARY'), this.getAuxiliaryIdentifier(identifier).path);
|
||||
}
|
||||
|
||||
public async validate(): Promise<void> {
|
||||
@@ -157,7 +167,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
expect(result).toMatchObject({ binary: true });
|
||||
expect(await arrayifyStream(result.data)).toEqual([ resourceData ]);
|
||||
expect(result.metadata.contentType).toEqual('text/plain');
|
||||
expect(result.metadata.get(resourceID.path)?.value).toBe(auxStrategy.getAuxiliaryIdentifier(resourceID).path);
|
||||
expect(result.metadata.get('AUXILIARY')?.value).toBe(auxStrategy.getAuxiliaryIdentifier(resourceID).path);
|
||||
});
|
||||
|
||||
it('will return a data stream that matches the metadata for containers.', async(): Promise<void> => {
|
||||
@@ -170,22 +180,20 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
expect(result).toMatchObject({ binary: false });
|
||||
expect(await arrayifyStream(result.data)).toBeRdfIsomorphic(metaMirror.quads());
|
||||
expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
|
||||
expect(result.metadata.get(resourceID.path)?.value).toBe(auxStrategy.getAuxiliaryIdentifier(resourceID).path);
|
||||
expect(result.metadata.get('AUXILIARY')?.value).toBe(auxStrategy.getAuxiliaryIdentifier(resourceID).path);
|
||||
});
|
||||
|
||||
it('will remove containment triples referencing auxiliary resources.', async(): Promise<void> => {
|
||||
const resourceID = { path: `${root}container/` };
|
||||
containerMetadata.identifier = namedNode(resourceID.path);
|
||||
containerMetadata.add(LDP.terms.contains, [
|
||||
DataFactory.namedNode(`${root}container/.dummy`),
|
||||
DataFactory.namedNode(`${root}container/resource`),
|
||||
DataFactory.namedNode(`${root}container/resource.dummy`),
|
||||
]);
|
||||
accessor.data[resourceID.path] = { metadata: containerMetadata } as Representation;
|
||||
accessor.data[`${resourceID.path}.dummy`] = representation;
|
||||
accessor.data[`${resourceID.path}resource`] = representation;
|
||||
accessor.data[`${resourceID.path}resource.dummy`] = representation;
|
||||
const result = await store.getRepresentation(resourceID);
|
||||
const contains = result.metadata.getAll(LDP.terms.contains);
|
||||
expect(contains).toHaveLength(1);
|
||||
expect(contains[0].value).toEqual(`${root}container/resource`);
|
||||
expect(contains[0].value).toEqual(`${resourceID.path}resource`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -280,7 +288,6 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
const resourceID = { path: root };
|
||||
representation.metadata.add(HTTP.slug, 'newName');
|
||||
accessor.data[`${root}newName`] = representation;
|
||||
accessor.data[root].metadata.add(LDP.contains, DataFactory.namedNode(`${root}newName`));
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result).not.toEqual({
|
||||
path: `${root}newName`,
|
||||
@@ -522,7 +529,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
it('will error when deleting non-empty containers.', async(): Promise<void> => {
|
||||
accessor.data[`${root}container/`] = representation;
|
||||
accessor.data[`${root}container/`].metadata.add(LDP.contains, DataFactory.namedNode(`${root}otherThing`));
|
||||
accessor.data[`${root}container/otherThing`] = representation;
|
||||
const result = store.deleteResource({ path: `${root}container/` });
|
||||
await expect(result).rejects.toThrow(ConflictHttpError);
|
||||
await expect(result).rejects.toThrow('Can only delete empty containers.');
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('A FileDataAccessor', (): void => {
|
||||
expect(metadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime));
|
||||
});
|
||||
|
||||
it('generates the metadata for a container and its non-meta children.', async(): Promise<void> => {
|
||||
it('generates the metadata for a container.', async(): Promise<void> => {
|
||||
cache.data = { container: { resource: 'data', 'resource.meta': 'metadata', notAFile: 5, container2: {}}};
|
||||
metadata = await accessor.getMetadata({ path: `${base}container/` });
|
||||
expect(metadata.identifier.value).toBe(`${base}container/`);
|
||||
@@ -121,18 +121,22 @@ describe('A FileDataAccessor', (): void => {
|
||||
expect(metadata.get(POSIX.size)).toBeUndefined();
|
||||
expect(metadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime));
|
||||
expect(metadata.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000), XSD.terms.integer));
|
||||
expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray(
|
||||
[ namedNode(`${base}container/resource`), namedNode(`${base}container/container2/`) ],
|
||||
);
|
||||
});
|
||||
|
||||
const childQuads = metadata.quads().filter((quad): boolean =>
|
||||
quad.subject.value === `${base}container/resource`);
|
||||
const childMetadata = new RepresentationMetadata({ path: `${base}container/resource` }).addQuads(childQuads);
|
||||
expect(childMetadata.get(RDF.type)?.value).toBe(LDP.Resource);
|
||||
expect(childMetadata.get(POSIX.size)).toEqualRdfTerm(toLiteral('data'.length, XSD.terms.integer));
|
||||
expect(childMetadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime));
|
||||
expect(childMetadata.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000),
|
||||
XSD.terms.integer));
|
||||
it('generates metadata for container child resources.', async(): Promise<void> => {
|
||||
cache.data = { container: { resource: 'data', 'resource.meta': 'metadata', notAFile: 5, container2: {}}};
|
||||
const children = [];
|
||||
for await (const child of accessor.getChildren({ path: `${base}container/` })) {
|
||||
children.push(child);
|
||||
}
|
||||
expect(children).toHaveLength(2);
|
||||
for (const child of children) {
|
||||
expect([ `${base}container/resource`, `${base}container/container2/` ]).toContain(child.identifier.value);
|
||||
expect(child.getAll(RDF.type)!.some((type): boolean => type.equals(LDP.terms.Resource))).toBe(true);
|
||||
expect(child.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime));
|
||||
expect(child.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000),
|
||||
XSD.terms.integer));
|
||||
}
|
||||
});
|
||||
|
||||
it('adds stored metadata when requesting metadata.', async(): Promise<void> => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'jest-rdf';
|
||||
import type { Readable } from 'stream';
|
||||
import { DataFactory } from 'n3';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||
import { InMemoryDataAccessor } from '../../../../src/storage/accessors/InMemoryDataAccessor';
|
||||
@@ -85,16 +84,20 @@ describe('An InMemoryDataAccessor', (): void => {
|
||||
expect(metadata.quads()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('generates the containment metadata for a container.', async(): Promise<void> => {
|
||||
it('generates the children for a container.', async(): Promise<void> => {
|
||||
await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
|
||||
await expect(accessor.writeDocument({ path: `${base}container/resource` }, data, metadata))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(accessor.writeContainer({ path: `${base}container/container2/` }, metadata))
|
||||
.resolves.toBeUndefined();
|
||||
metadata = await accessor.getMetadata({ path: `${base}container/` });
|
||||
expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray(
|
||||
[ DataFactory.namedNode(`${base}container/resource`), DataFactory.namedNode(`${base}container/container2/`) ],
|
||||
);
|
||||
|
||||
const children = [];
|
||||
for await (const child of accessor.getChildren({ path: `${base}container/` })) {
|
||||
children.push(child);
|
||||
}
|
||||
expect(children).toHaveLength(2);
|
||||
expect(children[0].identifier.value).toEqual(`${base}container/resource`);
|
||||
expect(children[1].identifier.value).toEqual(`${base}container/container2/`);
|
||||
});
|
||||
|
||||
it('adds stored metadata when requesting document metadata.', async(): Promise<void> => {
|
||||
@@ -136,10 +139,16 @@ describe('An InMemoryDataAccessor', (): void => {
|
||||
metadata = await accessor.getMetadata(identifier);
|
||||
expect(metadata.identifier.value).toBe(`${base}container/`);
|
||||
const quads = metadata.quads();
|
||||
expect(quads).toHaveLength(3);
|
||||
expect(quads).toHaveLength(2);
|
||||
expect(metadata.getAll(RDF.type).map((term): string => term.value))
|
||||
.toEqual([ LDP.Container, LDP.BasicContainer ]);
|
||||
expect(metadata.get(LDP.contains)?.value).toEqual(`${base}container/resource`);
|
||||
|
||||
const children = [];
|
||||
for await (const child of accessor.getChildren({ path: `${base}container/` })) {
|
||||
children.push(child);
|
||||
}
|
||||
expect(children).toHaveLength(1);
|
||||
expect(children[0].identifier.value).toEqual(`${base}container/resource`);
|
||||
|
||||
await expect(accessor.getMetadata({ path: `${base}container/resource` }))
|
||||
.resolves.toBeInstanceOf(RepresentationMetadata);
|
||||
@@ -147,7 +156,7 @@ describe('An InMemoryDataAccessor', (): void => {
|
||||
});
|
||||
|
||||
it('can write to the root container without overriding its children.', async(): Promise<void> => {
|
||||
const identifier = { path: `${base}` };
|
||||
const identifier = { path: base };
|
||||
const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: LDP.terms.Container });
|
||||
await expect(accessor.writeContainer(identifier, inputMetadata)).resolves.toBeUndefined();
|
||||
const resourceMetadata = new RepresentationMetadata();
|
||||
@@ -158,21 +167,39 @@ describe('An InMemoryDataAccessor', (): void => {
|
||||
metadata = await accessor.getMetadata(identifier);
|
||||
expect(metadata.identifier.value).toBe(`${base}`);
|
||||
const quads = metadata.quads();
|
||||
expect(quads).toHaveLength(2);
|
||||
expect(quads).toHaveLength(1);
|
||||
expect(metadata.getAll(RDF.type)).toHaveLength(1);
|
||||
expect(metadata.getAll(LDP.contains)).toHaveLength(1);
|
||||
|
||||
const children = [];
|
||||
for await (const child of accessor.getChildren(identifier)) {
|
||||
children.push(child);
|
||||
}
|
||||
expect(children).toHaveLength(1);
|
||||
expect(children[0].identifier.value).toEqual(`${base}resource`);
|
||||
|
||||
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 expect(accessor.writeDocument({ path: `${base}resource/` }, data, metadata)).resolves.toBeUndefined();
|
||||
|
||||
await expect(accessor.writeContainer({ path: `${base}resource/container` }, metadata))
|
||||
.rejects.toThrow('Invalid path.');
|
||||
});
|
||||
|
||||
it('returns no children for documents.', async(): Promise<void> => {
|
||||
const identifier = { path: `${base}resource` };
|
||||
const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: LDP.terms.Resource });
|
||||
await expect(accessor.writeDocument(identifier, data, inputMetadata)).resolves.toBeUndefined();
|
||||
|
||||
const children = [];
|
||||
for await (const child of accessor.getChildren(identifier)) {
|
||||
children.push(child);
|
||||
}
|
||||
expect(children).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleting a resource', (): void => {
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('A SparqlDataAccessor', (): void => {
|
||||
));
|
||||
});
|
||||
|
||||
it('requests container data for generating its metadata.', async(): Promise<void> => {
|
||||
it('does not set the content-type for container metadata.', async(): Promise<void> => {
|
||||
metadata = await accessor.getMetadata({ path: 'http://container/' });
|
||||
expect(metadata.quads()).toBeRdfIsomorphic([
|
||||
quad(namedNode('this'), namedNode('a'), namedNode('triple')),
|
||||
@@ -111,13 +111,25 @@ describe('A SparqlDataAccessor', (): void => {
|
||||
|
||||
expect(fetchTriples).toHaveBeenCalledTimes(1);
|
||||
expect(fetchTriples.mock.calls[0][0]).toBe(endpoint);
|
||||
expect(simplifyQuery(fetchTriples.mock.calls[0][1])).toBe(simplifyQuery([
|
||||
'CONSTRUCT { ?s ?p ?o. } WHERE {',
|
||||
' { GRAPH <http://container/> { ?s ?p ?o. } }',
|
||||
' UNION',
|
||||
' { GRAPH <meta:http://container/> { ?s ?p ?o. } }',
|
||||
'}',
|
||||
]));
|
||||
expect(simplifyQuery(fetchTriples.mock.calls[0][1])).toBe(simplifyQuery(
|
||||
'CONSTRUCT { ?s ?p ?o. } WHERE { GRAPH <meta:http://container/> { ?s ?p ?o. } }',
|
||||
));
|
||||
});
|
||||
|
||||
it('requests the container data to find its children.', async(): Promise<void> => {
|
||||
triples = [ quad(namedNode('http://container/'), LDP.terms.contains, namedNode('http://container/child')) ];
|
||||
const children = [];
|
||||
for await (const child of accessor.getChildren({ path: 'http://container/' })) {
|
||||
children.push(child);
|
||||
}
|
||||
expect(children).toHaveLength(1);
|
||||
expect(children[0].identifier.value).toBe('http://container/child');
|
||||
|
||||
expect(fetchTriples).toHaveBeenCalledTimes(1);
|
||||
expect(fetchTriples.mock.calls[0][0]).toBe(endpoint);
|
||||
expect(simplifyQuery(fetchTriples.mock.calls[0][1])).toBe(simplifyQuery(
|
||||
'CONSTRUCT { ?s ?p ?o. } WHERE { GRAPH <http://container/> { ?s ?p ?o. } }',
|
||||
));
|
||||
});
|
||||
|
||||
it('throws 404 if no metadata was found.', async(): Promise<void> => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Stats } from 'fs';
|
||||
import type { Dirent, Stats } from 'fs';
|
||||
|
||||
import { PassThrough } from 'stream';
|
||||
import streamifyArray from 'streamify-array';
|
||||
import type { SystemError } from '../../src/util/errors/SystemError';
|
||||
@@ -149,6 +150,19 @@ export function mockFs(rootFilepath?: string, time?: Date): { data: any } {
|
||||
}
|
||||
return Object.keys(folder[name]);
|
||||
},
|
||||
async* opendir(path: string): AsyncIterableIterator<Dirent> {
|
||||
const { folder, name } = getFolder(path);
|
||||
if (!folder[name]) {
|
||||
throwSystemError('ENOENT');
|
||||
}
|
||||
for (const child of Object.keys(folder[name])) {
|
||||
yield {
|
||||
name: child,
|
||||
isFile: (): boolean => typeof folder[name][child] === 'string',
|
||||
isDirectory: (): boolean => typeof folder[name][child] === 'object',
|
||||
} as Dirent;
|
||||
}
|
||||
},
|
||||
mkdir(path: string): void {
|
||||
const { folder, name } = getFolder(path);
|
||||
if (folder[name]) {
|
||||
|
||||
Reference in New Issue
Block a user