feat: Add BasicRepresentation.

This commit is contained in:
Ruben Verborgh 2021-01-10 23:58:52 +01:00
parent be1af89b56
commit 66e636878f
20 changed files with 283 additions and 153 deletions

View File

@ -1,4 +1,5 @@
[ [
"BasicRepresentation",
"Error", "Error",
"EventEmitter", "EventEmitter",
"ValuePreferencesArg" "ValuePreferencesArg"

View File

@ -71,6 +71,7 @@ export * from './ldp/permissions/MethodPermissionsExtractor';
export * from './ldp/permissions/SparqlPatchPermissionsExtractor'; export * from './ldp/permissions/SparqlPatchPermissionsExtractor';
// LDP/Representation // LDP/Representation
export * from './ldp/representation/BasicRepresentation';
export * from './ldp/representation/Representation'; export * from './ldp/representation/Representation';
export * from './ldp/representation/RepresentationMetadata'; export * from './ldp/representation/RepresentationMetadata';
export * from './ldp/representation/RepresentationPreferences'; export * from './ldp/representation/RepresentationPreferences';

View File

@ -1,12 +1,11 @@
import type { AclManager } from '../authorization/AclManager'; import type { AclManager } from '../authorization/AclManager';
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
import type { ResourceStore } from '../storage/ResourceStore'; import type { ResourceStore } from '../storage/ResourceStore';
import { TEXT_TURTLE } from '../util/ContentTypes'; import { TEXT_TURTLE } from '../util/ContentTypes';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { ensureTrailingSlash } from '../util/PathUtil'; import { ensureTrailingSlash } from '../util/PathUtil';
import { guardedStreamFrom } from '../util/StreamUtil';
import { Initializer } from './Initializer'; import { Initializer } from './Initializer';
/** /**
@ -66,15 +65,7 @@ export class AclInitializer extends Initializer {
acl:mode acl:Control; acl:mode acl:Control;
acl:accessTo <${this.baseUrl}>; acl:accessTo <${this.baseUrl}>;
acl:default <${this.baseUrl}>.`; acl:default <${this.baseUrl}>.`;
const metadata = new RepresentationMetadata(rootAcl, TEXT_TURTLE);
this.logger.debug(`Installing root ACL document at ${rootAcl.path}`); this.logger.debug(`Installing root ACL document at ${rootAcl.path}`);
await this.store.setRepresentation( await this.store.setRepresentation(rootAcl, new BasicRepresentation(acl, rootAcl, TEXT_TURTLE));
rootAcl,
{
binary: true,
data: guardedStreamFrom([ acl ]),
metadata,
},
);
} }
} }

View File

@ -1,4 +1,5 @@
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
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 { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
@ -7,7 +8,6 @@ import { TEXT_TURTLE } from '../util/ContentTypes';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { ensureTrailingSlash } from '../util/PathUtil'; import { ensureTrailingSlash } from '../util/PathUtil';
import { generateResourceQuads } from '../util/ResourceUtil'; import { generateResourceQuads } from '../util/ResourceUtil';
import { guardedStreamFrom } from '../util/StreamUtil';
import { PIM, RDF } from '../util/Vocabularies'; import { PIM, RDF } from '../util/Vocabularies';
import { Initializer } from './Initializer'; import { Initializer } from './Initializer';
import namedNode = DataFactory.namedNode; import namedNode = DataFactory.namedNode;
@ -54,20 +54,14 @@ export class RootContainerInitializer extends Initializer {
* Create a root container in a ResourceStore. * Create a root container in a ResourceStore.
*/ */
protected async createRootContainer(): Promise<void> { protected async createRootContainer(): Promise<void> {
const metadata = new RepresentationMetadata(this.baseId); const metadata = new RepresentationMetadata(this.baseId, TEXT_TURTLE);
metadata.addQuads(generateResourceQuads(namedNode(this.baseId.path), true)); metadata.addQuads(generateResourceQuads(namedNode(this.baseId.path), true));
// Make sure the root container is a pim:Storage // Make sure the root container is a pim:Storage
// This prevents deletion of the root container as storage root containers can not be deleted // This prevents deletion of the root container as storage root containers can not be deleted
metadata.add(RDF.type, PIM.terms.Storage); metadata.add(RDF.type, PIM.terms.Storage);
metadata.contentType = TEXT_TURTLE;
this.logger.debug(`Creating root container at ${this.baseId.path}`); this.logger.debug(`Creating root container at ${this.baseId.path}`);
await this.store.setRepresentation(this.baseId, { await this.store.setRepresentation(this.baseId, new BasicRepresentation([], metadata));
binary: true,
data: guardedStreamFrom([]),
metadata,
});
} }
} }

