feat: Only accept NamedNodes as predicates for metadata

* refactor: move toCachedNamedNode (private)

* chore: only NamedNodes predicates in removes

* feat: enforce NamedNode predicates in most cases

* feat: getAll only accepts NamedNodes

* feat: toCachedNamedNode only accepts string arg

* tests: use NamedNodes for getAll calls

* test: remove unnecessary string check for coverage

* tests: fix NamedNodes in new tests after rebase

* feat: metadatawriters store NamedNodes

* refactor: toCachedNamedNode as utility function

* fix: double write of linkRelMap

* test: use the CONTENT_TYPE constant
This commit is contained in:
Jasper Vaneessen
2022-04-15 09:53:39 +02:00
committed by GitHub
parent db906ae872
commit 668d0a331f
26 changed files with 172 additions and 183 deletions

View File

@@ -1,4 +1,5 @@
import { DataFactory } from 'n3';
import type { NamedNode } from 'rdf-js';
import { SOLID_META } from '../../util/Vocabularies';
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
@@ -11,12 +12,12 @@ import { MetadataGenerator } from './MetadataGenerator';
* In case the input is metadata of an auxiliary resource no metadata will be added
*/
export class LinkMetadataGenerator extends MetadataGenerator {
private readonly link: string;
private readonly link: NamedNode;
private readonly identifierStrategy: AuxiliaryIdentifierStrategy;
public constructor(link: string, identifierStrategy: AuxiliaryIdentifierStrategy) {
super();
this.link = link;
this.link = DataFactory.namedNode(link);
this.identifierStrategy = identifierStrategy;
}

View File

@@ -19,7 +19,7 @@ export class SlugParser extends MetadataParser {
throw new BadRequestHttpError('Request has multiple Slug headers');
}
this.logger.debug(`Request Slug is '${slug}'.`);
input.metadata.set(SOLID_HTTP.slug, slug);
input.metadata.set(SOLID_HTTP.terms.slug, slug);
}
}
}

View File

