diff --git a/.componentsignore b/.componentsignore index 988877fa3..5bcc29553 100644 --- a/.componentsignore +++ b/.componentsignore @@ -1,4 +1,5 @@ [ + "BasicRepresentation", "Error", "EventEmitter", "ValuePreferencesArg" diff --git a/src/index.ts b/src/index.ts index c28cb1bd2..6c3af7e40 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,6 +71,7 @@ export * from './ldp/permissions/MethodPermissionsExtractor'; export * from './ldp/permissions/SparqlPatchPermissionsExtractor'; // LDP/Representation +export * from './ldp/representation/BasicRepresentation'; export * from './ldp/representation/Representation'; export * from './ldp/representation/RepresentationMetadata'; export * from './ldp/representation/RepresentationPreferences'; diff --git a/src/init/AclInitializer.ts b/src/init/AclInitializer.ts index ba70dd81a..a9c2f7961 100644 --- a/src/init/AclInitializer.ts +++ b/src/init/AclInitializer.ts @@ -1,12 +1,11 @@ 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 { getLoggerFor } from '../logging/LogUtil'; import type { ResourceStore } from '../storage/ResourceStore'; import { TEXT_TURTLE } from '../util/ContentTypes'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { ensureTrailingSlash } from '../util/PathUtil'; -import { guardedStreamFrom } from '../util/StreamUtil'; import { Initializer } from './Initializer'; /** @@ -66,15 +65,7 @@ export class AclInitializer extends Initializer { acl:mode acl:Control; acl:accessTo <${this.baseUrl}>; acl:default <${this.baseUrl}>.`; - const metadata = new RepresentationMetadata(rootAcl, TEXT_TURTLE); this.logger.debug(`Installing root ACL document at ${rootAcl.path}`); - await this.store.setRepresentation( - rootAcl, - { - binary: true, - data: guardedStreamFrom([ acl ]), - metadata, - }, - ); + await this.store.setRepresentation(rootAcl, new BasicRepresentation(acl, rootAcl, TEXT_TURTLE)); } } diff --git a/src/init/RootContainerInitializer.ts b/src/init/RootContainerInitializer.ts index 2d4e6a719..ef83094cf 100644 --- a/src/init/RootContainerInitializer.ts +++ b/src/init/RootContainerInitializer.ts @@ -1,4 +1,5 @@ import { DataFactory } from 'n3'; +import { BasicRepresentation } from '../ldp/representation/BasicRepresentation'; import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import { getLoggerFor } from '../logging/LogUtil'; @@ -7,7 +8,6 @@ import { TEXT_TURTLE } from '../util/ContentTypes'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { ensureTrailingSlash } from '../util/PathUtil'; import { generateResourceQuads } from '../util/ResourceUtil'; -import { guardedStreamFrom } from '../util/StreamUtil'; import { PIM, RDF } from '../util/Vocabularies'; import { Initializer } from './Initializer'; import namedNode = DataFactory.namedNode; @@ -54,20 +54,14 @@ export class RootContainerInitializer extends Initializer { * Create a root container in a ResourceStore. */ protected async createRootContainer(): Promise { - const metadata = new RepresentationMetadata(this.baseId); + const metadata = new RepresentationMetadata(this.baseId, TEXT_TURTLE); metadata.addQuads(generateResourceQuads(namedNode(this.baseId.path), true)); // Make sure the root container is a pim:Storage // This prevents deletion of the root container as storage root containers can not be deleted metadata.add(RDF.type, PIM.terms.Storage); - metadata.contentType = TEXT_TURTLE; - this.logger.debug(`Creating root container at ${this.baseId.path}`); - await this.store.setRepresentation(this.baseId, { - binary: true, - data: guardedStreamFrom([]), - metadata, - }); + await this.store.setRepresentation(this.baseId, new BasicRepresentation([], metadata)); } } diff --git a/src/ldp/http/RawBodyParser.ts b/src/ldp/http/RawBodyParser.ts index a05bbeb71..57ac98d44 100644 --- a/src/ldp/http/RawBodyParser.ts +++ b/src/ldp/http/RawBodyParser.ts @@ -1,5 +1,6 @@ import { getLoggerFor } from '../../logging/LogUtil'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; +import { BasicRepresentation } from '../representation/BasicRepresentation'; import type { Representation } from '../representation/Representation'; import type { BodyParserArgs } 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'); } - return { - binary: true, - data: request, - metadata, - }; + return new BasicRepresentation(request, metadata); } } diff --git a/src/ldp/representation/BasicRepresentation.ts b/src/ldp/representation/BasicRepresentation.ts new file mode 100644 index 000000000..7c9a93f62 --- /dev/null +++ b/src/ldp/representation/BasicRepresentation.ts @@ -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; + 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 | 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 | 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 | 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 | 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 | 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; + } +} diff --git a/src/pods/generate/TemplatedResourcesGenerator.ts b/src/pods/generate/TemplatedResourcesGenerator.ts index f7982cf5c..9237679ad 100644 --- a/src/pods/generate/TemplatedResourcesGenerator.ts +++ b/src/pods/generate/TemplatedResourcesGenerator.ts @@ -1,5 +1,6 @@ import { promises as fsPromises } from 'fs'; import { Parser } from 'n3'; +import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import type { @@ -8,7 +9,6 @@ import type { ResourceLink, } from '../../storage/mapping/FileIdentifierMapper'; import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil'; -import { guardedStreamFrom } from '../../util/StreamUtil'; import type { Resource, ResourcesGenerator } from './ResourcesGenerator'; import type { TemplateEngine } from './TemplateEngine'; import Dict = NodeJS.Dict; @@ -123,11 +123,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator { return { identifier: link.identifier, - representation: { - binary: true, - data: guardedStreamFrom(data), - metadata, - }, + representation: new BasicRepresentation(data, metadata), }; } diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts index 30771f174..7618faee8 100644 --- a/src/storage/DataAccessorBasedStore.ts +++ b/src/storage/DataAccessorBasedStore.ts @@ -2,8 +2,9 @@ import arrayifyStream from 'arrayify-stream'; import { DataFactory } from 'n3'; import type { Quad, Term } from 'rdf-js'; import { v4 as uuid } from 'uuid'; +import { BasicRepresentation } from '../ldp/representation/BasicRepresentation'; 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 { INTERNAL_QUADS } from '../util/ContentTypes'; import { BadRequestHttpError } from '../util/errors/BadRequestHttpError'; @@ -21,7 +22,6 @@ import { } from '../util/PathUtil'; import { parseQuads } from '../util/QuadUtil'; import { generateResourceQuads } from '../util/ResourceUtil'; -import { guardedStreamFrom } from '../util/StreamUtil'; import { CONTENT_TYPE, HTTP, LDP, PIM, RDF } from '../util/Vocabularies'; import type { DataAccessor } from './accessors/DataAccessor'; 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 const metadata = await this.accessor.getMetadata(identifier); - let result: Representation; - - // Create the representation of a container - 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; + return this.isExistingContainer(metadata) ? + new BasicRepresentation(metadata.quads(), metadata, INTERNAL_QUADS) : + new BasicRepresentation(await this.accessor.getData(identifier), metadata); } public async addResource(container: ResourceIdentifier, representation: Representation): Promise { @@ -374,22 +355,10 @@ export class DataAccessorBasedStore implements ResourceStore { if (error instanceof NotFoundHttpError) { // Make sure the parent exists first await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container)); - await this.writeData(container, this.getEmptyContainerRepresentation(container), true); + await this.writeData(container, new BasicRepresentation([], container), true); } else { 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), - }; - } } diff --git a/src/storage/conversion/QuadToRdfConverter.ts b/src/storage/conversion/QuadToRdfConverter.ts index b0108931e..a1ec3bd1e 100644 --- a/src/storage/conversion/QuadToRdfConverter.ts +++ b/src/storage/conversion/QuadToRdfConverter.ts @@ -1,11 +1,10 @@ import type { Readable } from 'stream'; import { StreamWriter } from 'n3'; import rdfSerializer from 'rdf-serialize'; +import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation'; import type { Representation } from '../../ldp/representation/Representation'; -import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import type { ValuePreferences } from '../../ldp/representation/RepresentationPreferences'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; -import { guardStream } from '../../util/GuardedStream'; import { pipeSafely } from '../../util/StreamUtil'; import { PREFERRED_PREFIX_TERM } from '../../util/Vocabularies'; import { matchingMediaTypes } from './ConversionUtil'; @@ -27,22 +26,18 @@ export class QuadToRdfConverter extends TypedRepresentationConverter { public async handle({ representation: quads, preferences }: RepresentationConverterArgs): Promise { const contentType = matchingMediaTypes(preferences.type, await this.getOutputTypes())[0]; - const metadata = new RepresentationMetadata(quads.metadata, contentType); let data: Readable; // Use prefixes if possible (see https://github.com/rubensworks/rdf-serialize.js/issues/1) 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 ])); data = pipeSafely(quads.data, new StreamWriter({ format: contentType, prefixes })); // Otherwise, write without prefixes } else { data = rdfSerializer.serialize(quads.data, { contentType }) as Readable; } - return { - binary: true, - data: guardStream(data), - metadata, - }; + + return new BasicRepresentation(data, quads.metadata, contentType); } } diff --git a/src/storage/conversion/RdfToQuadConverter.ts b/src/storage/conversion/RdfToQuadConverter.ts index fdc17a4da..ee6b2183c 100644 --- a/src/storage/conversion/RdfToQuadConverter.ts +++ b/src/storage/conversion/RdfToQuadConverter.ts @@ -1,7 +1,7 @@ import { PassThrough } from 'stream'; import rdfParser from 'rdf-parse'; +import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation'; import type { Representation } from '../../ldp/representation/Representation'; -import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { pipeSafely } from '../../util/StreamUtil'; @@ -17,19 +17,13 @@ export class RdfToQuadConverter extends TypedRepresentationConverter { } public async handle({ representation, identifier }: RepresentationConverterArgs): Promise { - const metadata = new RepresentationMetadata(representation.metadata, INTERNAL_QUADS); const rawQuads = rdfParser.parse(representation.data, { contentType: representation.metadata.contentType!, baseIRI: identifier.path, }); - const pass = new PassThrough({ objectMode: true }); const data = pipeSafely(rawQuads, pass, (error): Error => new BadRequestHttpError(error.message)); - return { - binary: false, - data, - metadata, - }; + return new BasicRepresentation(data, representation.metadata, INTERNAL_QUADS); } } diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts index 26383958d..beb563493 100644 --- a/src/storage/patch/SparqlUpdatePatchHandler.ts +++ b/src/storage/patch/SparqlUpdatePatchHandler.ts @@ -5,14 +5,12 @@ import type { BaseQuad } from 'rdf-js'; import { someTerms } from 'rdf-terms'; import { Algebra } from 'sparqlalgebrajs'; import type { SparqlUpdatePatch } from '../../ldp/http/SparqlUpdatePatch'; -import type { Representation } from '../../ldp/representation/Representation'; -import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; +import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import { getLoggerFor } from '../../logging/LogUtil'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; -import { guardStream } from '../../util/GuardedStream'; import type { ResourceLocker } from '../../util/locking/ResourceLocker'; import type { ResourceStore } from '../ResourceStore'; 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}.`); // Write the result - const metadata = new RepresentationMetadata(identifier, INTERNAL_QUADS); - const representation: Representation = { - binary: false, - data: guardStream(store.match() as Readable), - metadata, - }; - await this.source.setRepresentation(identifier, representation); + await this.source.setRepresentation(identifier, new BasicRepresentation(store.match() as Readable, INTERNAL_QUADS)); } } diff --git a/src/util/GuardedStream.ts b/src/util/GuardedStream.ts index 2fc95a78d..aacd48a43 100644 --- a/src/util/GuardedStream.ts +++ b/src/util/GuardedStream.ts @@ -25,7 +25,7 @@ export type Guarded = T & G * Determines whether the stream is guarded from emitting errors. */ export function isGuarded(stream: T): stream is Guarded { - return guardedErrors in stream; + return typeof (stream as any)[guardedErrors] === 'object'; } /** diff --git a/src/util/StreamUtil.ts b/src/util/StreamUtil.ts index dfcb0e9e7..555c27825 100644 --- a/src/util/StreamUtil.ts +++ b/src/util/StreamUtil.ts @@ -97,10 +97,10 @@ export function transformSafely( } /** - * Converts an iterable to a stream and applies an error guard so that it is {@link Guarded}. - * @param iterable - Data to stream. + * Converts a string or array to a stream and applies an error guard so that it is {@link Guarded}. + * @param contents - Data to stream. * @param options - Options to pass to the Readable constructor. See {@link Readable.from}. */ -export function guardedStreamFrom(iterable: Iterable, options?: ReadableOptions): Guarded { - return guardStream(Readable.from(iterable, options)); +export function guardedStreamFrom(contents: string | Iterable, options?: ReadableOptions): Guarded { + return guardStream(Readable.from(typeof contents === 'string' ? [ contents ] : contents, options)); } diff --git a/test/integration/LdpHandlerWithAuth.test.ts b/test/integration/LdpHandlerWithAuth.test.ts index a6211ec2b..0690e3ffc 100644 --- a/test/integration/LdpHandlerWithAuth.test.ts +++ b/test/integration/LdpHandlerWithAuth.test.ts @@ -1,6 +1,6 @@ import { createReadStream } from 'fs'; 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 { 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); // Write test resource - await store.setRepresentation({ path: `${BASE}/permanent.txt` }, { - binary: true, - data: guardStream(createReadStream(joinFilePath(__dirname, '../assets/permanent.txt'))), - metadata: new RepresentationMetadata('text/plain'), - }); + await store.setRepresentation({ path: `${BASE}/permanent.txt` }, + new BasicRepresentation(createReadStream(joinFilePath(__dirname, '../assets/permanent.txt')), 'text/plain')); }); afterAll(async(): Promise => { diff --git a/test/integration/LockingResourceStore.test.ts b/test/integration/LockingResourceStore.test.ts index e4268c37a..e87cb0265 100644 --- a/test/integration/LockingResourceStore.test.ts +++ b/test/integration/LockingResourceStore.test.ts @@ -1,7 +1,7 @@ import streamifyArray from 'streamify-array'; import { RootContainerInitializer } from '../../src/init/RootContainerInitializer'; +import { BasicRepresentation } from '../../src/ldp/representation/BasicRepresentation'; import type { Representation } from '../../src/ldp/representation/Representation'; -import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata'; import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor'; import { DataAccessorBasedStore } from '../../src/storage/DataAccessorBasedStore'; 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 { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThreadedResourceLocker'; import { WrappedExpiringResourceLocker } from '../../src/util/locking/WrappedExpiringResourceLocker'; -import { guardedStreamFrom } from '../../src/util/StreamUtil'; import { BASE } from './Config'; describe('A LockingResourceStore', (): void => { @@ -39,9 +38,7 @@ describe('A LockingResourceStore', (): void => { store = new LockingResourceStore(source, expiringLocker); // Make sure something is in the store before we read from it in our tests. - const metadata = new RepresentationMetadata(APPLICATION_OCTET_STREAM); - const data = guardedStreamFrom([ 1, 2, 3 ]); - await store.setRepresentation({ path }, { metadata, data, binary: true }); + await store.setRepresentation({ path }, new BasicRepresentation([ 1, 2, 3 ], APPLICATION_OCTET_STREAM)); }); it('destroys the stream when nothing is read after 1000ms.', async(): Promise => { diff --git a/test/integration/RepresentationConverter.test.ts b/test/integration/RepresentationConverter.test.ts index 1eec24fc7..ae2dfa8f7 100644 --- a/test/integration/RepresentationConverter.test.ts +++ b/test/integration/RepresentationConverter.test.ts @@ -1,9 +1,8 @@ -import type { Representation } from '../../src/ldp/representation/Representation'; -import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata'; +import { BasicRepresentation } from '../../src/ldp/representation/BasicRepresentation'; import { ChainedConverter } from '../../src/storage/conversion/ChainedConverter'; import { QuadToRdfConverter } from '../../src/storage/conversion/QuadToRdfConverter'; import { RdfToQuadConverter } from '../../src/storage/conversion/RdfToQuadConverter'; -import { guardedStreamFrom, readableToString } from '../../src/util/StreamUtil'; +import { readableToString } from '../../src/util/StreamUtil'; describe('A ChainedConverter', (): void => { const converters = [ @@ -13,14 +12,10 @@ describe('A ChainedConverter', (): void => { const converter = new ChainedConverter(converters); it('can convert from JSON-LD to turtle.', async(): Promise => { - const metadata = new RepresentationMetadata('application/ld+json'); - const representation: Representation = { - binary: true, - data: guardedStreamFrom( - [ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ], - ), - metadata, - }; + const representation = new BasicRepresentation( + '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}', + 'application/ld+json', + ); const result = await converter.handleSafe({ representation, @@ -33,12 +28,10 @@ describe('A ChainedConverter', (): void => { }); it('can convert from turtle to JSON-LD.', async(): Promise => { - const metadata = new RepresentationMetadata('text/turtle'); - const representation: Representation = { - binary: true, - data: guardedStreamFrom([ ' .' ]), - metadata, - }; + const representation = new BasicRepresentation( + ' .', + 'text/turtle', + ); const result = await converter.handleSafe({ representation, diff --git a/test/unit/ldp/representation/BasicRepresentation.test.ts b/test/unit/ldp/representation/BasicRepresentation.test.ts new file mode 100644 index 000000000..5f5f5454a --- /dev/null +++ b/test/unit/ldp/representation/BasicRepresentation.test.ts @@ -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 => { + 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 => { + 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); + }); +}); diff --git a/test/unit/pods/GeneratedPodManager.test.ts b/test/unit/pods/GeneratedPodManager.test.ts index f76969dcc..6c66addca 100644 --- a/test/unit/pods/GeneratedPodManager.test.ts +++ b/test/unit/pods/GeneratedPodManager.test.ts @@ -1,5 +1,4 @@ -import { Readable } from 'stream'; -import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata'; +import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation'; import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { Agent } from '../../../src/pods/agent/Agent'; 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 => { - (store.getRepresentation as jest.Mock).mockImplementationOnce((): any => ({ - data: Readable.from([]), - metadata: new RepresentationMetadata(), - binary: true, - })); + (store.getRepresentation as jest.Mock).mockImplementationOnce((): any => new BasicRepresentation([], {})); const result = manager.createPod(agent); await expect(result).rejects.toThrow(`There already is a resource at ${base}user/`); await expect(result).rejects.toThrow(ConflictHttpError); diff --git a/test/unit/pods/agent/AgentJsonParser.test.ts b/test/unit/pods/agent/AgentJsonParser.test.ts index 642ec2ec4..6c5ff3290 100644 --- a/test/unit/pods/agent/AgentJsonParser.test.ts +++ b/test/unit/pods/agent/AgentJsonParser.test.ts @@ -1,3 +1,4 @@ +import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation'; import type { Representation } from '../../../../src/ldp/representation/Representation'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { AgentJsonParser } from '../../../../src/pods/agent/AgentJsonParser'; @@ -11,13 +12,8 @@ describe('An AgentJsonParser', (): void => { const parser = new AgentJsonParser(); beforeEach(async(): Promise => { - metadata = new RepresentationMetadata(); - metadata.contentType = 'application/json'; - representation = { - binary: true, - data: guardedStreamFrom([]), - metadata, - }; + metadata = new RepresentationMetadata('application/json'); + representation = new BasicRepresentation([], metadata); }); it('only supports JSON data.', async(): Promise => { diff --git a/test/util/TestHelpers.ts b/test/util/TestHelpers.ts index 6a64599b4..606e448a4 100644 --- a/test/util/TestHelpers.ts +++ b/test/util/TestHelpers.ts @@ -6,7 +6,7 @@ import * as url from 'url'; import type { MockResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http'; 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'; /* eslint-disable jest/no-standalone-expect */ @@ -45,16 +45,7 @@ export class AclHelper { acl.push('.'); - const representation = { - binary: true, - data: guardedStreamFrom(acl), - metadata: new RepresentationMetadata('text/turtle'), - }; - - return this.store.setRepresentation( - { path: `${this.id}.acl` }, - representation, - ); + await this.store.setRepresentation({ path: `${this.id}.acl` }, new BasicRepresentation(acl, 'text/turtle')); } }