View File

@ -1,5 +1,6 @@
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { BasicRepresentation } from '../representation/BasicRepresentation';
import type { Representation } from '../representation/Representation'; import type { Representation } from '../representation/Representation';
import type { BodyParserArgs } from './BodyParser'; import type { BodyParserArgs } from './BodyParser';
import { BodyParser } from './BodyParser'; import { BodyParser } from './BodyParser';
@ -27,10 +28,6 @@ export class RawBodyParser extends BodyParser {
throw new BadRequestHttpError('HTTP request body was passed without Content-Type header'); throw new BadRequestHttpError('HTTP request body was passed without Content-Type header');
} }
return { return new BasicRepresentation(request, metadata);
binary: true,
data: request,
metadata,
};
} }
} }

View File

@ -0,0 +1,113 @@
import type { Readable } from 'stream';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import type { Guarded } from '../../util/GuardedStream';
import { guardStream } from '../../util/GuardedStream';
import { guardedStreamFrom } from '../../util/StreamUtil';
import type { Representation } from './Representation';
import type { MetadataIdentifier, MetadataRecord } from './RepresentationMetadata';
import { RepresentationMetadata, isRepresentationMetadata } from './RepresentationMetadata';
/**
* Class with various constructors to facilitate creating a representation.
*
* A representation consists of 1) data, 2) metadata, and 3) a binary flag
* to indicate whether the data is a binary stream or an object stream.
*
* 1. The data can be given as a stream, array, or string.
* 2. The metadata can be specified as one or two parameters
* that will be passed to the {@link RepresentationMetadata} constructor.
* 3. The binary field is optional, and if not specified,
* is determined from the content type inside the metadata.
*/
export class BasicRepresentation implements Representation {
public readonly data: Guarded<Readable>;
public readonly metadata: RepresentationMetadata;
public readonly binary: boolean;
/**
* @param data - The representation data
* @param metadata - The representation metadata
* @param binary - Whether the representation is a binary or object stream
*/
public constructor(
data: Guarded<Readable> | Readable | any[] | string,
metadata: RepresentationMetadata | MetadataRecord,
binary?: boolean,
);
/**
* @param data - The representation data
* @param metadata - The representation metadata
* @param contentType - The representation's content type
* @param binary - Whether the representation is a binary or object stream
*/
public constructor(
data: Guarded<Readable> | Readable | any[] | string,
metadata: RepresentationMetadata | MetadataRecord,
contentType?: string,
binary?: boolean,
);
/**
* @param data - The representation data
* @param contentType - The representation's content type
* @param binary - Whether the representation is a binary or object stream
*/
public constructor(
data: Guarded<Readable> | Readable | any[] | string,
contentType: string,
binary?: boolean,
);
/**
* @param data - The representation data
* @param identifier - The representation's identifier
* @param metadata - The representation metadata
* @param binary - Whether the representation is a binary or object stream
*/
public constructor(
data: Guarded<Readable> | Readable | any[] | string,
identifier: MetadataIdentifier,
metadata?: MetadataRecord,
binary?: boolean,
);
/**
* @param data - The representation data
* @param identifier - The representation's identifier
* @param contentType - The representation's content type
* @param binary - Whether the representation is a binary or object stream
*/
public constructor(
data: Guarded<Readable> | Readable | any[] | string,
identifier: MetadataIdentifier,
contentType?: string,
binary?: boolean,
);
public constructor(
data: Readable | any[] | string,
metadata: RepresentationMetadata | MetadataRecord | MetadataIdentifier | string,
metadataRest?: MetadataRecord | string | boolean,
binary?: boolean,
) {
if (typeof data === 'string' || Array.isArray(data)) {
data = guardedStreamFrom(data);
}
this.data = guardStream(data);
if (typeof metadataRest === 'boolean') {
binary = metadataRest;
metadataRest = undefined;
}
if (!isRepresentationMetadata(metadata) || typeof metadataRest === 'string') {
metadata = new RepresentationMetadata(metadata as any, metadataRest as any);
}
this.metadata = metadata;
if (typeof binary !== 'boolean') {
binary = metadata.contentType !== INTERNAL_QUADS;
}
this.binary = binary;
}
}