@@ -1,3 +1,5 @@
import type { NamedNode } from 'n3';
import { DataFactory } from 'n3';
import { getLoggerFor } from '../../../logging/LogUtil';
import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/HeaderUtil';
@@ -9,19 +11,23 @@ import { MetadataWriter } from './MetadataWriter';
* The values of the objects will be put in a Link header with the corresponding "rel" value.
*/
export class LinkRelMetadataWriter extends MetadataWriter {
private readonly linkRelMap: Record<string, string>;
private readonly linkRelMap: Map<NamedNode, string>;
protected readonly logger = getLoggerFor(this);
public constructor(linkRelMap: Record<string, string>) {
super();
this.linkRelMap = linkRelMap;
this.linkRelMap = new Map<NamedNode, string>();
for (const [ key, value ] of Object.entries(linkRelMap)) {
this.linkRelMap.set(DataFactory.namedNode(key), value);
}
}
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
const keys = Object.keys(this.linkRelMap);
this.logger.debug(`Available link relations: ${keys.length}`);
for (const key of keys) {
const values = input.metadata.getAll(key).map((term): string => `<${term.value}>; rel="${this.linkRelMap[key]}"`);
this.logger.debug(`Available link relations: ${this.linkRelMap.size}`);
for (const [ predicate, relValue ] of this.linkRelMap) {
const values = input.metadata.getAll(predicate)
.map((term): string => `<${term.value}>; rel="${relValue}"`);
if (values.length > 0) {
this.logger.debug(`Adding Link header ${values}`);
addHeader(input.response, 'Link', values);

View File

@@ -1,3 +1,5 @@
import type { NamedNode } from 'n3';
import { DataFactory } from 'n3';
import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/HeaderUtil';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
@@ -8,11 +10,15 @@ import { MetadataWriter } from './MetadataWriter';
* The header value(s) will be the same as the corresponding object value(s).
*/
export class MappedMetadataWriter extends MetadataWriter {
private readonly headerMap: [string, string][];
private readonly headerMap: Map<NamedNode, string>;
public constructor(headerMap: Record<string, string>) {
super();
this.headerMap = Object.entries(headerMap);
this.headerMap = new Map<NamedNode, string>();
for (const [ key, value ] of Object.entries(headerMap)) {
this.headerMap.set(DataFactory.namedNode(key), value);
}
}
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {

View File

@@ -4,7 +4,7 @@ import { getLoggerFor } from '../../logging/LogUtil';
import { InternalServerError } from '../../util/errors/InternalServerError';
import type { ContentType } from '../../util/HeaderUtil';
import { parseContentType } from '../../util/HeaderUtil';
import { toNamedTerm, toObjectTerm, toCachedNamedNode, isTerm, toLiteral } from '../../util/TermUtil';
import { toNamedTerm, toObjectTerm, isTerm, toLiteral } from '../../util/TermUtil';
import { CONTENT_TYPE_TERM, CONTENT_LENGTH_TERM, XSD, SOLID_META, RDFS } from '../../util/Vocabularies';
import type { ResourceIdentifier } from './ResourceIdentifier';
import { isResourceIdentifier } from './ResourceIdentifier';
@@ -21,6 +21,22 @@ export function isRepresentationMetadata(object: any): object is RepresentationM
return typeof object?.setMetadata === 'function';
}
// Caches named node conversions
const cachedNamedNodes: Record<string, NamedNode> = {};
/**
* Converts the incoming name (URI or shorthand) to a named node.
* The generated terms get cached to reduce the number of created nodes,
* so only use this for internal constants!
* @param name - Predicate to potentially transform.
*/
function toCachedNamedNode(name: string): NamedNode {
if (!(name in cachedNamedNodes)) {
cachedNamedNodes[name] = DataFactory.namedNode(name);
}
return cachedNamedNodes[name];
}
/**
* Stores the metadata triples and provides methods for easy access.
* Most functions return the metadata object to allow for chaining.
@@ -116,7 +132,7 @@ export class RepresentationMetadata {
*/
public quads(
subject: NamedNode | BlankNode | string | null = null,
predicate: NamedNode | string | null = null,
predicate: NamedNode | null = null,
object: NamedNode | BlankNode | Literal | string | null = null,
graph: MetadataGraph | null = null,
): Quad[] {
@@ -167,12 +183,12 @@ export class RepresentationMetadata {
*/
public addQuad(
subject: NamedNode | BlankNode | string,
predicate: NamedNode | string,
predicate: NamedNode,
object: NamedNode | BlankNode | Literal | string,
graph?: MetadataGraph,
): this {
this.store.addQuad(toNamedTerm(subject),
toCachedNamedNode(predicate),
predicate,
toObjectTerm(object, true),
graph ? toNamedTerm(graph) : undefined);
return this;
@@ -194,12 +210,12 @@ export class RepresentationMetadata {
*/
public removeQuad(
subject: NamedNode | BlankNode | string,
predicate: NamedNode | string,
predicate: NamedNode,
object: NamedNode | BlankNode | Literal | string,
graph?: MetadataGraph,
): this {
const quads = this.quads(toNamedTerm(subject),
toCachedNamedNode(predicate),
predicate,
toObjectTerm(object, true),
graph ? toNamedTerm(graph) : undefined);
return this.removeQuads(quads);
@@ -219,7 +235,7 @@ export class RepresentationMetadata {
* @param object - Value(s) to add.
* @param graph - Optional graph of where to add the values to.
*/
public add(predicate: NamedNode | string, object: MetadataValue, graph?: MetadataGraph): this {
public add(predicate: NamedNode, object: MetadataValue, graph?: MetadataGraph): this {
return this.forQuads(predicate, object, (pred, obj): any => this.addQuad(this.id, pred, obj, graph));
}
@@ -229,7 +245,7 @@ export class RepresentationMetadata {
* @param object - Value(s) to remove.
* @param graph - Optional graph of where to remove the values from.
*/
public remove(predicate: NamedNode | string, object: MetadataValue, graph?: MetadataGraph): this {
public remove(predicate: NamedNode, object: MetadataValue, graph?: MetadataGraph): this {
return this.forQuads(predicate, object, (pred, obj): any => this.removeQuad(this.id, pred, obj, graph));
}
@@ -237,12 +253,11 @@ export class RepresentationMetadata {
* Helper function to simplify add/remove
* Runs the given function on all predicate/object pairs, but only converts the predicate to a named node once.
*/
private forQuads(predicate: NamedNode | string, object: MetadataValue,
private forQuads(predicate: NamedNode, object: MetadataValue,
forFn: (pred: NamedNode, obj: NamedNode | Literal) => void): this {
const predicateNode = toCachedNamedNode(predicate);
const objects = Array.isArray(object) ? object : [ object ];
for (const obj of objects) {
forFn(predicateNode, toObjectTerm(obj, true));
forFn(predicate, toObjectTerm(obj, true));
}
return this;
}
@@ -252,8 +267,8 @@ export class RepresentationMetadata {
* @param predicate - Predicate to remove.
* @param graph - Optional graph where to remove from.
*/
public removeAll(predicate: NamedNode | string, graph?: MetadataGraph): this {
this.removeQuads(this.store.getQuads(this.id, toCachedNamedNode(predicate), null, graph ?? null));
public removeAll(predicate: NamedNode, graph?: MetadataGraph): this {
this.removeQuads(this.store.getQuads(this.id, predicate, null, graph ?? null));
return this;
}
@@ -278,8 +293,8 @@ export class RepresentationMetadata {
*
* @returns An array with all matches.
*/
public getAll(predicate: NamedNode | string, graph?: MetadataGraph): Term[] {
return this.store.getQuads(this.id, toCachedNamedNode(predicate), null, graph ?? null)
public getAll(predicate: NamedNode, graph?: MetadataGraph): Term[] {
return this.store.getQuads(this.id, predicate, null, graph ?? null)
.map((quad): Term => quad.object);
}
@@ -292,15 +307,15 @@ export class RepresentationMetadata {
*
* @returns The corresponding value. Undefined if there is no match
*/
public get(predicate: NamedNode | string, graph?: MetadataGraph): Term | undefined {
public get(predicate: NamedNode, graph?: MetadataGraph): Term | undefined {
const terms = this.getAll(predicate, graph);
if (terms.length === 0) {
return;
}
if (terms.length > 1) {
this.logger.error(`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`);
this.logger.error(`Multiple results for ${predicate.value}`);
throw new InternalServerError(
`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`,
`Multiple results for ${predicate.value}`,
);
}
return terms[0];
@@ -313,7 +328,7 @@ export class RepresentationMetadata {
* @param object - Value(s) to set.
* @param graph - Optional graph where the triple should be stored.
*/
public set(predicate: NamedNode | string, object?: MetadataValue, graph?: MetadataGraph): this {
public set(predicate: NamedNode, object?: MetadataValue, graph?: MetadataGraph): this {
this.removeAll(predicate, graph);
if (object) {
this.add(predicate, object, graph);