diff --git a/.eslintrc.js b/.eslintrc.js index 0fe626a441..71cc119cca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,15 +27,18 @@ module.exports = { '@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-unnecessary-condition': 'off', // problems with optional parameters '@typescript-eslint/space-before-function-paren': [ 'error', 'never' ], + '@typescript-eslint/unified-signatures': 'off', 'class-methods-use-this': 'off', // conflicts with functions from interfaces that sometimes don't require `this` 'comma-dangle': ['error', 'always-multiline'], 'dot-location': ['error', 'property'], 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], 'max-len': ['error', { code: 120, ignoreUrls: true }], + 'no-param-reassign': 'off', // necessary in constructor overloading 'no-underscore-dangle': 'off', // conflicts with external libraries 'padding-line-between-statements': 'off', - 'tsdoc/syntax': 'error', 'prefer-named-capture-group': 'off', + 'tsdoc/syntax': 'error', + 'unicorn/no-fn-reference-in-iterator': 'off', // this prevents some functional programming paradigms // Import 'sort-imports': 'off', // Disabled in favor of eslint-plugin-import diff --git a/src/init/Setup.ts b/src/init/Setup.ts index 8c6f45f184..f048295832 100644 --- a/src/init/Setup.ts +++ b/src/init/Setup.ts @@ -4,7 +4,7 @@ import { RepresentationMetadata } from '../ldp/representation/RepresentationMeta import { ExpressHttpServer } from '../server/ExpressHttpServer'; import { ResourceStore } from '../storage/ResourceStore'; import { TEXT_TURTLE } from '../util/ContentTypes'; -import { CONTENT_TYPE } from '../util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../util/MetadataTypes'; /** * Invokes all logic to setup a server. @@ -50,11 +50,10 @@ export class Setup { acl:mode acl:Control; acl:accessTo <${this.base}>; acl:default <${this.base}>.`; - const aclId = await this.aclManager.getAcl({ path: this.base }); - const metadata = new RepresentationMetadata(aclId.path); - metadata.set(CONTENT_TYPE, TEXT_TURTLE); + const baseAclId = await this.aclManager.getAcl({ path: this.base }); + const metadata = new RepresentationMetadata(baseAclId.path, { [MA_CONTENT_TYPE]: TEXT_TURTLE }); await this.store.setRepresentation( - aclId, + baseAclId, { binary: true, data: streamifyArray([ acl ]), diff --git a/src/ldp/http/BasicResponseWriter.ts b/src/ldp/http/BasicResponseWriter.ts index 7bd6946024..855cd62401 100644 --- a/src/ldp/http/BasicResponseWriter.ts +++ b/src/ldp/http/BasicResponseWriter.ts @@ -1,7 +1,6 @@ import { HttpResponse } from '../../server/HttpResponse'; import { HttpError } from '../../util/errors/HttpError'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE } from '../../util/MetadataTypes'; import { ResponseDescription } from '../operations/ResponseDescription'; import { ResponseWriter } from './ResponseWriter'; @@ -30,7 +29,7 @@ export class BasicResponseWriter extends ResponseWriter { } else { input.response.setHeader('location', input.result.identifier.path); if (input.result.body) { - const contentType = input.result.body.metadata.get(CONTENT_TYPE)?.value ?? 'text/plain'; + const contentType = input.result.body.metadata.contentType ?? 'text/plain'; input.response.setHeader('content-type', contentType); input.result.body.data.pipe(input.response); } diff --git a/src/ldp/http/RawBodyParser.ts b/src/ldp/http/RawBodyParser.ts index 6212aadc94..f1456708fb 100644 --- a/src/ldp/http/RawBodyParser.ts +++ b/src/ldp/http/RawBodyParser.ts @@ -1,6 +1,6 @@ import { HttpRequest } from '../../server/HttpRequest'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE, SLUG, TYPE } from '../../util/MetadataTypes'; +import { HTTP_SLUG, RDF_TYPE, MA_CONTENT_TYPE } from '../../util/MetadataTypes'; import { Representation } from '../representation/Representation'; import { RepresentationMetadata } from '../representation/RepresentationMetadata'; import { BodyParser } from './BodyParser'; @@ -40,8 +40,7 @@ export class RawBodyParser extends BodyParser { private parseMetadata(input: HttpRequest): RepresentationMetadata { const contentType = /^[^;]*/u.exec(input.headers['content-type']!)![0]; - const metadata: RepresentationMetadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, contentType); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: contentType }); const { link, slug } = input.headers; @@ -49,7 +48,7 @@ export class RawBodyParser extends BodyParser { if (Array.isArray(slug)) { throw new UnsupportedHttpError('At most 1 slug header is allowed.'); } - metadata.set(SLUG, slug); + metadata.set(HTTP_SLUG, slug); } // There are similarities here to Accept header parsing so that library should become more generic probably @@ -60,11 +59,12 @@ export class RawBodyParser extends BodyParser { const [ , rel ] = /^ *; *rel="(.*)"$/u.exec(rest) ?? []; return { url, rel }; }); - parsedLinks.forEach((entry): void => { + for (const entry of parsedLinks) { if (entry.rel === 'type') { - metadata.set(TYPE, entry.url); + metadata.set(RDF_TYPE, entry.url); + break; } - }); + } } return metadata; diff --git a/src/ldp/http/SparqlUpdateBodyParser.ts b/src/ldp/http/SparqlUpdateBodyParser.ts index b5629e130d..c10fe16229 100644 --- a/src/ldp/http/SparqlUpdateBodyParser.ts +++ b/src/ldp/http/SparqlUpdateBodyParser.ts @@ -3,7 +3,7 @@ import { translate } from 'sparqlalgebrajs'; import { HttpRequest } from '../../server/HttpRequest'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; -import { CONTENT_TYPE } from '../../util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../util/MetadataTypes'; import { readableToString } from '../../util/Util'; import { RepresentationMetadata } from '../representation/RepresentationMetadata'; import { BodyParser } from './BodyParser'; @@ -35,8 +35,7 @@ export class SparqlUpdateBodyParser extends BodyParser { const sparql = await readableToString(toAlgebraStream); const algebra = translate(sparql, { quads: true }); - const metadata = new RepresentationMetadata(); - metadata.add(CONTENT_TYPE, 'application/sparql-update'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'application/sparql-update' }); // Prevent body from being requested again return { diff --git a/src/ldp/representation/MetadataUtil.ts b/src/ldp/representation/MetadataUtil.ts new file mode 100644 index 0000000000..95a34c99f4 --- /dev/null +++ b/src/ldp/representation/MetadataUtil.ts @@ -0,0 +1,41 @@ +import { literal, namedNode } from '@rdfjs/data-model'; +import type { Literal, NamedNode, Term } from 'rdf-js'; +import { MA_CONTENT_TYPE } from '../../util/MetadataTypes'; + +// Shorthands for commonly used predicates +const shorthands: { [id: string]: NamedNode } = { + contentType: namedNode(MA_CONTENT_TYPE), +}; + +// Caches named node conversions +const termMap: { [id: string]: NamedNode } = {}; + +/** + * @param input - Checks if this is a {@link Term}. + */ +export const isTerm = (input?: any): input is Term => input?.termType; + +/** + * Converts the incoming predicate to a named node. + * In case of string, first checks if it is a shorthand, if not a new named node gets made. + * @param predicate - Predicate to potentially transform. + */ +export const getPredicateTerm = (predicate: NamedNode | string): NamedNode => { + if (typeof predicate === 'string') { + if (shorthands[predicate]) { + return shorthands[predicate]; + } + if (!termMap[predicate]) { + termMap[predicate] = namedNode(predicate); + } + return termMap[predicate]; + } + return predicate; +}; + +/** + * Converts an object to a literal when needed. + * @param object - Object to potentially transform. + */ +export const getObjectTerm = (object: NamedNode | Literal | string): NamedNode | Literal => + typeof object === 'string' ? literal(object) : object; diff --git a/src/ldp/representation/RepresentationMetadata.ts b/src/ldp/representation/RepresentationMetadata.ts index 9ad9d91fd0..74e5cbcce2 100644 --- a/src/ldp/representation/RepresentationMetadata.ts +++ b/src/ldp/representation/RepresentationMetadata.ts @@ -1,9 +1,13 @@ -import { quad as createQuad, literal, namedNode } from '@rdfjs/data-model'; +import { quad as createQuad, namedNode } from '@rdfjs/data-model'; import { Store } from 'n3'; import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js'; +import { getObjectTerm, getPredicateTerm, isTerm } from './MetadataUtil'; + +export type MetadataOverrideValue = NamedNode | Literal | string | (NamedNode | Literal | string)[]; /** * Stores the metadata triples and provides methods for easy access. + * Most functions return the metadata object to allow for chaining. */ export class RepresentationMetadata { private store: Store; @@ -13,21 +17,59 @@ export class RepresentationMetadata { * @param identifier - Identifier of the resource relevant to this metadata. * A blank node will be generated if none is provided. * Strings will be converted to named nodes. @ignored - * @param quads - Quads to fill the metadata with. @ignored + * @param overrides - Key/value map of extra values that need to be added to the metadata. @ignored * - * `@ignored` tags are necessary for Components-Generator.js + * `@ignored` tag is necessary for Components-Generator.js */ - public constructor(identifier?: NamedNode | BlankNode | string, quads?: Quad[]) { - this.store = new Store(quads); - if (identifier) { - if (typeof identifier === 'string') { - this.id = namedNode(identifier); - } else { - this.id = identifier; - } + public constructor(identifier?: NamedNode | BlankNode | string, overrides?: { [pred: string]: MetadataOverrideValue}); + + /** + * @param metadata - Starts as a copy of the input metadata. + * @param overrides - Key/value map of extra values that need to be added to the metadata. + * Will override values that were set by the input metadata. + */ + public constructor(metadata?: RepresentationMetadata, overrides?: { [pred: string]: MetadataOverrideValue}); + + /** + * @param overrides - Key/value map of extra values that need to be added to the metadata. + */ + public constructor(overrides?: { [pred: string]: MetadataOverrideValue}); + + public constructor( + input?: NamedNode | BlankNode | string | RepresentationMetadata | { [pred: string]: MetadataOverrideValue}, + overrides?: { [pred: string]: MetadataOverrideValue}, + ) { + this.store = new Store(); + if (typeof input === 'string') { + this.id = namedNode(input); + } else if (isTerm(input)) { + this.id = input; + } else if (input instanceof RepresentationMetadata) { + this.id = input.identifier; + this.addQuads(input.quads()); } else { + overrides = input; this.id = this.store.createBlankNode(); } + + if (overrides) { + this.setOverrides(overrides); + } + } + + private setOverrides(overrides: { [pred: string]: MetadataOverrideValue}): void { + for (const predicate of Object.keys(overrides)) { + const namedPredicate = getPredicateTerm(predicate); + this.removeAll(namedPredicate); + + let objects = overrides[predicate]; + if (!Array.isArray(objects)) { + objects = [ objects ]; + } + for (const object of objects.map(getObjectTerm)) { + this.store.addQuad(this.id, namedPredicate, object); + } + } } /** @@ -46,31 +88,47 @@ export class RepresentationMetadata { } public set identifier(id: NamedNode | BlankNode) { - const quads = this.quads().map((quad): Quad => { - if (quad.subject.equals(this.id)) { - return createQuad(id, quad.predicate, quad.object, quad.graph); - } - if (quad.object.equals(this.id)) { - return createQuad(quad.subject, quad.predicate, id, quad.graph); - } - return quad; - }); - this.store = new Store(quads); - this.id = id; + if (!id.equals(this.id)) { + // Convert all instances of the old identifier to the new identifier in the stored quads + const quads = this.quads().map((quad): Quad => { + if (quad.subject.equals(this.id)) { + return createQuad(id, quad.predicate, quad.object, quad.graph); + } + if (quad.object.equals(this.id)) { + return createQuad(quad.subject, quad.predicate, id, quad.graph); + } + return quad; + }); + this.store = new Store(quads); + this.id = id; + } + } + + /** + * Helper function to import all entries from the given metadata. + * If the new metadata has a different identifier the internal one will be updated. + * @param metadata - Metadata to import. + */ + public setMetadata(metadata: RepresentationMetadata): this { + this.identifier = metadata.identifier; + this.addQuads(metadata.quads()); + return this; } /** * @param quads - Quads to add to the metadata. */ - public addQuads(quads: Quad[]): void { + public addQuads(quads: Quad[]): this { this.store.addQuads(quads); + return this; } /** * @param quads - Quads to remove from the metadata. */ - public removeQuads(quads: Quad[]): void { + public removeQuads(quads: Quad[]): this { this.store.removeQuads(quads); + return this; } /** @@ -78,8 +136,9 @@ export class RepresentationMetadata { * @param predicate - Predicate linking identifier to value. * @param object - Value to add. */ - public add(predicate: NamedNode, object: NamedNode | Literal | string): void { - this.store.addQuad(this.id, predicate, typeof object === 'string' ? literal(object) : object); + public add(predicate: NamedNode | string, object: NamedNode | Literal | string): this { + this.store.addQuad(this.id, getPredicateTerm(predicate), getObjectTerm(object)); + return this; } /** @@ -87,16 +146,29 @@ export class RepresentationMetadata { * @param predicate - Predicate linking identifier to value. * @param object - Value to remove. */ - public remove(predicate: NamedNode, object: NamedNode | Literal | string): void { - this.store.removeQuad(this.id, predicate, typeof object === 'string' ? literal(object) : object); + public remove(predicate: NamedNode | string, object: NamedNode | Literal | string): this { + this.store.removeQuad(this.id, getPredicateTerm(predicate), getObjectTerm(object)); + return this; } /** * Removes all values linked through the given predicate. * @param predicate - Predicate to remove. */ - public removeAll(predicate: NamedNode): void { - this.removeQuads(this.store.getQuads(this.id, predicate, null, null)); + public removeAll(predicate: NamedNode | string): this { + this.removeQuads(this.store.getQuads(this.id, getPredicateTerm(predicate), null, null)); + return this; + } + + /** + * Finds all object values matching the given predicate. + * @param predicate - Predicate to get the values for. + * + * @returns An array with all matches. + */ + public getAll(predicate: NamedNode | string): Term[] { + return this.store.getQuads(this.id, getPredicateTerm(predicate), null, null) + .map((quad): Term => quad.object); } /** @@ -107,24 +179,41 @@ export class RepresentationMetadata { * * @returns The corresponding value. Undefined if there is no match */ - public get(predicate: NamedNode): Term | undefined { - const quads = this.store.getQuads(this.id, predicate, null, null); - if (quads.length === 0) { + public get(predicate: NamedNode | string): Term | undefined { + const terms = this.getAll(predicate); + if (terms.length === 0) { return; } - if (quads.length > 1) { - throw new Error(`Multiple results for ${predicate.value}`); + if (terms.length > 1) { + throw new Error(`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`); } - return quads[0].object; + return terms[0]; } /** * Sets the value for the given predicate, removing all other instances. + * In case the object is undefined this is identical to `removeAll(predicate)`. * @param predicate - Predicate linking to the value. * @param object - Value to set. */ - public set(predicate: NamedNode, object: NamedNode | Literal | string): void { + public set(predicate: NamedNode | string, object?: NamedNode | Literal | string): this { this.removeAll(predicate); - this.add(predicate, object); + if (object) { + this.add(predicate, object); + } + return this; + } + + // Syntactic sugar for common predicates + + /** + * Shorthand for the CONTENT_TYPE predicate. + */ + public get contentType(): string | undefined { + return this.get(getPredicateTerm('contentType'))?.value; + } + + public set contentType(input) { + this.set(getPredicateTerm('contentType'), input); } } diff --git a/src/storage/FileResourceStore.ts b/src/storage/FileResourceStore.ts index 11a16515fb..06749fb7f7 100644 --- a/src/storage/FileResourceStore.ts +++ b/src/storage/FileResourceStore.ts @@ -14,7 +14,15 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError'; import { InteractionController } from '../util/InteractionController'; import { MetadataController } from '../util/MetadataController'; -import { BYTE_SIZE, CONTENT_TYPE, LAST_CHANGED, SLUG, TYPE } from '../util/MetadataTypes'; +import { + HTTP_BYTE_SIZE, + HTTP_LAST_CHANGED, + HTTP_SLUG, + MA_CONTENT_TYPE, + RDF_TYPE, + XSD_DATE_TIME, + XSD_INTEGER, +} from '../util/MetadataTypes'; import { ensureTrailingSlash } from '../util/Util'; import { ExtensionBasedMapper } from './ExtensionBasedMapper'; import { ResourceStore } from './ResourceStore'; @@ -57,11 +65,11 @@ export class FileResourceStore implements ResourceStore { // Get the path from the request URI, all metadata triples if any, and the Slug and Link header values. const path = this.resourceMapper.getRelativePath(container); - const slug = representation.metadata.get(SLUG)?.value; - const type = representation.metadata.get(TYPE)?.value; + const slug = representation.metadata.get(HTTP_SLUG)?.value; + const types = representation.metadata.getAll(RDF_TYPE); // Create a new container or resource in the parent container with a specific name based on the incoming headers. - const isContainer = this.interactionController.isContainer(slug, type); + const isContainer = this.interactionController.isContainer(slug, types); const newIdentifier = this.interactionController.generateIdentifier(isContainer, slug); let metadata; // eslint-disable-next-line no-param-reassign @@ -154,14 +162,14 @@ export class FileResourceStore implements ResourceStore { // eslint-disable-next-line no-param-reassign representation.metadata.identifier = DataFactory.namedNode(identifier.path); const raw = representation.metadata.quads(); - const type = representation.metadata.get(TYPE)?.value; + const types = representation.metadata.getAll(RDF_TYPE); let metadata: Readable | undefined; if (raw.length > 0) { - metadata = streamifyArray(raw); + metadata = this.metadataController.serializeQuads(raw); } // Create a new container or resource in the parent container with a specific name based on the incoming headers. - const isContainer = this.interactionController.isContainer(documentName, type); + const isContainer = this.interactionController.isContainer(documentName, types); const newIdentifier = this.interactionController.generateIdentifier(isContainer, documentName); return isContainer ? await this.setDirectoryRepresentation(containerPath, newIdentifier, metadata) : @@ -222,10 +230,10 @@ export class FileResourceStore implements ResourceStore { } catch (_) { // Metadata file doesn't exist so lets keep `rawMetaData` an empty array. } - const metadata = new RepresentationMetadata(this.resourceMapper.mapFilePathToUrl(path), rawMetadata); - metadata.set(LAST_CHANGED, stats.mtime.toISOString()); - metadata.set(BYTE_SIZE, DataFactory.literal(stats.size)); - metadata.set(CONTENT_TYPE, contentType); + const metadata = new RepresentationMetadata(this.resourceMapper.mapFilePathToUrl(path)).addQuads(rawMetadata) + .set(HTTP_LAST_CHANGED, DataFactory.literal(stats.mtime.toISOString(), XSD_DATE_TIME)) + .set(HTTP_BYTE_SIZE, DataFactory.literal(stats.size, XSD_INTEGER)); + metadata.contentType = contentType; return { metadata, data: readStream, binary: true }; } @@ -256,9 +264,9 @@ export class FileResourceStore implements ResourceStore { // Metadata file doesn't exist so lets keep `rawMetaData` an empty array. } - const metadata = new RepresentationMetadata(containerURI, rawMetadata); - metadata.set(LAST_CHANGED, stats.mtime.toISOString()); - metadata.set(CONTENT_TYPE, INTERNAL_QUADS); + const metadata = new RepresentationMetadata(containerURI).addQuads(rawMetadata) + .set(HTTP_LAST_CHANGED, DataFactory.literal(stats.mtime.toISOString(), XSD_DATE_TIME)) + .set(MA_CONTENT_TYPE, INTERNAL_QUADS); return { binary: false, diff --git a/src/storage/InMemoryResourceStore.ts b/src/storage/InMemoryResourceStore.ts index 2b1c25ef96..599b143799 100644 --- a/src/storage/InMemoryResourceStore.ts +++ b/src/storage/InMemoryResourceStore.ts @@ -6,7 +6,7 @@ import { RepresentationMetadata } from '../ldp/representation/RepresentationMeta import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import { TEXT_TURTLE } from '../util/ContentTypes'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; -import { CONTENT_TYPE } from '../util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../util/MetadataTypes'; import { ensureTrailingSlash } from '../util/Util'; import { ResourceStore } from './ResourceStore'; @@ -26,8 +26,7 @@ export class InMemoryResourceStore implements ResourceStore { public constructor(base: string) { this.base = ensureTrailingSlash(base); - const metadata = new RepresentationMetadata(); - metadata.add(CONTENT_TYPE, TEXT_TURTLE); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: TEXT_TURTLE }); this.store = { // Default root entry (what you get when the identifier is equal to the base) '': { diff --git a/src/storage/RepresentationConvertingStore.ts b/src/storage/RepresentationConvertingStore.ts index 5e9eda3b5d..65ab2b583c 100644 --- a/src/storage/RepresentationConvertingStore.ts +++ b/src/storage/RepresentationConvertingStore.ts @@ -1,7 +1,6 @@ import { Representation } from '../ldp/representation/Representation'; import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences'; import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; -import { CONTENT_TYPE } from '../util/MetadataTypes'; import { matchingMediaType } from '../util/Util'; import { Conditions } from './Conditions'; import { RepresentationConverter } from './conversion/RepresentationConverter'; @@ -38,12 +37,12 @@ export class RepresentationConvertingStore type.weight > 0 && - matchingMediaType(type.value, contentType.value)), + matchingMediaType(type.value, contentType)), ); } } diff --git a/src/storage/conversion/ChainedConverter.ts b/src/storage/conversion/ChainedConverter.ts index 6cddbd4180..d0e0bd7d68 100644 --- a/src/storage/conversion/ChainedConverter.ts +++ b/src/storage/conversion/ChainedConverter.ts @@ -1,7 +1,7 @@ import { Representation } from '../../ldp/representation/Representation'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences'; -import { CONTENT_TYPE } from '../../util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../util/MetadataTypes'; import { matchingMediaType } from '../../util/Util'; import { RepresentationConverterArgs } from './RepresentationConverter'; import { TypedRepresentationConverter } from './TypedRepresentationConverter'; @@ -53,8 +53,7 @@ export class ChainedConverter extends TypedRepresentationConverter { const idx = this.converters.length - 1; const lastChain = await this.getMatchingType(this.converters[idx - 1], this.converters[idx]); const oldMeta = input.representation.metadata; - const metadata = new RepresentationMetadata(oldMeta.identifier, oldMeta.quads()); - metadata.set(CONTENT_TYPE, lastChain); + const metadata = new RepresentationMetadata(oldMeta, { [MA_CONTENT_TYPE]: lastChain }); const representation: Representation = { ...input.representation, metadata }; await this.last.canHandle({ ...input, representation }); } diff --git a/src/storage/conversion/ConversionUtil.ts b/src/storage/conversion/ConversionUtil.ts index 4e6bce51c0..0d2921f6bf 100644 --- a/src/storage/conversion/ConversionUtil.ts +++ b/src/storage/conversion/ConversionUtil.ts @@ -1,7 +1,6 @@ import { RepresentationPreference } from '../../ldp/representation/RepresentationPreference'; import { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE } from '../../util/MetadataTypes'; import { matchingMediaType } from '../../util/Util'; import { RepresentationConverterArgs } from './RepresentationConverter'; @@ -35,11 +34,11 @@ RepresentationPreference[] => { */ export const checkRequest = (request: RepresentationConverterArgs, supportedIn: string[], supportedOut: string[]): void => { - const inType = request.representation.metadata.get(CONTENT_TYPE); + const inType = request.representation.metadata.contentType; if (!inType) { throw new UnsupportedHttpError('Input type required for conversion.'); } - if (!supportedIn.some((type): boolean => matchingMediaType(inType.value, type))) { + if (!supportedIn.some((type): boolean => matchingMediaType(inType, type))) { throw new UnsupportedHttpError(`Can only convert from ${supportedIn} to ${supportedOut}.`); } if (matchingTypes(request.preferences, supportedOut).length <= 0) { diff --git a/src/storage/conversion/QuadToRdfConverter.ts b/src/storage/conversion/QuadToRdfConverter.ts index 769c186bf7..0d064a7931 100644 --- a/src/storage/conversion/QuadToRdfConverter.ts +++ b/src/storage/conversion/QuadToRdfConverter.ts @@ -4,7 +4,7 @@ import { Representation } from '../../ldp/representation/Representation'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; -import { CONTENT_TYPE } from '../../util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../util/MetadataTypes'; import { checkRequest, matchingTypes } from './ConversionUtil'; import { RepresentationConverterArgs } from './RepresentationConverter'; import { TypedRepresentationConverter } from './TypedRepresentationConverter'; @@ -31,8 +31,7 @@ export class QuadToRdfConverter extends TypedRepresentationConverter { private async quadsToRdf(quads: Representation, preferences: RepresentationPreferences): Promise { const contentType = matchingTypes(preferences, await rdfSerializer.getContentTypes())[0].value; - const metadata = new RepresentationMetadata(quads.metadata.identifier, quads.metadata.quads()); - metadata.set(CONTENT_TYPE, contentType); + const metadata = new RepresentationMetadata(quads.metadata, { [MA_CONTENT_TYPE]: contentType }); return { binary: true, data: rdfSerializer.serialize(quads.data, { contentType }) as Readable, diff --git a/src/storage/conversion/QuadToTurtleConverter.ts b/src/storage/conversion/QuadToTurtleConverter.ts index c8099ca8b9..718b4ece37 100644 --- a/src/storage/conversion/QuadToTurtleConverter.ts +++ b/src/storage/conversion/QuadToTurtleConverter.ts @@ -2,7 +2,7 @@ import { StreamWriter } from 'n3'; import { Representation } from '../../ldp/representation/Representation'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { INTERNAL_QUADS, TEXT_TURTLE } from '../../util/ContentTypes'; -import { CONTENT_TYPE } from '../../util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../util/MetadataTypes'; import { checkRequest } from './ConversionUtil'; import { RepresentationConverter, RepresentationConverterArgs } from './RepresentationConverter'; @@ -19,8 +19,7 @@ export class QuadToTurtleConverter extends RepresentationConverter { } private quadsToTurtle(quads: Representation): Representation { - const metadata = new RepresentationMetadata(quads.metadata.identifier, quads.metadata.quads()); - metadata.set(CONTENT_TYPE, TEXT_TURTLE); + const metadata = new RepresentationMetadata(quads.metadata, { [MA_CONTENT_TYPE]: TEXT_TURTLE }); return { binary: true, data: quads.data.pipe(new StreamWriter({ format: TEXT_TURTLE })), diff --git a/src/storage/conversion/RdfToQuadConverter.ts b/src/storage/conversion/RdfToQuadConverter.ts index 54d404a936..4a751605c8 100644 --- a/src/storage/conversion/RdfToQuadConverter.ts +++ b/src/storage/conversion/RdfToQuadConverter.ts @@ -3,7 +3,7 @@ import rdfParser from 'rdf-parse'; import { Representation } from '../../ldp/representation/Representation'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; -import { CONTENT_TYPE } from '../../util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../util/MetadataTypes'; import { pipeStreamsAndErrors } from '../../util/Util'; import { checkRequest } from './ConversionUtil'; import { RepresentationConverterArgs } from './RepresentationConverter'; @@ -30,10 +30,9 @@ export class RdfToQuadConverter extends TypedRepresentationConverter { } private rdfToQuads(representation: Representation, baseIRI: string): Representation { - const metadata = new RepresentationMetadata(representation.metadata.identifier, representation.metadata.quads()); - metadata.set(CONTENT_TYPE, INTERNAL_QUADS); + const metadata = new RepresentationMetadata(representation.metadata, { [MA_CONTENT_TYPE]: INTERNAL_QUADS }); const rawQuads = rdfParser.parse(representation.data, { - contentType: representation.metadata.get(CONTENT_TYPE)!.value, + contentType: representation.metadata.contentType!, baseIRI, }); diff --git a/src/storage/conversion/TurtleToQuadConverter.ts b/src/storage/conversion/TurtleToQuadConverter.ts index 533c17ce11..83c25baa44 100644 --- a/src/storage/conversion/TurtleToQuadConverter.ts +++ b/src/storage/conversion/TurtleToQuadConverter.ts @@ -4,7 +4,7 @@ import { Representation } from '../../ldp/representation/Representation'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { TEXT_TURTLE, INTERNAL_QUADS } from '../../util/ContentTypes'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE } from '../../util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../util/MetadataTypes'; import { checkRequest } from './ConversionUtil'; import { RepresentationConverter, RepresentationConverterArgs } from './RepresentationConverter'; @@ -21,8 +21,7 @@ export class TurtleToQuadConverter extends RepresentationConverter { } private turtleToQuads(turtle: Representation, baseIRI: string): Representation { - const metadata = new RepresentationMetadata(turtle.metadata.identifier, turtle.metadata.quads()); - metadata.set(CONTENT_TYPE, INTERNAL_QUADS); + const metadata = new RepresentationMetadata(turtle.metadata, { [MA_CONTENT_TYPE]: INTERNAL_QUADS }); // Catch parsing errors and emit correct error // Node 10 requires both writableObjectMode and readableObjectMode diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts index d4edf5b908..98cf9516c8 100644 --- a/src/storage/patch/SparqlUpdatePatchHandler.ts +++ b/src/storage/patch/SparqlUpdatePatchHandler.ts @@ -10,7 +10,7 @@ import { RepresentationMetadata } from '../../ldp/representation/RepresentationM import { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE } from '../../util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../util/MetadataTypes'; import { ResourceLocker } from '../ResourceLocker'; import { ResourceStore } from '../ResourceStore'; import { PatchHandler } from './PatchHandler'; @@ -67,8 +67,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler { }); store.removeQuads(deletes); store.addQuads(inserts); - const metadata = new RepresentationMetadata(input.identifier.path); - metadata.set(CONTENT_TYPE, INTERNAL_QUADS); + const metadata = new RepresentationMetadata(input.identifier.path, { [MA_CONTENT_TYPE]: INTERNAL_QUADS }); const representation: Representation = { binary: false, data: store.match() as Readable, diff --git a/src/util/InteractionController.ts b/src/util/InteractionController.ts index 042ba2e9f7..cd154ad8a6 100644 --- a/src/util/InteractionController.ts +++ b/src/util/InteractionController.ts @@ -1,3 +1,4 @@ +import type { Term } from 'rdf-js'; import { v4 as uuid } from 'uuid'; import { LINK_TYPE_LDP_BC, LINK_TYPE_LDPC } from './LinkTypes'; import { trimTrailingSlashes } from './Util'; @@ -5,20 +6,20 @@ import { trimTrailingSlashes } from './Util'; export class InteractionController { /** * Check whether a new container or a resource should be created based on the given parameters. - * @param slug - Incoming slug header. - * @param link - Incoming link header. + * @param slug - Incoming slug metadata. + * @param types - Incoming type metadata. */ - public isContainer(slug?: string, link?: string): boolean { - if (!slug || !slug.endsWith('/')) { - return Boolean(link === LINK_TYPE_LDPC) || Boolean(link === LINK_TYPE_LDP_BC); + public isContainer(slug?: string, types?: Term[]): boolean { + if (types && types.length > 0) { + return types.some((type): boolean => type.value === LINK_TYPE_LDPC || type.value === LINK_TYPE_LDP_BC); } - return !link || link === LINK_TYPE_LDPC || link === LINK_TYPE_LDP_BC; + return Boolean(slug?.endsWith('/')); } /** * Get the identifier path the new resource should have. * @param isContainer - Whether or not the resource is a container. - * @param slug - Incoming slug header. + * @param slug - Incoming slug metadata. */ public generateIdentifier(isContainer: boolean, slug?: string): string { if (!slug) { diff --git a/src/util/MetadataTypes.ts b/src/util/MetadataTypes.ts index a8f10ffc6d..2ff7fbc0f9 100644 --- a/src/util/MetadataTypes.ts +++ b/src/util/MetadataTypes.ts @@ -1,9 +1,11 @@ -import { namedNode } from '@rdfjs/data-model'; -import { NamedNode } from 'rdf-js'; +export const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; -export const TYPE: NamedNode = namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'); -export const CONTENT_TYPE: NamedNode = namedNode('http://www.w3.org/ns/ma-ont#format'); -export const SLUG: NamedNode = namedNode('http://example.com/slug'); -export const LAST_CHANGED: NamedNode = namedNode('http://example.com/lastChanged'); -export const BYTE_SIZE: NamedNode = namedNode('http://example.com/byteSize'); -export const ACL_RESOURCE: NamedNode = namedNode('http://example.com/acl'); +export const ACL_RESOURCE = 'urn:solid:acl:resource'; + +export const MA_CONTENT_TYPE = 'http://www.w3.org/ns/ma-ont#format'; +export const HTTP_SLUG = 'urn:solid:http:slug'; +export const HTTP_LAST_CHANGED = 'urn:solid:http:lastChanged'; +export const HTTP_BYTE_SIZE = 'urn:solid:http:byteSize'; + +export const XSD_DATE_TIME = 'http://www.w3.org/2001/XMLSchema#dateTime'; +export const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer'; diff --git a/test/integration/RepresentationConverter.test.ts b/test/integration/RepresentationConverter.test.ts index 5986160692..9b342ff6b5 100644 --- a/test/integration/RepresentationConverter.test.ts +++ b/test/integration/RepresentationConverter.test.ts @@ -4,7 +4,7 @@ import { RepresentationMetadata } from '../../src/ldp/representation/Representat import { ChainedConverter } from '../../src/storage/conversion/ChainedConverter'; import { QuadToRdfConverter } from '../../src/storage/conversion/QuadToRdfConverter'; import { RdfToQuadConverter } from '../../src/storage/conversion/RdfToQuadConverter'; -import { CONTENT_TYPE } from '../../src/util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../src/util/MetadataTypes'; import { readableToString } from '../../src/util/Util'; describe('A ChainedConverter', (): void => { @@ -15,8 +15,7 @@ describe('A ChainedConverter', (): void => { const converter = new ChainedConverter(converters); it('can convert from JSON-LD to turtle.', async(): Promise => { - const metadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, 'application/ld+json'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'application/ld+json' }); const representation: Representation = { binary: true, data: streamifyArray([ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ]), @@ -30,12 +29,11 @@ describe('A ChainedConverter', (): void => { }); await expect(readableToString(result.data)).resolves.toEqual(' .\n'); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); + expect(result.metadata.contentType).toEqual('text/turtle'); }); it('can convert from turtle to JSON-LD.', async(): Promise => { - const metadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, 'text/turtle'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }); const representation: Representation = { binary: true, data: streamifyArray([ ' .' ]), @@ -51,6 +49,6 @@ describe('A ChainedConverter', (): void => { expect(JSON.parse(await readableToString(result.data))).toEqual( [{ '@id': 'http://test.com/s', 'http://test.com/p': [{ '@id': 'http://test.com/o' }]}], ); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('application/ld+json'); + expect(result.metadata.contentType).toEqual('application/ld+json'); }); }); diff --git a/test/integration/RequestParser.test.ts b/test/integration/RequestParser.test.ts index 8482c3beac..4598698e02 100644 --- a/test/integration/RequestParser.test.ts +++ b/test/integration/RequestParser.test.ts @@ -7,7 +7,6 @@ import { BasicTargetExtractor } from '../../src/ldp/http/BasicTargetExtractor'; import { RawBodyParser } from '../../src/ldp/http/RawBodyParser'; import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata'; import { HttpRequest } from '../../src/server/HttpRequest'; -import { CONTENT_TYPE } from '../../src/util/MetadataTypes'; describe('A BasicRequestParser with simple input parsers', (): void => { const targetExtractor = new BasicTargetExtractor(); @@ -41,7 +40,7 @@ describe('A BasicRequestParser with simple input parsers', (): void => { metadata: expect.any(RepresentationMetadata), }, }); - expect(result.body?.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); + expect(result.body?.metadata.contentType).toEqual('text/turtle'); await expect(arrayifyStream(result.body!.data)).resolves.toEqual( [ ' .' ], diff --git a/test/unit/ldp/http/BasicResponseWriter.test.ts b/test/unit/ldp/http/BasicResponseWriter.test.ts index 46a08009f5..a584a4ae6a 100644 --- a/test/unit/ldp/http/BasicResponseWriter.test.ts +++ b/test/unit/ldp/http/BasicResponseWriter.test.ts @@ -5,7 +5,7 @@ import { BasicResponseWriter } from '../../../../src/ldp/http/BasicResponseWrite import { ResponseDescription } from '../../../../src/ldp/operations/ResponseDescription'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; describe('A BasicResponseWriter', (): void => { const writer = new BasicResponseWriter(); @@ -48,8 +48,7 @@ describe('A BasicResponseWriter', (): void => { }); it('responds with a content-type if the metadata has it.', async(done): Promise => { - const metadata = new RepresentationMetadata(); - metadata.add(CONTENT_TYPE, 'text/turtle'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }); const body = { binary: true, data: streamifyArray([ ' .' ]), diff --git a/test/unit/ldp/http/RawBodyParser.test.ts b/test/unit/ldp/http/RawBodyParser.test.ts index a9f619e2f4..3bad403ccb 100644 --- a/test/unit/ldp/http/RawBodyParser.test.ts +++ b/test/unit/ldp/http/RawBodyParser.test.ts @@ -5,7 +5,7 @@ import { RepresentationMetadata } from '../../../../src/ldp/representation/Repre import { HttpRequest } from '../../../../src/server/HttpRequest'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; import 'jest-rdf'; -import { CONTENT_TYPE, SLUG, TYPE } from '../../../../src/util/MetadataTypes'; +import { HTTP_SLUG, RDF_TYPE } from '../../../../src/util/MetadataTypes'; describe('A RawBodyparser', (): void => { const bodyParser = new RawBodyParser(); @@ -43,7 +43,7 @@ describe('A RawBodyparser', (): void => { data: input, metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); + expect(result.metadata.contentType).toEqual('text/turtle'); await expect(arrayifyStream(result.data)).resolves.toEqual( [ ' .' ], ); @@ -53,8 +53,8 @@ describe('A RawBodyparser', (): void => { const input = {} as HttpRequest; input.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle', slug: 'slugText' }; const result = (await bodyParser.handle(input))!; - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); - expect(result.metadata.get(SLUG)?.value).toEqual('slugText'); + expect(result.metadata.contentType).toEqual('text/turtle'); + expect(result.metadata.get(HTTP_SLUG)?.value).toEqual('slugText'); }); it('errors if there are multiple slugs.', async(): Promise => { @@ -71,8 +71,8 @@ describe('A RawBodyparser', (): void => { 'content-type': 'text/turtle', link: '; rel="type"' }; const result = (await bodyParser.handle(input))!; - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); - expect(result.metadata.get(TYPE)?.value).toEqual('http://www.w3.org/ns/ldp#Container'); + expect(result.metadata.contentType).toEqual('text/turtle'); + expect(result.metadata.get(RDF_TYPE)?.value).toEqual('http://www.w3.org/ns/ldp#Container'); }); it('ignores unknown link headers.', async(): Promise => { @@ -82,6 +82,6 @@ describe('A RawBodyparser', (): void => { link: [ '', 'badLink' ]}; const result = (await bodyParser.handle(input))!; expect(result.metadata.quads()).toHaveLength(1); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); + expect(result.metadata.contentType).toEqual('text/turtle'); }); }); diff --git a/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts b/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts index 495f47a9e8..d325011ae6 100644 --- a/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts +++ b/test/unit/ldp/http/SparqlUpdateBodyParser.test.ts @@ -6,7 +6,6 @@ import { SparqlUpdateBodyParser } from '../../../../src/ldp/http/SparqlUpdateBod import { HttpRequest } from '../../../../src/server/HttpRequest'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError'; -import { CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; describe('A SparqlUpdateBodyParser', (): void => { const bodyParser = new SparqlUpdateBodyParser(); @@ -35,7 +34,7 @@ describe('A SparqlUpdateBodyParser', (): void => { namedNode('http://test.com/o'), ) ]); expect(result.binary).toBe(true); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('application/sparql-update'); + expect(result.metadata.contentType).toEqual('application/sparql-update'); // Workaround for Node 10 not exposing objectMode expect((await arrayifyStream(result.data)).join('')).toEqual( diff --git a/test/unit/ldp/representation/RepresentationMetadata.test.ts b/test/unit/ldp/representation/RepresentationMetadata.test.ts index b16c5abca7..fb14220070 100644 --- a/test/unit/ldp/representation/RepresentationMetadata.test.ts +++ b/test/unit/ldp/representation/RepresentationMetadata.test.ts @@ -1,6 +1,7 @@ import { literal, namedNode, quad } from '@rdfjs/data-model'; -import { Literal, NamedNode, Quad } from 'rdf-js'; +import type { Literal, NamedNode, Quad } from 'rdf-js'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; +import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; describe('A RepresentationMetadata', (): void => { let metadata: RepresentationMetadata; @@ -30,15 +31,47 @@ describe('A RepresentationMetadata', (): void => { expect(metadata.identifier).toEqualRdfTerm(namedNode('identifier')); }); - it('stores input quads.', async(): Promise => { - metadata = new RepresentationMetadata(identifier, inputQuads); - expect(metadata.quads()).toBeRdfIsomorphic(inputQuads); + it('copies an other metadata object.', async(): Promise => { + const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' }); + metadata = new RepresentationMetadata(other); + expect(metadata.identifier).toEqualRdfTerm(namedNode('otherId')); + expect(metadata.quads()).toBeRdfIsomorphic([ + quad(namedNode('otherId'), namedNode('test:pred'), literal('objVal')) ]); + }); + + it('takes overrides for specific predicates.', async(): Promise => { + metadata = new RepresentationMetadata({ predVal: 'objVal' }); + expect(metadata.get('predVal')).toEqualRdfTerm(literal('objVal')); + + metadata = new RepresentationMetadata({ predVal: literal('objVal') }); + expect(metadata.get('predVal')).toEqualRdfTerm(literal('objVal')); + + metadata = new RepresentationMetadata({ predVal: [ 'objVal1', literal('objVal2') ], predVal2: 'objVal3' }); + expect(metadata.getAll('predVal')).toEqualRdfTermArray([ literal('objVal1'), literal('objVal2') ]); + expect(metadata.get('predVal2')).toEqualRdfTerm(literal('objVal3')); + }); + + it('can combine overrides with an identifier.', async(): Promise => { + metadata = new RepresentationMetadata(identifier, { predVal: 'objVal' }); + expect(metadata.quads()).toBeRdfIsomorphic([ + quad(identifier, namedNode('predVal'), literal('objVal')) ]); + }); + + it('can combine overrides with other metadata.', async(): Promise => { + const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' }); + metadata = new RepresentationMetadata(other, { 'test:pred': 'objVal2' }); + expect(metadata.quads()).toBeRdfIsomorphic([ + quad(namedNode('otherId'), namedNode('test:pred'), literal('objVal2')) ]); }); }); describe('instantiated', (): void => { beforeEach(async(): Promise => { - metadata = new RepresentationMetadata(identifier, inputQuads); + metadata = new RepresentationMetadata(identifier).addQuads(inputQuads); + }); + + it('can get all quads.', async(): Promise => { + expect(metadata.quads()).toBeRdfIsomorphic(inputQuads); }); it('can change the stored identifier.', async(): Promise => { @@ -57,6 +90,28 @@ describe('A RepresentationMetadata', (): void => { expect(metadata.quads()).toBeRdfIsomorphic(newQuads); }); + it('can copy metadata.', async(): Promise => { + const other = new RepresentationMetadata(identifier, { 'test:pred': 'objVal' }); + metadata.setMetadata(other); + + expect(metadata.identifier).toEqual(other.identifier); + expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.concat([ + quad(identifier, namedNode('test:pred'), literal('objVal')) ])); + }); + + it('updates its identifier when copying metadata.', async(): Promise => { + const other = new RepresentationMetadata('otherId', { 'test:pred': 'objVal' }); + metadata.setMetadata(other); + + // `setMetadata` should have the same result as the following + const expectedMetadata = new RepresentationMetadata(identifier).addQuads(inputQuads); + expectedMetadata.identifier = namedNode('otherId'); + expectedMetadata.add('test:pred', 'objVal'); + + expect(metadata.identifier).toEqual(other.identifier); + expect(metadata.quads()).toBeRdfIsomorphic(expectedMetadata.quads()); + }); + it('can add quads.', async(): Promise => { const newQuads: Quad[] = [ quad(namedNode('random'), namedNode('new'), namedNode('triple')), @@ -100,6 +155,10 @@ describe('A RepresentationMetadata', (): void => { expect(metadata.quads()).toBeRdfIsomorphic(updatedNodes); }); + it('can get all values for a predicate.', async(): Promise => { + expect(metadata.getAll(namedNode('has'))).toEqualRdfTermArray([ literal('data'), literal('moreData') ]); + }); + it('can get the single value for a predicate.', async(): Promise => { expect(metadata.get(namedNode('hasOne'))).toEqualRdfTerm(literal('otherData')); }); @@ -110,11 +169,27 @@ describe('A RepresentationMetadata', (): void => { it('errors if there are multiple values when getting a value.', async(): Promise => { expect((): any => metadata.get(namedNode('has'))).toThrow(Error); + expect((): any => metadata.get('has')).toThrow(Error); }); - it('can set the value of predicate.', async(): Promise => { + it('can set the value of a predicate.', async(): Promise => { metadata.set(namedNode('has'), literal('singleValue')); expect(metadata.get(namedNode('has'))).toEqualRdfTerm(literal('singleValue')); }); + + it('has a shorthand for content-type.', async(): Promise => { + expect(metadata.contentType).toBeUndefined(); + metadata.contentType = 'a/b'; + expect(metadata.get(MA_CONTENT_TYPE)).toEqualRdfTerm(literal('a/b')); + expect(metadata.contentType).toEqual('a/b'); + metadata.contentType = undefined; + expect(metadata.contentType).toBeUndefined(); + }); + + it('errors if a shorthand has multiple values.', async(): Promise => { + metadata.add(MA_CONTENT_TYPE, 'a/b'); + metadata.add(MA_CONTENT_TYPE, 'c/d'); + expect((): any => metadata.contentType).toThrow(); + }); }); }); diff --git a/test/unit/storage/FileResourceStore.test.ts b/test/unit/storage/FileResourceStore.test.ts index ea35928c33..972a5e8f7f 100644 --- a/test/unit/storage/FileResourceStore.test.ts +++ b/test/unit/storage/FileResourceStore.test.ts @@ -17,7 +17,7 @@ import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/Unsuppor import { InteractionController } from '../../../src/util/InteractionController'; import { LINK_TYPE_LDP_BC, LINK_TYPE_LDPR } from '../../../src/util/LinkTypes'; import { MetadataController } from '../../../src/util/MetadataController'; -import { BYTE_SIZE, CONTENT_TYPE, LAST_CHANGED, SLUG, TYPE } from '../../../src/util/MetadataTypes'; +import { HTTP_BYTE_SIZE, HTTP_LAST_CHANGED, HTTP_SLUG, RDF_TYPE } from '../../../src/util/MetadataTypes'; import { LDP, RDF, STAT, TERMS, XML } from '../../../src/util/Prefixes'; const { join: joinPath } = posix; @@ -133,8 +133,8 @@ describe('A FileResourceStore', (): void => { (fs.createReadStream as jest.Mock).mockImplementationOnce((): any => new Error('Metadata file does not exist.')); // Write container (POST) - representation.metadata.add(TYPE, LINK_TYPE_LDP_BC); - representation.metadata.add(SLUG, 'myContainer/'); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC); + representation.metadata.add(HTTP_SLUG, 'myContainer/'); const identifier = await store.addResource({ path: base }, representation); expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'myContainer/'), { recursive: true }); expect(identifier.path).toBe(`${base}myContainer/`); @@ -146,8 +146,8 @@ describe('A FileResourceStore', (): void => { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual(INTERNAL_QUADS); + expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); + expect(result.metadata.contentType).toEqual(INTERNAL_QUADS); await expect(arrayifyStream(result.data)).resolves.toBeDefined(); }); @@ -156,8 +156,8 @@ describe('A FileResourceStore', (): void => { (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats); // Tests - representation.metadata.add(TYPE, LINK_TYPE_LDP_BC); - representation.metadata.add(SLUG, 'myContainer/'); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC); + representation.metadata.add(HTTP_SLUG, 'myContainer/'); await expect(store.addResource({ path: `${base}foo` }, representation)).rejects.toThrow(MethodNotAllowedHttpError); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo')); }); @@ -173,19 +173,19 @@ describe('A FileResourceStore', (): void => { (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats); // Tests - representation.metadata.add(TYPE, LINK_TYPE_LDP_BC); - representation.metadata.add(SLUG, 'myContainer/'); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC); + representation.metadata.add(HTTP_SLUG, 'myContainer/'); await expect(store.addResource({ path: `${base}doesnotexist` }, representation)) .rejects.toThrow(MethodNotAllowedHttpError); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexist')); - representation.metadata.set(TYPE, LINK_TYPE_LDPR); - representation.metadata.set(SLUG, 'file.txt'); + representation.metadata.set(RDF_TYPE, LINK_TYPE_LDPR); + representation.metadata.set(HTTP_SLUG, 'file.txt'); await expect(store.addResource({ path: `${base}doesnotexist` }, representation)) .rejects.toThrow(MethodNotAllowedHttpError); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexist')); - representation.metadata.removeAll(TYPE); + representation.metadata.removeAll(RDF_TYPE); await expect(store.addResource({ path: `${base}existingresource` }, representation)) .rejects.toThrow(MethodNotAllowedHttpError); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'existingresource')); @@ -217,9 +217,9 @@ describe('A FileResourceStore', (): void => { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); - expect(result.metadata.get(BYTE_SIZE)?.value).toEqual(`${stats.size}`); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/plain'); + expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); + expect(result.metadata.get(HTTP_BYTE_SIZE)?.value).toEqual(`${stats.size}`); + expect(result.metadata.contentType).toEqual('text/plain'); await expect(arrayifyStream(result.data)).resolves.toEqual([ rawData ]); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt')); expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt')); @@ -254,8 +254,8 @@ describe('A FileResourceStore', (): void => { (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats); // Tests - representation.metadata.add(TYPE, LINK_TYPE_LDPR); - representation.metadata.add(SLUG, 'file.txt'); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDPR); + representation.metadata.add(HTTP_SLUG, 'file.txt'); const identifier = await store.addResource({ path: `${base}doesnotexistyet/` }, representation); expect(identifier.path).toBe(`${base}doesnotexistyet/file.txt`); expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexistyet/'), @@ -280,7 +280,7 @@ describe('A FileResourceStore', (): void => { (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats); // Tests - representation.metadata.add(TYPE, LINK_TYPE_LDPR); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDPR); representation.data = readableMock; await store.addResource({ path: `${base}foo/` }, representation); expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'), { recursive: true }); @@ -368,8 +368,8 @@ describe('A FileResourceStore', (): void => { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual(INTERNAL_QUADS); + expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); + expect(result.metadata.contentType).toEqual(INTERNAL_QUADS); await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray(quads); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); @@ -387,7 +387,7 @@ describe('A FileResourceStore', (): void => { (fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats); // Tests - representation.metadata.add(TYPE, LINK_TYPE_LDPR); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDPR); await store.setRepresentation({ path: `${base}alreadyexists.txt` }, representation); expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(2); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists.txt')); @@ -403,7 +403,7 @@ describe('A FileResourceStore', (): void => { await expect(store.setRepresentation({ path: `${base}alreadyexists` }, representation)).rejects .toThrow(ConflictHttpError); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists')); - representation.metadata.add(TYPE, LINK_TYPE_LDP_BC); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC); await expect(store.setRepresentation({ path: `${base}alreadyexists/` }, representation)).rejects .toThrow(ConflictHttpError); expect(fsPromises.access as jest.Mock).toBeCalledTimes(1); @@ -417,7 +417,7 @@ describe('A FileResourceStore', (): void => { (fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true); // Tests - representation.metadata.add(TYPE, LINK_TYPE_LDP_BC); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC); await store.setRepresentation({ path: `${base}foo/` }, representation); expect(fsPromises.mkdir as jest.Mock).toBeCalledTimes(1); expect(fsPromises.access as jest.Mock).toBeCalledTimes(1); @@ -435,8 +435,8 @@ describe('A FileResourceStore', (): void => { (fsPromises.unlink as jest.Mock).mockReturnValueOnce(true); // Tests - representation.metadata.add(TYPE, LINK_TYPE_LDPR); - representation.metadata.add(SLUG, 'file.txt'); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDPR); + representation.metadata.add(HTTP_SLUG, 'file.txt'); await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error); expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata')); expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt')); @@ -452,8 +452,8 @@ describe('A FileResourceStore', (): void => { (fsPromises.rmdir as jest.Mock).mockReturnValueOnce(true); // Tests - representation.metadata.add(TYPE, LINK_TYPE_LDP_BC); - representation.metadata.add(SLUG, 'foo/'); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC); + representation.metadata.add(HTTP_SLUG, 'foo/'); await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error); expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/')); }); @@ -464,7 +464,7 @@ describe('A FileResourceStore', (): void => { (fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true); // Tests - representation.metadata.add(SLUG, 'myContainer/'); + representation.metadata.add(HTTP_SLUG, 'myContainer/'); const identifier = await store.addResource({ path: base }, representation); expect(identifier.path).toBe(`${base}myContainer/`); expect(fsPromises.mkdir as jest.Mock).toBeCalledTimes(1); @@ -484,9 +484,9 @@ describe('A FileResourceStore', (): void => { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('application/octet-stream'); - expect(result.metadata.get(LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); - expect(result.metadata.get(BYTE_SIZE)?.value).toEqual(`${stats.size}`); + expect(result.metadata.contentType).toEqual('application/octet-stream'); + expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); + expect(result.metadata.get(HTTP_BYTE_SIZE)?.value).toEqual(`${stats.size}`); }); it('errors when performing a PUT on the root path.', async(): Promise => { @@ -519,8 +519,8 @@ describe('A FileResourceStore', (): void => { (fsPromises.mkdir as jest.Mock).mockReturnValue(true); // Tests - representation.metadata.add(TYPE, LINK_TYPE_LDP_BC); - representation.metadata.add(SLUG, 'bar'); + representation.metadata.add(RDF_TYPE, LINK_TYPE_LDP_BC); + representation.metadata.add(HTTP_SLUG, 'bar'); const identifier = await store.addResource({ path: `${base}foo` }, representation); expect(identifier.path).toBe(`${base}foo/bar/`); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo')); @@ -552,8 +552,8 @@ describe('A FileResourceStore', (): void => { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); - expect(result.metadata.get(BYTE_SIZE)?.value).toEqual(`${stats.size}`); + expect(result.metadata.get(HTTP_LAST_CHANGED)?.value).toEqual(stats.mtime.toISOString()); + expect(result.metadata.get(HTTP_BYTE_SIZE)?.value).toEqual(`${stats.size}`); await expect(arrayifyStream(result.data)).resolves.toEqual([ rawData ]); expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, name)); expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, name)); diff --git a/test/unit/storage/RepresentationConvertingStore.test.ts b/test/unit/storage/RepresentationConvertingStore.test.ts index b2f39acef1..3219427914 100644 --- a/test/unit/storage/RepresentationConvertingStore.test.ts +++ b/test/unit/storage/RepresentationConvertingStore.test.ts @@ -2,15 +2,14 @@ import { RepresentationMetadata } from '../../../src/ldp/representation/Represen import { RepresentationConverter } from '../../../src/storage/conversion/RepresentationConverter'; import { RepresentationConvertingStore } from '../../../src/storage/RepresentationConvertingStore'; import { ResourceStore } from '../../../src/storage/ResourceStore'; -import { CONTENT_TYPE } from '../../../src/util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../../src/util/MetadataTypes'; describe('A RepresentationConvertingStore', (): void => { let store: RepresentationConvertingStore; let source: ResourceStore; let handleSafeFn: jest.Mock, []>; let converter: RepresentationConverter; - const metadata = new RepresentationMetadata(); - metadata.add(CONTENT_TYPE, 'text/turtle'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }); beforeEach(async(): Promise => { source = { @@ -31,7 +30,7 @@ describe('A RepresentationConvertingStore', (): void => { data: 'data', metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); + expect(result.metadata.contentType).toEqual('text/turtle'); expect(source.getRepresentation).toHaveBeenCalledTimes(1); expect(source.getRepresentation).toHaveBeenLastCalledWith( { path: 'path' }, { type: [{ value: 'text/*', weight: 0 }, { value: 'text/turtle', weight: 1 }]}, undefined, @@ -45,7 +44,7 @@ describe('A RepresentationConvertingStore', (): void => { data: 'data', metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); + expect(result.metadata.contentType).toEqual('text/turtle'); expect(source.getRepresentation).toHaveBeenCalledTimes(1); expect(source.getRepresentation).toHaveBeenLastCalledWith( { path: 'path' }, {}, undefined, diff --git a/test/unit/storage/conversion/ChainedConverter.test.ts b/test/unit/storage/conversion/ChainedConverter.test.ts index 74d461d820..5f8d2956bb 100644 --- a/test/unit/storage/conversion/ChainedConverter.test.ts +++ b/test/unit/storage/conversion/ChainedConverter.test.ts @@ -5,7 +5,7 @@ import { ChainedConverter } from '../../../../src/storage/conversion/ChainedConv import { checkRequest } from '../../../../src/storage/conversion/ConversionUtil'; import { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter'; import { TypedRepresentationConverter } from '../../../../src/storage/conversion/TypedRepresentationConverter'; -import { CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; class DummyConverter extends TypedRepresentationConverter { private readonly inTypes: { [contentType: string]: number }; @@ -30,9 +30,8 @@ class DummyConverter extends TypedRepresentationConverter { } public async handle(input: RepresentationConverterArgs): Promise { - const oldMeta = input.representation.metadata; - const metadata = new RepresentationMetadata(oldMeta.identifier, oldMeta.quads()); - metadata.set(CONTENT_TYPE, input.preferences.type![0].value); + const metadata = new RepresentationMetadata(input.representation.metadata, + { [MA_CONTENT_TYPE]: input.preferences.type![0].value }); return { ...input.representation, metadata }; } } @@ -52,8 +51,7 @@ describe('A ChainedConverter', (): void => { ]; converter = new ChainedConverter(converters); - const metadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, 'text/turtle'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }); representation = { metadata } as Representation; preferences = { type: [{ value: 'internal/quads', weight: 1 }]}; args = { representation, preferences, identifier: { path: 'path' }}; @@ -79,7 +77,7 @@ describe('A ChainedConverter', (): void => { }); it('errors if the start of the chain does not support the representation type.', async(): Promise => { - representation.metadata.set(CONTENT_TYPE, 'bad/type'); + representation.metadata.contentType = 'bad/type'; await expect(converter.canHandle(args)).rejects.toThrow(); }); @@ -94,7 +92,7 @@ describe('A ChainedConverter', (): void => { jest.spyOn(converters[2], 'handle'); const result = await converter.handle(args); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('internal/quads'); + expect(result.metadata.contentType).toEqual('internal/quads'); expect((converters[0] as any).handle).toHaveBeenCalledTimes(1); expect((converters[1] as any).handle).toHaveBeenCalledTimes(1); expect((converters[2] as any).handle).toHaveBeenCalledTimes(1); diff --git a/test/unit/storage/conversion/ConversionUtil.test.ts b/test/unit/storage/conversion/ConversionUtil.test.ts index 76aec3b31b..c46ba2af37 100644 --- a/test/unit/storage/conversion/ConversionUtil.test.ts +++ b/test/unit/storage/conversion/ConversionUtil.test.ts @@ -3,7 +3,6 @@ import { RepresentationMetadata } from '../../../../src/ldp/representation/Repre import { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences'; import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; import { checkRequest, matchingTypes } from '../../../../src/storage/conversion/ConversionUtil'; -import { CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; describe('A ConversionUtil', (): void => { const identifier: ResourceIdentifier = { path: 'path' }; @@ -23,21 +22,21 @@ describe('A ConversionUtil', (): void => { }); it('requires a matching input type.', async(): Promise => { - metadata.add(CONTENT_TYPE, 'a/x'); + metadata.contentType = 'a/x'; const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]}; expect((): any => checkRequest({ identifier, representation, preferences }, [ 'c/x' ], [ '*/*' ])) .toThrow('Can only convert from c/x to */*.'); }); it('requires a matching output type.', async(): Promise => { - metadata.add(CONTENT_TYPE, 'a/x'); + metadata.contentType = 'a/x'; const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]}; expect((): any => checkRequest({ identifier, representation, preferences }, [ '*/*' ], [ 'c/x' ])) .toThrow('Can only convert from */* to c/x.'); }); it('succeeds with a valid input and output type.', async(): Promise => { - metadata.add(CONTENT_TYPE, 'a/x'); + metadata.contentType = 'a/x'; const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]}; expect(checkRequest({ identifier, representation, preferences }, [ '*/*' ], [ '*/*' ])) .toBeUndefined(); diff --git a/test/unit/storage/conversion/QuadToRdfConverter.test.ts b/test/unit/storage/conversion/QuadToRdfConverter.test.ts index 06c1d50763..d9d0c3f5e5 100644 --- a/test/unit/storage/conversion/QuadToRdfConverter.test.ts +++ b/test/unit/storage/conversion/QuadToRdfConverter.test.ts @@ -8,13 +8,12 @@ import { RepresentationPreferences } from '../../../../src/ldp/representation/Re import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; import { QuadToRdfConverter } from '../../../../src/storage/conversion/QuadToRdfConverter'; import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes'; -import { CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; describe('A QuadToRdfConverter', (): void => { const converter = new QuadToRdfConverter(); const identifier: ResourceIdentifier = { path: 'path' }; - const metadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, INTERNAL_QUADS); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: INTERNAL_QUADS }); it('supports parsing quads.', async(): Promise => { await expect(converter.getInputTypes()).resolves.toEqual({ [INTERNAL_QUADS]: 1 }); @@ -51,7 +50,7 @@ describe('A QuadToRdfConverter', (): void => { binary: true, metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); + expect(result.metadata.contentType).toEqual('text/turtle'); await expect(stringifyStream(result.data)).resolves.toEqual( ` . `, @@ -59,7 +58,7 @@ describe('A QuadToRdfConverter', (): void => { }); it('converts quads to JSON-LD.', async(): Promise => { - metadata.set(CONTENT_TYPE, INTERNAL_QUADS); + metadata.contentType = INTERNAL_QUADS; const representation = { data: streamifyArray([ triple( namedNode('http://test.com/s'), @@ -74,7 +73,7 @@ describe('A QuadToRdfConverter', (): void => { binary: true, metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('application/ld+json'); + expect(result.metadata.contentType).toEqual('application/ld+json'); await expect(stringifyStream(result.data)).resolves.toEqual( `[ { diff --git a/test/unit/storage/conversion/QuadToTurtleConverter.test.ts b/test/unit/storage/conversion/QuadToTurtleConverter.test.ts index 3f35a535d2..fe70504b84 100644 --- a/test/unit/storage/conversion/QuadToTurtleConverter.test.ts +++ b/test/unit/storage/conversion/QuadToTurtleConverter.test.ts @@ -7,13 +7,12 @@ import { RepresentationPreferences } from '../../../../src/ldp/representation/Re import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; import { QuadToTurtleConverter } from '../../../../src/storage/conversion/QuadToTurtleConverter'; import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes'; -import { CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; describe('A QuadToTurtleConverter', (): void => { const converter = new QuadToTurtleConverter(); const identifier: ResourceIdentifier = { path: 'path' }; - const metadata = new RepresentationMetadata(); - metadata.add(CONTENT_TYPE, INTERNAL_QUADS); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: INTERNAL_QUADS }); it('can handle quad to turtle conversions.', async(): Promise => { const representation = { metadata } as Representation; @@ -36,7 +35,7 @@ describe('A QuadToTurtleConverter', (): void => { binary: true, metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual('text/turtle'); + expect(result.metadata.contentType).toEqual('text/turtle'); await expect(arrayifyStream(result.data)).resolves.toContain( ' ', ); diff --git a/test/unit/storage/conversion/RdfToQuadConverter.test.ts b/test/unit/storage/conversion/RdfToQuadConverter.test.ts index a528178d38..c842b0f076 100644 --- a/test/unit/storage/conversion/RdfToQuadConverter.test.ts +++ b/test/unit/storage/conversion/RdfToQuadConverter.test.ts @@ -10,7 +10,7 @@ import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceI import { RdfToQuadConverter } from '../../../../src/storage/conversion/RdfToQuadConverter'; import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; describe('A RdfToQuadConverter.test.ts', (): void => { const converter = new RdfToQuadConverter(); @@ -25,24 +25,21 @@ describe('A RdfToQuadConverter.test.ts', (): void => { }); it('can handle turtle to quad conversions.', async(): Promise => { - const metadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, 'text/turtle'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }); const representation = { metadata } as Representation; const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]}; await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined(); }); it('can handle JSON-LD to quad conversions.', async(): Promise => { - const metadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, 'application/ld+json'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'application/ld+json' }); const representation = { metadata } as Representation; const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]}; await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined(); }); it('converts turtle to quads.', async(): Promise => { - const metadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, 'text/turtle'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }); const representation = { data: streamifyArray([ ' .' ]), metadata, @@ -54,7 +51,7 @@ describe('A RdfToQuadConverter.test.ts', (): void => { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual(INTERNAL_QUADS); + expect(result.metadata.contentType).toEqual(INTERNAL_QUADS); await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple( namedNode('http://test.com/s'), namedNode('http://test.com/p'), @@ -63,8 +60,7 @@ describe('A RdfToQuadConverter.test.ts', (): void => { }); it('converts JSON-LD to quads.', async(): Promise => { - const metadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, 'application/ld+json'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'application/ld+json' }); const representation = { data: streamifyArray([ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ]), metadata, @@ -76,7 +72,7 @@ describe('A RdfToQuadConverter.test.ts', (): void => { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual(INTERNAL_QUADS); + expect(result.metadata.contentType).toEqual(INTERNAL_QUADS); await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple( namedNode('http://test.com/s'), namedNode('http://test.com/p'), @@ -85,8 +81,7 @@ describe('A RdfToQuadConverter.test.ts', (): void => { }); it('throws an UnsupportedHttpError on invalid triple data.', async(): Promise => { - const metadata = new RepresentationMetadata(); - metadata.set(CONTENT_TYPE, 'text/turtle'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }); const representation = { data: streamifyArray([ ' { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual(INTERNAL_QUADS); + expect(result.metadata.contentType).toEqual(INTERNAL_QUADS); await expect(arrayifyStream(result.data)).rejects.toThrow(UnsupportedHttpError); }); }); diff --git a/test/unit/storage/conversion/TurtleToQuadConverter.test.ts b/test/unit/storage/conversion/TurtleToQuadConverter.test.ts index 8468f7c8a2..c708248d5f 100644 --- a/test/unit/storage/conversion/TurtleToQuadConverter.test.ts +++ b/test/unit/storage/conversion/TurtleToQuadConverter.test.ts @@ -9,13 +9,12 @@ import { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceI import { TurtleToQuadConverter } from '../../../../src/storage/conversion/TurtleToQuadConverter'; import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; +import { MA_CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; describe('A TurtleToQuadConverter', (): void => { const converter = new TurtleToQuadConverter(); const identifier: ResourceIdentifier = { path: 'path' }; - const metadata = new RepresentationMetadata(); - metadata.add(CONTENT_TYPE, 'text/turtle'); + const metadata = new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }); it('can handle turtle to quad conversions.', async(): Promise => { const representation = { metadata } as Representation; @@ -35,7 +34,7 @@ describe('A TurtleToQuadConverter', (): void => { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual(INTERNAL_QUADS); + expect(result.metadata.contentType).toEqual(INTERNAL_QUADS); await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple( namedNode('http://test.com/s'), namedNode('http://test.com/p'), @@ -55,7 +54,7 @@ describe('A TurtleToQuadConverter', (): void => { data: expect.any(Readable), metadata: expect.any(RepresentationMetadata), }); - expect(result.metadata.get(CONTENT_TYPE)?.value).toEqual(INTERNAL_QUADS); + expect(result.metadata.contentType).toEqual(INTERNAL_QUADS); await expect(arrayifyStream(result.data)).rejects.toThrow(UnsupportedHttpError); }); }); diff --git a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts index 84fd7320b1..8d3d96e245 100644 --- a/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts +++ b/test/unit/storage/patch/SparqlUpdatePatchHandler.test.ts @@ -11,7 +11,6 @@ import { ResourceLocker } from '../../../../src/storage/ResourceLocker'; import { ResourceStore } from '../../../../src/storage/ResourceStore'; import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; -import { CONTENT_TYPE } from '../../../../src/util/MetadataTypes'; describe('A SparqlUpdatePatchHandler', (): void => { let handler: SparqlUpdatePatchHandler; @@ -77,7 +76,7 @@ describe('A SparqlUpdatePatchHandler', (): void => { binary: false, metadata: expect.any(RepresentationMetadata), })); - expect(setParams[1].metadata.get(CONTENT_TYPE)?.value).toEqual(INTERNAL_QUADS); + expect(setParams[1].metadata.contentType).toEqual(INTERNAL_QUADS); await expect(arrayifyStream(setParams[1].data)).resolves.toBeRdfIsomorphic(quads); }; diff --git a/test/util/TestHelpers.ts b/test/util/TestHelpers.ts index ad86eb6900..fc1367609a 100644 --- a/test/util/TestHelpers.ts +++ b/test/util/TestHelpers.ts @@ -5,10 +5,11 @@ import { join } from 'path'; import * as url from 'url'; import { createResponse, MockResponse } from 'node-mocks-http'; import streamifyArray from 'streamify-array'; -import { ResourceStore } from '../../index'; +import { RepresentationMetadata, ResourceStore } from '../../index'; import { PermissionSet } from '../../src/ldp/permissions/PermissionSet'; import { HttpHandler } from '../../src/server/HttpHandler'; import { HttpRequest } from '../../src/server/HttpRequest'; +import { MA_CONTENT_TYPE } from '../../src/util/MetadataTypes'; import { call } from './Util'; export class AclTestHelper { @@ -49,11 +50,7 @@ export class AclTestHelper { const representation = { binary: true, data: streamifyArray(acl), - metadata: { - raw: [], - profiles: [], - contentType: 'text/turtle', - }, + metadata: new RepresentationMetadata({ [MA_CONTENT_TYPE]: 'text/turtle' }), }; return this.store.setRepresentation( diff --git a/tsdoc.json b/tsdoc.json index 8d4ea8b117..d7ca66faa3 100644 --- a/tsdoc.json +++ b/tsdoc.json @@ -6,4 +6,4 @@ "syntaxKind": "modifier" } ] -} \ No newline at end of file +}