View File

@ -1,5 +1,6 @@
import { promises as fsPromises } from 'fs'; import { promises as fsPromises } from 'fs';
import { Parser } from 'n3'; import { Parser } from 'n3';
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
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 type { import type {
@ -8,7 +9,6 @@ import type {
ResourceLink, ResourceLink,
} from '../../storage/mapping/FileIdentifierMapper'; } from '../../storage/mapping/FileIdentifierMapper';
import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil'; import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil';
import { guardedStreamFrom } from '../../util/StreamUtil';
import type { Resource, ResourcesGenerator } from './ResourcesGenerator'; import type { Resource, ResourcesGenerator } from './ResourcesGenerator';
import type { TemplateEngine } from './TemplateEngine'; import type { TemplateEngine } from './TemplateEngine';
import Dict = NodeJS.Dict; import Dict = NodeJS.Dict;
@ -123,11 +123,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
return { return {
identifier: link.identifier, identifier: link.identifier,
representation: { representation: new BasicRepresentation(data, metadata),
binary: true,
data: guardedStreamFrom(data),
metadata,
},
}; };
} }

View File

@ -2,8 +2,9 @@ import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import type { Quad, Term } from 'rdf-js'; import type { Quad, Term } from 'rdf-js';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
import type { Representation } from '../ldp/representation/Representation'; import type { Representation } from '../ldp/representation/Representation';
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { INTERNAL_QUADS } from '../util/ContentTypes'; import { INTERNAL_QUADS } from '../util/ContentTypes';
import { BadRequestHttpError } from '../util/errors/BadRequestHttpError'; import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
@ -21,7 +22,6 @@ import {
} from '../util/PathUtil'; } from '../util/PathUtil';
import { parseQuads } from '../util/QuadUtil'; import { parseQuads } from '../util/QuadUtil';
import { generateResourceQuads } from '../util/ResourceUtil'; import { generateResourceQuads } from '../util/ResourceUtil';
import { guardedStreamFrom } from '../util/StreamUtil';
import { CONTENT_TYPE, HTTP, LDP, PIM, RDF } from '../util/Vocabularies'; import { CONTENT_TYPE, HTTP, LDP, PIM, RDF } from '../util/Vocabularies';
import type { DataAccessor } from './accessors/DataAccessor'; import type { DataAccessor } from './accessors/DataAccessor';
import type { ResourceStore } from './ResourceStore'; import type { ResourceStore } from './ResourceStore';
@ -64,28 +64,9 @@ export class DataAccessorBasedStore implements ResourceStore {
// In the future we want to use getNormalizedMetadata and redirect in case the identifier differs // In the future we want to use getNormalizedMetadata and redirect in case the identifier differs
const metadata = await this.accessor.getMetadata(identifier); const metadata = await this.accessor.getMetadata(identifier);
let result: Representation; return this.isExistingContainer(metadata) ?
new BasicRepresentation(metadata.quads(), metadata, INTERNAL_QUADS) :
// Create the representation of a container new BasicRepresentation(await this.accessor.getData(identifier), metadata);
if (this.isExistingContainer(metadata)) {
// Generate the data stream before setting the content-type to prevent unnecessary triples
const data = guardedStreamFrom(metadata.quads());
metadata.contentType = INTERNAL_QUADS;
result = {
binary: false,
data,
metadata,
};
// Obtain a representation of a document
} else {
result = {
binary: metadata.contentType !== INTERNAL_QUADS,
data: await this.accessor.getData(identifier),
metadata,
};
}
return result;
} }
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> { public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
@ -374,22 +355,10 @@ export class DataAccessorBasedStore implements ResourceStore {
if (error instanceof NotFoundHttpError) { if (error instanceof NotFoundHttpError) {
// Make sure the parent exists first // Make sure the parent exists first
await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container)); await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container));
await this.writeData(container, this.getEmptyContainerRepresentation(container), true); await this.writeData(container, new BasicRepresentation([], container), true);
} else { } else {
throw error; throw error;
} }
} }
} }
/**
* Generates the minimal representation for an empty container.
* @param container - Identifier of this new container.
*/
protected getEmptyContainerRepresentation(container: ResourceIdentifier): Representation {
return {
binary: true,
data: guardedStreamFrom([]),
metadata: new RepresentationMetadata(container),
};
}
} }

