feat: Add getChildren function to DataAccessor interface

DataAccessors are now no longer responsible for generating ldp:contains triples.
This commit is contained in:
Joachim Van Herwegen 2021-05-11 14:40:42 +02:00
parent 52a3b84ee0
commit cae9d54fac
12 changed files with 187 additions and 137 deletions

View File

@ -63,7 +63,7 @@ export class RepresentationMetadata {
public constructor(contentType?: string); public constructor(contentType?: string);
/** /**
* @param overrides - Metadata values (defaulting to content type if a string) * @param metadata - Metadata values (defaulting to content type if a string)
*/ */
public constructor(metadata?: RepresentationMetadata | MetadataRecord | string); public constructor(metadata?: RepresentationMetadata | MetadataRecord | string);

View File

@ -94,9 +94,13 @@ export class DataAccessorBasedStore implements ResourceStore {
await this.auxiliaryStrategy.addMetadata(metadata); await this.auxiliaryStrategy.addMetadata(metadata);
if (isContainerPath(metadata.identifier.value)) { if (isContainerPath(metadata.identifier.value)) {
// Remove containment references of auxiliary resources // Add containment triples of non-auxiliary resources
const auxContains = this.getContainedAuxiliaryResources(metadata); for await (const child of this.accessor.getChildren(identifier)) {
metadata.remove(LDP.terms.contains, auxContains); if (!this.auxiliaryStrategy.isAuxiliaryIdentifier({ path: child.identifier.value })) {
metadata.addQuads(child.quads());
metadata.add(LDP.terms.contains, child.identifier as NamedNode);
}
}
// Generate a container representation from the metadata // Generate a container representation from the metadata
const data = metadata.quads(); const data = metadata.quads();
@ -139,7 +143,7 @@ export class DataAccessorBasedStore implements ResourceStore {
// using the HTTP Slug header as defined in [RFC5023]. // using the HTTP Slug header as defined in [RFC5023].
// Clients who want the server to assign a URI of a resource, MUST use the POST request." // Clients who want the server to assign a URI of a resource, MUST use the POST request."
// https://solid.github.io/specification/protocol#resource-type-heuristics // https://solid.github.io/specification/protocol#resource-type-heuristics
const newID = this.createSafeUri(container, representation.metadata, parentMetadata); const newID = await this.createSafeUri(container, representation.metadata);
// Write the data. New containers should never be made for a POST request. // Write the data. New containers should never be made for a POST request.
await this.writeData(newID, representation, isContainerIdentifier(newID), false); await this.writeData(newID, representation, isContainerIdentifier(newID), false);
@ -205,8 +209,7 @@ export class DataAccessorBasedStore implements ResourceStore {
// https://solid.github.io/specification/protocol#deleting-resources // https://solid.github.io/specification/protocol#deleting-resources
if (isContainerIdentifier(identifier)) { if (isContainerIdentifier(identifier)) {
// Auxiliary resources are not counted when deleting a container since they will also be deleted // Auxiliary resources are not counted when deleting a container since they will also be deleted
const auxContains = this.getContainedAuxiliaryResources(metadata); if (await this.hasProperChildren(identifier)) {
if (metadata.getAll(LDP.contains).length > auxContains.length) {
throw new ConflictHttpError('Can only delete empty containers.'); throw new ConflictHttpError('Can only delete empty containers.');
} }
} }
@ -399,10 +402,9 @@ export class DataAccessorBasedStore implements ResourceStore {
* *
* @param container - Identifier of the target container. * @param container - Identifier of the target container.
* @param metadata - Metadata of the new resource. * @param metadata - Metadata of the new resource.
* @param parentMetadata - Metadata of the parent container.
*/ */
protected createSafeUri(container: ResourceIdentifier, metadata: RepresentationMetadata, protected async createSafeUri(container: ResourceIdentifier, metadata: RepresentationMetadata):
parentMetadata: RepresentationMetadata): ResourceIdentifier { Promise<ResourceIdentifier> {
// Get all values needed for naming the resource // Get all values needed for naming the resource
const isContainer = this.isNewContainer(metadata); const isContainer = this.isNewContainer(metadata);
const slug = metadata.get(HTTP.slug)?.value; const slug = metadata.get(HTTP.slug)?.value;
@ -418,11 +420,9 @@ export class DataAccessorBasedStore implements ResourceStore {
} }
// Make sure we don't already have a resource with this exact name (or with differing trailing slash) // Make sure we don't already have a resource with this exact name (or with differing trailing slash)
const withSlash = ensureTrailingSlash(newID.path); const withSlash = { path: ensureTrailingSlash(newID.path) };
const withoutSlash = trimTrailingSlashes(newID.path); const withoutSlash = { path: trimTrailingSlashes(newID.path) };
const exists = parentMetadata.getAll(LDP.contains).some((term): boolean => if (await this.resourceExists(withSlash) || await this.resourceExists(withoutSlash)) {
term.value === withSlash || term.value === withoutSlash);
if (exists) {
newID = this.createURI(container, isContainer); newID = this.createURI(container, isContainer);
} }
@ -458,11 +458,15 @@ export class DataAccessorBasedStore implements ResourceStore {
} }
/** /**
* Extracts the identifiers of all auxiliary resources contained within the given metadata. * Checks if the given container has any non-auxiliary resources.
*/ */
protected getContainedAuxiliaryResources(metadata: RepresentationMetadata): NamedNode[] { protected async hasProperChildren(container: ResourceIdentifier): Promise<boolean> {
return metadata.getAll(LDP.terms.contains).filter((object): boolean => for await (const child of this.accessor.getChildren(container)) {
this.auxiliaryStrategy.isAuxiliaryIdentifier({ path: object.value })) as NamedNode[]; if (!this.auxiliaryStrategy.isAuxiliaryIdentifier({ path: child.identifier.value })) {
return true;
}
}
return false;
} }
/** /**

View File

@ -11,7 +11,7 @@ import type { Guarded } from '../../util/GuardedStream';
* * If the input identifier ends with a slash, it should be assumed the identifier is targeting a container. * * If the input identifier ends with a slash, it should be assumed the identifier is targeting a container.
* * Similarly, if there is no trailing slash it should assume a document. * * Similarly, if there is no trailing slash it should assume a document.
* * It should always throw a NotFoundHttpError if it does not have data matching the input identifier. * * It should always throw a NotFoundHttpError if it does not have data matching the input identifier.
* * DataAccessors are responsible for generating the relevant containment triples for containers. * * DataAccessors should not generate containment triples. This will be done externally using `getChildren`.
*/ */
export interface DataAccessor { export interface DataAccessor {
/** /**
@ -36,6 +36,20 @@ export interface DataAccessor {
*/ */
getMetadata: (identifier: ResourceIdentifier) => Promise<RepresentationMetadata>; getMetadata: (identifier: ResourceIdentifier) => Promise<RepresentationMetadata>;
/**
* Returns metadata for all resources in the requested container.
* This should not be all metadata of those resources (but it can be),
* but instead the main metadata you want to show in situations
* where all these resources are presented simultaneously.
* Generally this would be metadata that is present for all of these resources,
* such as resource type or last modified date.
*
* It can be safely assumed that the incoming identifier will always correspond to a container.
*
* @param identifier - Identifier of the parent container.
*/
getChildren: (identifier: ResourceIdentifier) => AsyncIterableIterator<RepresentationMetadata>;
/** /**
* Writes data and metadata for a document. * Writes data and metadata for a document.
* If any data and/or metadata exist for the given identifier, it should be overwritten. * If any data and/or metadata exist for the given identifier, it should be overwritten.

View File

@ -14,7 +14,7 @@ import { guardStream } from '../../util/GuardedStream';
import type { Guarded } from '../../util/GuardedStream'; import type { Guarded } from '../../util/GuardedStream';
import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil'; import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil';
import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil'; import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil'; import { generateResourceQuads } from '../../util/ResourceUtil';
import { toLiteral } from '../../util/TermUtil'; import { toLiteral } from '../../util/TermUtil';
import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../util/Vocabularies'; import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../util/Vocabularies';
import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper'; import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper';
@ -70,6 +70,11 @@ export class FileDataAccessor implements DataAccessor {
throw new NotFoundHttpError(); throw new NotFoundHttpError();
} }
public async* getChildren(identifier: ResourceIdentifier): AsyncIterableIterator<RepresentationMetadata> {
const link = await this.resourceMapper.mapUrlToFilePath(identifier);
yield* this.getChildMetadata(link);
}
/** /**
* Writes the given data as a file (and potential metadata as additional file). * Writes the given data as a file (and potential metadata as additional file).
* The metadata file will be written first and will be deleted if something goes wrong writing the actual data. * The metadata file will be written first and will be deleted if something goes wrong writing the actual data.
@ -194,8 +199,7 @@ export class FileDataAccessor implements DataAccessor {
*/ */
private async getDirectoryMetadata(link: ResourceLink, stats: Stats): private async getDirectoryMetadata(link: ResourceLink, stats: Stats):
Promise<RepresentationMetadata> { Promise<RepresentationMetadata> {
return (await this.getBaseMetadata(link, stats, true)) return await this.getBaseMetadata(link, stats, true);
.addQuads(await this.getChildMetadataQuads(link));
} }
/** /**
@ -277,46 +281,38 @@ export class FileDataAccessor implements DataAccessor {
} }
/** /**
* Generate all containment related triples for a container. * Generate metadata for all children in a container.
* These include the actual containment triples and specific triples for every child resource.
* *
* @param link - Path related metadata. * @param link - Path related metadata.
*/ */
private async getChildMetadataQuads(link: ResourceLink): Promise<Quad[]> { private async* getChildMetadata(link: ResourceLink): AsyncIterableIterator<RepresentationMetadata> {
const quads: Quad[] = []; const dir = await fsPromises.opendir(link.filePath);
const childURIs: string[] = [];
const files = await fsPromises.readdir(link.filePath);
// For every child in the container we want to generate specific metadata // For every child in the container we want to generate specific metadata
for (const childName of files) { for await (const entry of dir) {
// Hide metadata files from containment triples const childName = entry.name;
// Hide metadata files
if (this.isMetadataPath(childName)) { if (this.isMetadataPath(childName)) {
continue; continue;
} }
// Ignore non-file/directory entries in the folder // Ignore non-file/directory entries in the folder
const childStats = await fsPromises.lstat(joinFilePath(link.filePath, childName)); if (!entry.isFile() && !entry.isDirectory()) {
if (!childStats.isFile() && !childStats.isDirectory()) {
continue; continue;
} }
// Generate the URI corresponding to the child resource // Generate the URI corresponding to the child resource
const childLink = await this.resourceMapper const childLink = await this.resourceMapper
.mapFilePathToUrl(joinFilePath(link.filePath, childName), childStats.isDirectory()); .mapFilePathToUrl(joinFilePath(link.filePath, childName), entry.isDirectory());
// Generate metadata of this specific child // Generate metadata of this specific child
const subject = DataFactory.namedNode(childLink.identifier.path); const subject = DataFactory.namedNode(childLink.identifier.path);
const childStats = await fsPromises.lstat(joinFilePath(link.filePath, childName));
const quads: Quad[] = [];
quads.push(...generateResourceQuads(subject, childStats.isDirectory())); quads.push(...generateResourceQuads(subject, childStats.isDirectory()));
quads.push(...this.generatePosixQuads(subject, childStats)); quads.push(...this.generatePosixQuads(subject, childStats));
childURIs.push(childLink.identifier.path); yield new RepresentationMetadata(subject).addQuads(quads);
} }
// Generate containment metadata
const containsQuads = generateContainmentQuads(
DataFactory.namedNode(link.identifier.path), childURIs,
);
return quads.concat(containsQuads);
} }
/** /**

View File

@ -1,13 +1,11 @@
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import type { NamedNode } from 'rdf-js';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { InternalServerError } from '../../util/errors/InternalServerError'; import { InternalServerError } from '../../util/errors/InternalServerError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import type { Guarded } from '../../util/GuardedStream'; import type { Guarded } from '../../util/GuardedStream';
import type { IdentifierStrategy } from '../../util/identifiers/IdentifierStrategy'; import type { IdentifierStrategy } from '../../util/identifiers/IdentifierStrategy';
import { generateContainmentQuads } from '../../util/ResourceUtil';
import { guardedStreamFrom } from '../../util/StreamUtil'; import { guardedStreamFrom } from '../../util/StreamUtil';
import type { DataAccessor } from './DataAccessor'; import type { DataAccessor } from './DataAccessor';
@ -46,7 +44,15 @@ export class InMemoryDataAccessor implements DataAccessor {
public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> { public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
const entry = this.getEntry(identifier); const entry = this.getEntry(identifier);
return this.generateMetadata(entry); return new RepresentationMetadata(entry.metadata);
}
public async* getChildren(identifier: ResourceIdentifier): AsyncIterableIterator<RepresentationMetadata> {
const entry = this.getEntry(identifier);
if (!this.isDataEntry(entry)) {
const childNames = Object.keys(entry.entries);
yield* childNames.map((name): RepresentationMetadata => new RepresentationMetadata({ path: name }));
}
} }
public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata): public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata):
@ -141,14 +147,4 @@ export class InMemoryDataAccessor implements DataAccessor {
} }
return entry; return entry;
} }
private generateMetadata(entry: CacheEntry): RepresentationMetadata {
const metadata = new RepresentationMetadata(entry.metadata);
if (!this.isDataEntry(entry)) {
const childNames = Object.keys(entry.entries);
const quads = generateContainmentQuads(metadata.identifier as NamedNode, childNames);
metadata.addQuads(quads);
}
return metadata;
}
} }

View File

@ -4,16 +4,15 @@ import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint';
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import type { NamedNode, Quad } from 'rdf-js'; import type { NamedNode, Quad } from 'rdf-js';
import type { import type {
ConstructQuery, GraphPattern, ConstructQuery,
GraphPattern,
GraphQuads, GraphQuads,
InsertDeleteOperation, InsertDeleteOperation,
SparqlGenerator, SparqlGenerator,
Update, Update,
UpdateOperation, UpdateOperation,
} from 'sparqljs'; } from 'sparqljs';
import { import { Generator } from 'sparqljs';
Generator,
} from 'sparqljs';
import type { Representation } from '../../ldp/representation/Representation'; import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
@ -83,9 +82,7 @@ export class SparqlDataAccessor implements DataAccessor {
*/ */
public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> { public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
const name = namedNode(identifier.path); const name = namedNode(identifier.path);
const query = isContainerIdentifier(identifier) ? const query = this.sparqlConstruct(this.getMetadataNode(name));
this.sparqlConstructContainer(name) :
this.sparqlConstruct(this.getMetadataNode(name));
const stream = await this.sendSparqlConstruct(query); const stream = await this.sendSparqlConstruct(query);
const quads = await arrayifyStream(stream); const quads = await arrayifyStream(stream);
@ -101,6 +98,15 @@ export class SparqlDataAccessor implements DataAccessor {
return metadata; return metadata;
} }
public async* getChildren(identifier: ResourceIdentifier): AsyncIterableIterator<RepresentationMetadata> {
// Only triples that have a container identifier as subject are the containment triples
const name = namedNode(identifier.path);
const stream = await this.sendSparqlConstruct(this.sparqlConstruct(name));
for await (const entry of stream) {
yield new RepresentationMetadata((entry as Quad).object as NamedNode);
}
}
/** /**
* Writes the given metadata for the container. * Writes the given metadata for the container.
*/ */
@ -186,23 +192,6 @@ export class SparqlDataAccessor implements DataAccessor {
}; };
} }
private sparqlConstructContainer(name: NamedNode): ConstructQuery {
const pattern = quad(variable('s'), variable('p'), variable('o'));
return {
queryType: 'CONSTRUCT',
template: [ pattern ],
where: [{
type: 'union',
patterns: [
this.sparqlSelectGraph(name, [ pattern ]),
this.sparqlSelectGraph(this.getMetadataNode(name), [ pattern ]),
],
}],
type: 'query',
prefixes: {},
};
}
private sparqlSelectGraph(name: NamedNode, triples: Quad[]): GraphPattern { private sparqlSelectGraph(name: NamedNode, triples: Quad[]): GraphPattern {
return { return {
type: 'graph', type: 'graph',
@ -215,8 +204,8 @@ export class SparqlDataAccessor implements DataAccessor {
* Creates an update query that overwrites the data and metadata of a resource. * Creates an update query that overwrites the data and metadata of a resource.
* If there are no triples we assume it's a container (so don't overwrite the main graph with containment triples). * If there are no triples we assume it's a container (so don't overwrite the main graph with containment triples).
* @param name - Name of the resource to update. * @param name - Name of the resource to update.
* @param parent - Name of the parent to update the containment triples.
* @param metadata - New metadata of the resource. * @param metadata - New metadata of the resource.
* @param parent - Name of the parent to update the containment triples.
* @param triples - New data of the resource. * @param triples - New data of the resource.
*/ */
private sparqlInsert(name: NamedNode, metadata: RepresentationMetadata, parent?: NamedNode, triples?: Quad[]): private sparqlInsert(name: NamedNode, metadata: RepresentationMetadata, parent?: NamedNode, triples?: Quad[]):

View File

@ -1,5 +1,4 @@
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3';
import type { NamedNode, Quad } from 'rdf-js'; import type { NamedNode, Quad } from 'rdf-js';
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation'; import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
import type { Representation } from '../ldp/representation/Representation'; import type { Representation } from '../ldp/representation/Representation';
@ -27,18 +26,6 @@ export function generateResourceQuads(subject: NamedNode, isContainer: boolean):
return quads; return quads;
} }
/**
* Helper function to generate the quads describing that the resource URIs are children of the container URI.
* @param containerURI - The URI of the container.
* @param childURIs - The URI of the child resources.
*
* @returns The generated quads.
*/
export function generateContainmentQuads(containerURI: NamedNode, childURIs: string[]): Quad[] {
return new RepresentationMetadata(containerURI,
{ [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads();
}
/** /**
* Helper function to clone a representation, the original representation can still be used. * Helper function to clone a representation, the original representation can still be used.
* This function loads the entire stream in memory. * This function loads the entire stream in memory.

View File

@ -19,6 +19,7 @@ import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
import type { Guarded } from '../../../src/util/GuardedStream'; import type { Guarded } from '../../../src/util/GuardedStream';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy'; import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
import { trimTrailingSlashes } from '../../../src/util/PathUtil';
import * as quadUtil from '../../../src/util/QuadUtil'; import * as quadUtil from '../../../src/util/QuadUtil';
import { guardedStreamFrom } from '../../../src/util/StreamUtil'; import { guardedStreamFrom } from '../../../src/util/StreamUtil';
import { CONTENT_TYPE, HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies'; 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; 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> { public async modifyResource(): Promise<void> {
throw new Error('modify'); throw new Error('modify');
} }
@ -100,7 +110,7 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
public async addMetadata(metadata: RepresentationMetadata): Promise<void> { public async addMetadata(metadata: RepresentationMetadata): Promise<void> {
const identifier = { path: metadata.identifier.value }; const identifier = { path: metadata.identifier.value };
// Random triple to test on // 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> { public async validate(): Promise<void> {
@ -157,7 +167,7 @@ describe('A DataAccessorBasedStore', (): void => {
expect(result).toMatchObject({ binary: true }); expect(result).toMatchObject({ binary: true });
expect(await arrayifyStream(result.data)).toEqual([ resourceData ]); expect(await arrayifyStream(result.data)).toEqual([ resourceData ]);
expect(result.metadata.contentType).toEqual('text/plain'); 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> => { 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(result).toMatchObject({ binary: false });
expect(await arrayifyStream(result.data)).toBeRdfIsomorphic(metaMirror.quads()); expect(await arrayifyStream(result.data)).toBeRdfIsomorphic(metaMirror.quads());
expect(result.metadata.contentType).toEqual(INTERNAL_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> => { it('will remove containment triples referencing auxiliary resources.', async(): Promise<void> => {
const resourceID = { path: `${root}container/` }; const resourceID = { path: `${root}container/` };
containerMetadata.identifier = namedNode(resourceID.path); 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] = { 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 result = await store.getRepresentation(resourceID);
const contains = result.metadata.getAll(LDP.terms.contains); const contains = result.metadata.getAll(LDP.terms.contains);
expect(contains).toHaveLength(1); 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 }; const resourceID = { path: root };
representation.metadata.add(HTTP.slug, 'newName'); representation.metadata.add(HTTP.slug, 'newName');
accessor.data[`${root}newName`] = representation; accessor.data[`${root}newName`] = representation;
accessor.data[root].metadata.add(LDP.contains, DataFactory.namedNode(`${root}newName`));
const result = await store.addResource(resourceID, representation); const result = await store.addResource(resourceID, representation);
expect(result).not.toEqual({ expect(result).not.toEqual({
path: `${root}newName`, path: `${root}newName`,
@ -522,7 +529,7 @@ describe('A DataAccessorBasedStore', (): void => {
it('will error when deleting non-empty containers.', async(): Promise<void> => { it('will error when deleting non-empty containers.', async(): Promise<void> => {
accessor.data[`${root}container/`] = representation; 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/` }); const result = store.deleteResource({ path: `${root}container/` });
await expect(result).rejects.toThrow(ConflictHttpError); await expect(result).rejects.toThrow(ConflictHttpError);
await expect(result).rejects.toThrow('Can only delete empty containers.'); await expect(result).rejects.toThrow('Can only delete empty containers.');

View File

@ -111,7 +111,7 @@ describe('A FileDataAccessor', (): void => {
expect(metadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime)); 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: {}}}; cache.data = { container: { resource: 'data', 'resource.meta': 'metadata', notAFile: 5, container2: {}}};
metadata = await accessor.getMetadata({ path: `${base}container/` }); metadata = await accessor.getMetadata({ path: `${base}container/` });
expect(metadata.identifier.value).toBe(`${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(POSIX.size)).toBeUndefined();
expect(metadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime)); 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.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 => it('generates metadata for container child resources.', async(): Promise<void> => {
quad.subject.value === `${base}container/resource`); cache.data = { container: { resource: 'data', 'resource.meta': 'metadata', notAFile: 5, container2: {}}};
const childMetadata = new RepresentationMetadata({ path: `${base}container/resource` }).addQuads(childQuads); const children = [];
expect(childMetadata.get(RDF.type)?.value).toBe(LDP.Resource); for await (const child of accessor.getChildren({ path: `${base}container/` })) {
expect(childMetadata.get(POSIX.size)).toEqualRdfTerm(toLiteral('data'.length, XSD.terms.integer)); children.push(child);
expect(childMetadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime)); }
expect(childMetadata.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000), 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)); XSD.terms.integer));
}
}); });
it('adds stored metadata when requesting metadata.', async(): Promise<void> => { it('adds stored metadata when requesting metadata.', async(): Promise<void> => {

View File

@ -1,6 +1,5 @@
import 'jest-rdf'; import 'jest-rdf';
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import { DataFactory } from 'n3';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
import { InMemoryDataAccessor } from '../../../../src/storage/accessors/InMemoryDataAccessor'; import { InMemoryDataAccessor } from '../../../../src/storage/accessors/InMemoryDataAccessor';
@ -85,16 +84,20 @@ describe('An InMemoryDataAccessor', (): void => {
expect(metadata.quads()).toHaveLength(0); 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.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined();
await expect(accessor.writeDocument({ path: `${base}container/resource` }, data, metadata)) await expect(accessor.writeDocument({ path: `${base}container/resource` }, data, metadata))
.resolves.toBeUndefined(); .resolves.toBeUndefined();
await expect(accessor.writeContainer({ path: `${base}container/container2/` }, metadata)) await expect(accessor.writeContainer({ path: `${base}container/container2/` }, metadata))
.resolves.toBeUndefined(); .resolves.toBeUndefined();
metadata = await accessor.getMetadata({ path: `${base}container/` });
expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray( const children = [];
[ DataFactory.namedNode(`${base}container/resource`), DataFactory.namedNode(`${base}container/container2/`) ], 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> => { it('adds stored metadata when requesting document metadata.', async(): Promise<void> => {
@ -136,10 +139,16 @@ describe('An InMemoryDataAccessor', (): void => {
metadata = await accessor.getMetadata(identifier); metadata = await accessor.getMetadata(identifier);
expect(metadata.identifier.value).toBe(`${base}container/`); expect(metadata.identifier.value).toBe(`${base}container/`);
const quads = metadata.quads(); const quads = metadata.quads();
expect(quads).toHaveLength(3); expect(quads).toHaveLength(2);
expect(metadata.getAll(RDF.type).map((term): string => term.value)) expect(metadata.getAll(RDF.type).map((term): string => term.value))
.toEqual([ LDP.Container, LDP.BasicContainer ]); .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` })) await expect(accessor.getMetadata({ path: `${base}container/resource` }))
.resolves.toBeInstanceOf(RepresentationMetadata); .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> => { 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 }); const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: LDP.terms.Container });
await expect(accessor.writeContainer(identifier, inputMetadata)).resolves.toBeUndefined(); await expect(accessor.writeContainer(identifier, inputMetadata)).resolves.toBeUndefined();
const resourceMetadata = new RepresentationMetadata(); const resourceMetadata = new RepresentationMetadata();
@ -158,21 +167,39 @@ describe('An InMemoryDataAccessor', (): void => {
metadata = await accessor.getMetadata(identifier); metadata = await accessor.getMetadata(identifier);
expect(metadata.identifier.value).toBe(`${base}`); expect(metadata.identifier.value).toBe(`${base}`);
const quads = metadata.quads(); const quads = metadata.quads();
expect(quads).toHaveLength(2); expect(quads).toHaveLength(1);
expect(metadata.getAll(RDF.type)).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` })) await expect(accessor.getMetadata({ path: `${base}resource` }))
.resolves.toBeInstanceOf(RepresentationMetadata); .resolves.toBeInstanceOf(RepresentationMetadata);
expect(await readableToString(await accessor.getData({ path: `${base}resource` }))).toBe('data'); 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.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('Invalid path.'); .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 => { describe('deleting a resource', (): void => {

View File

@ -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/' }); metadata = await accessor.getMetadata({ path: 'http://container/' });
expect(metadata.quads()).toBeRdfIsomorphic([ expect(metadata.quads()).toBeRdfIsomorphic([
quad(namedNode('this'), namedNode('a'), namedNode('triple')), quad(namedNode('this'), namedNode('a'), namedNode('triple')),
@ -111,13 +111,25 @@ describe('A SparqlDataAccessor', (): void => {
expect(fetchTriples).toHaveBeenCalledTimes(1); expect(fetchTriples).toHaveBeenCalledTimes(1);
expect(fetchTriples.mock.calls[0][0]).toBe(endpoint); expect(fetchTriples.mock.calls[0][0]).toBe(endpoint);
expect(simplifyQuery(fetchTriples.mock.calls[0][1])).toBe(simplifyQuery([ expect(simplifyQuery(fetchTriples.mock.calls[0][1])).toBe(simplifyQuery(
'CONSTRUCT { ?s ?p ?o. } WHERE {', 'CONSTRUCT { ?s ?p ?o. } WHERE { GRAPH <meta:http://container/> { ?s ?p ?o. } }',
' { GRAPH <http://container/> { ?s ?p ?o. } }', ));
' UNION', });
' { 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> => { it('throws 404 if no metadata was found.', async(): Promise<void> => {

View File

@ -1,4 +1,5 @@
import type { Stats } from 'fs'; import type { Dirent, Stats } from 'fs';
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import type { SystemError } from '../../src/util/errors/SystemError'; 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]); 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 { mkdir(path: string): void {
const { folder, name } = getFolder(path); const { folder, name } = getFolder(path);
if (folder[name]) { if (folder[name]) {