View File

@ -1,11 +1,10 @@
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import { StreamWriter } from 'n3'; import { StreamWriter } from 'n3';
import rdfSerializer from 'rdf-serialize'; import rdfSerializer from 'rdf-serialize';
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
import type { Representation } from '../../ldp/representation/Representation'; import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ValuePreferences } from '../../ldp/representation/RepresentationPreferences'; import type { ValuePreferences } from '../../ldp/representation/RepresentationPreferences';
import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { guardStream } from '../../util/GuardedStream';
import { pipeSafely } from '../../util/StreamUtil'; import { pipeSafely } from '../../util/StreamUtil';
import { PREFERRED_PREFIX_TERM } from '../../util/Vocabularies'; import { PREFERRED_PREFIX_TERM } from '../../util/Vocabularies';
import { matchingMediaTypes } from './ConversionUtil'; import { matchingMediaTypes } from './ConversionUtil';
@ -27,22 +26,18 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
public async handle({ representation: quads, preferences }: RepresentationConverterArgs): Promise<Representation> { public async handle({ representation: quads, preferences }: RepresentationConverterArgs): Promise<Representation> {
const contentType = matchingMediaTypes(preferences.type, await this.getOutputTypes())[0]; const contentType = matchingMediaTypes(preferences.type, await this.getOutputTypes())[0];
const metadata = new RepresentationMetadata(quads.metadata, contentType);
let data: Readable; let data: Readable;
// Use prefixes if possible (see https://github.com/rubensworks/rdf-serialize.js/issues/1) // Use prefixes if possible (see https://github.com/rubensworks/rdf-serialize.js/issues/1)
if (/(?:turtle|trig)$/u.test(contentType)) { if (/(?:turtle|trig)$/u.test(contentType)) {
const prefixes = Object.fromEntries(metadata.quads(null, PREFERRED_PREFIX_TERM, null) const prefixes = Object.fromEntries(quads.metadata.quads(null, PREFERRED_PREFIX_TERM, null)
.map(({ subject, object }): [string, string] => [ object.value, subject.value ])); .map(({ subject, object }): [string, string] => [ object.value, subject.value ]));
data = pipeSafely(quads.data, new StreamWriter({ format: contentType, prefixes })); data = pipeSafely(quads.data, new StreamWriter({ format: contentType, prefixes }));
// Otherwise, write without prefixes // Otherwise, write without prefixes
} else { } else {
data = rdfSerializer.serialize(quads.data, { contentType }) as Readable; data = rdfSerializer.serialize(quads.data, { contentType }) as Readable;
} }
return {
binary: true, return new BasicRepresentation(data, quads.metadata, contentType);
data: guardStream(data),
metadata,
};
} }
} }

View File

@ -1,7 +1,7 @@
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
import rdfParser from 'rdf-parse'; import rdfParser from 'rdf-parse';
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
import type { Representation } from '../../ldp/representation/Representation'; import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { pipeSafely } from '../../util/StreamUtil'; import { pipeSafely } from '../../util/StreamUtil';
@ -17,19 +17,13 @@ export class RdfToQuadConverter extends TypedRepresentationConverter {
} }
public async handle({ representation, identifier }: RepresentationConverterArgs): Promise<Representation> { public async handle({ representation, identifier }: RepresentationConverterArgs): Promise<Representation> {
const metadata = new RepresentationMetadata(representation.metadata, INTERNAL_QUADS);
const rawQuads = rdfParser.parse(representation.data, { const rawQuads = rdfParser.parse(representation.data, {
contentType: representation.metadata.contentType!, contentType: representation.metadata.contentType!,
baseIRI: identifier.path, baseIRI: identifier.path,
}); });
const pass = new PassThrough({ objectMode: true }); const pass = new PassThrough({ objectMode: true });
const data = pipeSafely(rawQuads, pass, (error): Error => new BadRequestHttpError(error.message)); const data = pipeSafely(rawQuads, pass, (error): Error => new BadRequestHttpError(error.message));
return { return new BasicRepresentation(data, representation.metadata, INTERNAL_QUADS);
binary: false,
data,
metadata,
};
} }
} }

View File

@ -5,14 +5,12 @@ import type { BaseQuad } from 'rdf-js';
import { someTerms } from 'rdf-terms'; import { someTerms } from 'rdf-terms';
import { Algebra } from 'sparqlalgebrajs'; import { Algebra } from 'sparqlalgebrajs';
import type { SparqlUpdatePatch } from '../../ldp/http/SparqlUpdatePatch'; import type { SparqlUpdatePatch } from '../../ldp/http/SparqlUpdatePatch';
import type { Representation } from '../../ldp/representation/Representation'; import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { guardStream } from '../../util/GuardedStream';
import type { ResourceLocker } from '../../util/locking/ResourceLocker'; import type { ResourceLocker } from '../../util/locking/ResourceLocker';
import type { ResourceStore } from '../ResourceStore'; import type { ResourceStore } from '../ResourceStore';
import { PatchHandler } from './PatchHandler'; import { PatchHandler } from './PatchHandler';
@ -108,12 +106,6 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
this.logger.debug(`${store.size} quads will be stored to ${identifier.path}.`); this.logger.debug(`${store.size} quads will be stored to ${identifier.path}.`);
// Write the result // Write the result
const metadata = new RepresentationMetadata(identifier, INTERNAL_QUADS); await this.source.setRepresentation(identifier, new BasicRepresentation(store.match() as Readable, INTERNAL_QUADS));
const representation: Representation = {
binary: false,
data: guardStream(store.match() as Readable),
metadata,
};
await this.source.setRepresentation(identifier, representation);
} }
} }

View File

@ -25,7 +25,7 @@ export type Guarded<T extends NodeJS.EventEmitter = NodeJS.EventEmitter> = T & G
* Determines whether the stream is guarded from emitting errors. * Determines whether the stream is guarded from emitting errors.
*/ */
export function isGuarded<T extends NodeJS.EventEmitter>(stream: T): stream is Guarded<T> { export function isGuarded<T extends NodeJS.EventEmitter>(stream: T): stream is Guarded<T> {
return guardedErrors in stream; return typeof (stream as any)[guardedErrors] === 'object';
} }
/** /**

View File

@ -97,10 +97,10 @@ export function transformSafely<T = any>(
} }
/** /**
* Converts an iterable to a stream and applies an error guard so that it is {@link Guarded}. * Converts a string or array to a stream and applies an error guard so that it is {@link Guarded}.
* @param iterable - Data to stream. * @param contents - Data to stream.
* @param options - Options to pass to the Readable constructor. See {@link Readable.from}. * @param options - Options to pass to the Readable constructor. See {@link Readable.from}.
*/ */
export function guardedStreamFrom(iterable: Iterable<any>, options?: ReadableOptions): Guarded<Readable> { export function guardedStreamFrom(contents: string | Iterable<any>, options?: ReadableOptions): Guarded<Readable> {
return guardStream(Readable.from(iterable, options)); return guardStream(Readable.from(typeof contents === 'string' ? [ contents ] : contents, options));
} }

View File

@ -1,6 +1,6 @@
import { createReadStream } from 'fs'; import { createReadStream } from 'fs';
import type { HttpHandler, Initializer, ResourceStore } from '../../src/'; import type { HttpHandler, Initializer, ResourceStore } from '../../src/';
import { LDP, RepresentationMetadata, guardStream, joinFilePath } from '../../src/'; import { LDP, BasicRepresentation, joinFilePath } from '../../src/';
import { AclHelper, ResourceHelper } from '../util/TestHelpers'; import { AclHelper, ResourceHelper } from '../util/TestHelpers';
import { BASE, getTestFolder, createFolder, removeFolder, instantiateFromConfig } from './Config'; import { BASE, getTestFolder, createFolder, removeFolder, instantiateFromConfig } from './Config';
@ -53,11 +53,8 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, se
resourceHelper = new ResourceHelper(handler, BASE); resourceHelper = new ResourceHelper(handler, BASE);
// Write test resource // Write test resource
await store.setRepresentation({ path: `${BASE}/permanent.txt` }, { await store.setRepresentation({ path: `${BASE}/permanent.txt` },
binary: true, new BasicRepresentation(createReadStream(joinFilePath(__dirname, '../assets/permanent.txt')), 'text/plain'));
data: guardStream(createReadStream(joinFilePath(__dirname, '../assets/permanent.txt'))),
metadata: new RepresentationMetadata('text/plain'),
});
}); });
afterAll(async(): Promise<void> => { afterAll(async(): Promise<void> => {

View File

@ -1,7 +1,7 @@
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import { RootContainerInitializer } from '../../src/init/RootContainerInitializer'; import { RootContainerInitializer } from '../../src/init/RootContainerInitializer';
import { BasicRepresentation } from '../../src/ldp/representation/BasicRepresentation';
import type { Representation } from '../../src/ldp/representation/Representation'; import type { Representation } from '../../src/ldp/representation/Representation';
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor'; import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
import { DataAccessorBasedStore } from '../../src/storage/DataAccessorBasedStore'; import { DataAccessorBasedStore } from '../../src/storage/DataAccessorBasedStore';
import { LockingResourceStore } from '../../src/storage/LockingResourceStore'; import { LockingResourceStore } from '../../src/storage/LockingResourceStore';
@ -12,7 +12,6 @@ import type { ExpiringResourceLocker } from '../../src/util/locking/ExpiringReso
import type { ResourceLocker } from '../../src/util/locking/ResourceLocker'; import type { ResourceLocker } from '../../src/util/locking/ResourceLocker';
import { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThreadedResourceLocker'; import { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThreadedResourceLocker';
import { WrappedExpiringResourceLocker } from '../../src/util/locking/WrappedExpiringResourceLocker'; import { WrappedExpiringResourceLocker } from '../../src/util/locking/WrappedExpiringResourceLocker';
import { guardedStreamFrom } from '../../src/util/StreamUtil';
import { BASE } from './Config'; import { BASE } from './Config';
describe('A LockingResourceStore', (): void => { describe('A LockingResourceStore', (): void => {
@ -39,9 +38,7 @@ describe('A LockingResourceStore', (): void => {
store = new LockingResourceStore(source, expiringLocker); store = new LockingResourceStore(source, expiringLocker);
// Make sure something is in the store before we read from it in our tests. // Make sure something is in the store before we read from it in our tests.
const metadata = new RepresentationMetadata(APPLICATION_OCTET_STREAM); await store.setRepresentation({ path }, new BasicRepresentation([ 1, 2, 3 ], APPLICATION_OCTET_STREAM));
const data = guardedStreamFrom([ 1, 2, 3 ]);
await store.setRepresentation({ path }, { metadata, data, binary: true });
}); });
it('destroys the stream when nothing is read after 1000ms.', async(): Promise<void> => { it('destroys the stream when nothing is read after 1000ms.', async(): Promise<void> => {

View File

@ -1,9 +1,8 @@
import type { Representation } from '../../src/ldp/representation/Representation'; import { BasicRepresentation } from '../../src/ldp/representation/BasicRepresentation';
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
import { ChainedConverter } from '../../src/storage/conversion/ChainedConverter'; import { ChainedConverter } from '../../src/storage/conversion/ChainedConverter';
import { QuadToRdfConverter } from '../../src/storage/conversion/QuadToRdfConverter'; import { QuadToRdfConverter } from '../../src/storage/conversion/QuadToRdfConverter';
import { RdfToQuadConverter } from '../../src/storage/conversion/RdfToQuadConverter'; import { RdfToQuadConverter } from '../../src/storage/conversion/RdfToQuadConverter';
import { guardedStreamFrom, readableToString } from '../../src/util/StreamUtil'; import { readableToString } from '../../src/util/StreamUtil';
describe('A ChainedConverter', (): void => { describe('A ChainedConverter', (): void => {
const converters = [ const converters = [
@ -13,14 +12,10 @@ describe('A ChainedConverter', (): void => {
const converter = new ChainedConverter(converters); const converter = new ChainedConverter(converters);
it('can convert from JSON-LD to turtle.', async(): Promise<void> => { it('can convert from JSON-LD to turtle.', async(): Promise<void> => {
const metadata = new RepresentationMetadata('application/ld+json'); const representation = new BasicRepresentation(
const representation: Representation = { '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}',
binary: true, 'application/ld+json',
data: guardedStreamFrom( );
[ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ],
),
metadata,
};
const result = await converter.handleSafe({ const result = await converter.handleSafe({
representation, representation,
@ -33,12 +28,10 @@ describe('A ChainedConverter', (): void => {
}); });
it('can convert from turtle to JSON-LD.', async(): Promise<void> => { it('can convert from turtle to JSON-LD.', async(): Promise<void> => {
const metadata = new RepresentationMetadata('text/turtle'); const representation = new BasicRepresentation(
const representation: Representation = { '<http://test.com/s> <http://test.com/p> <http://test.com/o>.',
binary: true, 'text/turtle',
data: guardedStreamFrom([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]), );
metadata,
};
const result = await converter.handleSafe({ const result = await converter.handleSafe({
representation, representation,

View File

@ -0,0 +1,118 @@
import 'jest-rdf';
import { namedNode } from '@rdfjs/data-model';
import arrayifyStream from 'arrayify-stream';
import streamifyArray from 'streamify-array';
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
describe('BasicRepresentation', (): void => {
it('creates a representation with (data, metadata, binary).', (): void => {
const data = guardedStreamFrom([ '' ]);
const metadata = new RepresentationMetadata();
const representation = new BasicRepresentation(data, metadata, true);
expect(representation.data).toBe(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, metadata).', (): void => {
const data = guardedStreamFrom([ '' ]);
let metadata = new RepresentationMetadata();
let representation = new BasicRepresentation(data, metadata);
expect(representation.data).toBe(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
metadata = new RepresentationMetadata(INTERNAL_QUADS);
representation = new BasicRepresentation(data, metadata);
expect(representation.data).toBe(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(false);
});
it('creates a representation with (unguarded data, metadata).', (): void => {
const data = streamifyArray([ '' ]);
const metadata = new RepresentationMetadata();
const representation = new BasicRepresentation(data, metadata);
expect(representation.data).toBe(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
});
it('creates a representation with (array data, metadata).', async(): Promise<void> => {
const data = [ 'my', 'data' ];
const metadata = new RepresentationMetadata();
const representation = new BasicRepresentation(data, metadata);
expect(await arrayifyStream(representation.data)).toEqual(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
});
it('creates a representation with (string data, metadata).', async(): Promise<void> => {
const data = 'my data';
const metadata = new RepresentationMetadata();
const representation = new BasicRepresentation(data, metadata);
expect(await arrayifyStream(representation.data)).toEqual([ data ]);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, metadata record).', (): void => {
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, { [CONTENT_TYPE]: 'text/custom' });
expect(representation.data).toBe(data);
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, content type).', (): void => {
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, 'text/custom');
expect(representation.data).toBe(data);
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, identifier, metadata record).', (): void => {
const identifier = { path: 'http://example.org/#' };
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, identifier, { [CONTENT_TYPE]: 'text/custom' });
expect(representation.data).toBe(data);
expect(representation.metadata.identifier).toEqualRdfTerm(namedNode(identifier.path));
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, identifier, content type).', (): void => {
const identifier = { path: 'http://example.org/#' };
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, identifier, 'text/custom');
expect(representation.data).toBe(data);
expect(representation.metadata.identifier).toEqualRdfTerm(namedNode(identifier.path));
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, identifier term, metadata record).', (): void => {
const identifier = namedNode('http://example.org/#');
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, identifier, { [CONTENT_TYPE]: 'text/custom' });
expect(representation.data).toBe(data);
expect(representation.metadata.identifier).toBe(identifier);
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, identifier term, content type).', (): void => {
const identifier = namedNode('http://example.org/#');
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, identifier, 'text/custom');
expect(representation.data).toBe(data);
expect(representation.metadata.identifier).toBe(identifier);
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
});

View File

@ -1,5 +1,4 @@
import { Readable } from 'stream'; import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
import type { Agent } from '../../../src/pods/agent/Agent'; import type { Agent } from '../../../src/pods/agent/Agent';
import type { IdentifierGenerator } from '../../../src/pods/generate/IdentifierGenerator'; import type { IdentifierGenerator } from '../../../src/pods/generate/IdentifierGenerator';
@ -46,11 +45,7 @@ describe('A GeneratedPodManager', (): void => {
}); });
it('throws an error if the generate identifier is not available.', async(): Promise<void> => { it('throws an error if the generate identifier is not available.', async(): Promise<void> => {
(store.getRepresentation as jest.Mock).mockImplementationOnce((): any => ({ (store.getRepresentation as jest.Mock).mockImplementationOnce((): any => new BasicRepresentation([], {}));
data: Readable.from([]),
metadata: new RepresentationMetadata(),
binary: true,
}));
const result = manager.createPod(agent); const result = manager.createPod(agent);
await expect(result).rejects.toThrow(`There already is a resource at ${base}user/`); await expect(result).rejects.toThrow(`There already is a resource at ${base}user/`);
await expect(result).rejects.toThrow(ConflictHttpError); await expect(result).rejects.toThrow(ConflictHttpError);

View File

@ -1,3 +1,4 @@
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
import type { Representation } from '../../../../src/ldp/representation/Representation'; import type { Representation } from '../../../../src/ldp/representation/Representation';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import { AgentJsonParser } from '../../../../src/pods/agent/AgentJsonParser'; import { AgentJsonParser } from '../../../../src/pods/agent/AgentJsonParser';
@ -11,13 +12,8 @@ describe('An AgentJsonParser', (): void => {
const parser = new AgentJsonParser(); const parser = new AgentJsonParser();
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
metadata = new RepresentationMetadata(); metadata = new RepresentationMetadata('application/json');
metadata.contentType = 'application/json'; representation = new BasicRepresentation([], metadata);
representation = {
binary: true,
data: guardedStreamFrom([]),
metadata,
};
}); });
it('only supports JSON data.', async(): Promise<void> => { it('only supports JSON data.', async(): Promise<void> => {

View File

@ -6,7 +6,7 @@ import * as url from 'url';
import type { MockResponse } from 'node-mocks-http'; import type { MockResponse } from 'node-mocks-http';
import { createResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http';
import type { ResourceStore, PermissionSet, HttpHandler, HttpRequest } from '../../src/'; import type { ResourceStore, PermissionSet, HttpHandler, HttpRequest } from '../../src/';
import { guardedStreamFrom, RepresentationMetadata, joinFilePath, ensureTrailingSlash } from '../../src/'; import { BasicRepresentation, joinFilePath, ensureTrailingSlash } from '../../src/';
import { performRequest } from './Util'; import { performRequest } from './Util';
/* eslint-disable jest/no-standalone-expect */ /* eslint-disable jest/no-standalone-expect */
@ -45,16 +45,7 @@ export class AclHelper {
acl.push('.'); acl.push('.');
const representation = { await this.store.setRepresentation({ path: `${this.id}.acl` }, new BasicRepresentation(acl, 'text/turtle'));
binary: true,
data: guardedStreamFrom(acl),
metadata: new RepresentationMetadata('text/turtle'),
};
return this.store.setRepresentation(
{ path: `${this.id}.acl` },
representation,
);
} }
} }