Skip to content

Commit 668d0a3

Browse files
authored
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
1 parent db906ae commit 668d0a3

26 files changed

+172
-183
lines changed

src/http/auxiliary/LinkMetadataGenerator.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DataFactory } from 'n3';
2+
import type { NamedNode } from 'rdf-js';
23
import { SOLID_META } from '../../util/Vocabularies';
34
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
45
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
@@ -11,12 +12,12 @@ import { MetadataGenerator } from './MetadataGenerator';
1112
* In case the input is metadata of an auxiliary resource no metadata will be added
1213
*/
1314
export class LinkMetadataGenerator extends MetadataGenerator {
14-
private readonly link: string;
15+
private readonly link: NamedNode;
1516
private readonly identifierStrategy: AuxiliaryIdentifierStrategy;
1617

1718
public constructor(link: string, identifierStrategy: AuxiliaryIdentifierStrategy) {
1819
super();
19-
this.link = link;
20+
this.link = DataFactory.namedNode(link);
2021
this.identifierStrategy = identifierStrategy;
2122
}
2223

src/http/input/metadata/SlugParser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class SlugParser extends MetadataParser {
1919
throw new BadRequestHttpError('Request has multiple Slug headers');
2020
}
2121
this.logger.debug(`Request Slug is '${slug}'.`);
22-
input.metadata.set(SOLID_HTTP.slug, slug);
22+
input.metadata.set(SOLID_HTTP.terms.slug, slug);
2323
}
2424
}
2525
}

src/http/output/metadata/LinkRelMetadataWriter.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { NamedNode } from 'n3';
2+
import { DataFactory } from 'n3';
13
import { getLoggerFor } from '../../../logging/LogUtil';
24
import type { HttpResponse } from '../../../server/HttpResponse';
35
import { addHeader } from '../../../util/HeaderUtil';
@@ -9,19 +11,23 @@ import { MetadataWriter } from './MetadataWriter';
911
* The values of the objects will be put in a Link header with the corresponding "rel" value.
1012
*/
1113
export class LinkRelMetadataWriter extends MetadataWriter {
12-
private readonly linkRelMap: Record<string, string>;
14+
private readonly linkRelMap: Map<NamedNode, string>;
1315
protected readonly logger = getLoggerFor(this);
1416

1517
public constructor(linkRelMap: Record<string, string>) {
1618
super();
17-
this.linkRelMap = linkRelMap;
19+
20+
this.linkRelMap = new Map<NamedNode, string>();
21+
for (const [ key, value ] of Object.entries(linkRelMap)) {
22+
this.linkRelMap.set(DataFactory.namedNode(key), value);
23+
}
1824
}
1925

2026
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
21-
const keys = Object.keys(this.linkRelMap);
22-
this.logger.debug(`Available link relations: ${keys.length}`);
23-
for (const key of keys) {
24-
const values = input.metadata.getAll(key).map((term): string => `<${term.value}>; rel="${this.linkRelMap[key]}"`);
27+
this.logger.debug(`Available link relations: ${this.linkRelMap.size}`);
28+
for (const [ predicate, relValue ] of this.linkRelMap) {
29+
const values = input.metadata.getAll(predicate)
30+
.map((term): string => `<${term.value}>; rel="${relValue}"`);
2531
if (values.length > 0) {
2632
this.logger.debug(`Adding Link header ${values}`);
2733
addHeader(input.response, 'Link', values);

src/http/output/metadata/MappedMetadataWriter.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { NamedNode } from 'n3';
2+
import { DataFactory } from 'n3';
13
import type { HttpResponse } from '../../../server/HttpResponse';
24
import { addHeader } from '../../../util/HeaderUtil';
35
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
@@ -8,11 +10,15 @@ import { MetadataWriter } from './MetadataWriter';
810
* The header value(s) will be the same as the corresponding object value(s).
911
*/
1012
export class MappedMetadataWriter extends MetadataWriter {
11-
private readonly headerMap: [string, string][];
13+
private readonly headerMap: Map<NamedNode, string>;
1214

1315
public constructor(headerMap: Record<string, string>) {
1416
super();
15-
this.headerMap = Object.entries(headerMap);
17+
18+
this.headerMap = new Map<NamedNode, string>();
19+
for (const [ key, value ] of Object.entries(headerMap)) {
20+
this.headerMap.set(DataFactory.namedNode(key), value);
21+
}
1622
}
1723

1824
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {

src/http/representation/RepresentationMetadata.ts

+34-19
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getLoggerFor } from '../../logging/LogUtil';
44
import { InternalServerError } from '../../util/errors/InternalServerError';
55
import type { ContentType } from '../../util/HeaderUtil';
66
import { parseContentType } from '../../util/HeaderUtil';
7-
import { toNamedTerm, toObjectTerm, toCachedNamedNode, isTerm, toLiteral } from '../../util/TermUtil';
7+
import { toNamedTerm, toObjectTerm, isTerm, toLiteral } from '../../util/TermUtil';
88
import { CONTENT_TYPE_TERM, CONTENT_LENGTH_TERM, XSD, SOLID_META, RDFS } from '../../util/Vocabularies';
99
import type { ResourceIdentifier } from './ResourceIdentifier';
1010
import { isResourceIdentifier } from './ResourceIdentifier';
@@ -21,6 +21,22 @@ export function isRepresentationMetadata(object: any): object is RepresentationM
2121
return typeof object?.setMetadata === 'function';
2222
}
2323

24+
// Caches named node conversions
25+
const cachedNamedNodes: Record<string, NamedNode> = {};
26+
27+
/**
28+
* Converts the incoming name (URI or shorthand) to a named node.
29+
* The generated terms get cached to reduce the number of created nodes,
30+
* so only use this for internal constants!
31+
* @param name - Predicate to potentially transform.
32+
*/
33+
function toCachedNamedNode(name: string): NamedNode {
34+
if (!(name in cachedNamedNodes)) {
35+
cachedNamedNodes[name] = DataFactory.namedNode(name);
36+
}
37+
return cachedNamedNodes[name];
38+
}
39+
2440
/**
2541
* Stores the metadata triples and provides methods for easy access.
2642
* Most functions return the metadata object to allow for chaining.
@@ -116,7 +132,7 @@ export class RepresentationMetadata {
116132
*/
117133
public quads(
118134
subject: NamedNode | BlankNode | string | null = null,
119-
predicate: NamedNode | string | null = null,
135+
predicate: NamedNode | null = null,
120136
object: NamedNode | BlankNode | Literal | string | null = null,
121137
graph: MetadataGraph | null = null,
122138
): Quad[] {
@@ -167,12 +183,12 @@ export class RepresentationMetadata {
167183
*/
168184
public addQuad(
169185
subject: NamedNode | BlankNode | string,
170-
predicate: NamedNode | string,
186+
predicate: NamedNode,
171187
object: NamedNode | BlankNode | Literal | string,
172188
graph?: MetadataGraph,
173189
): this {
174190
this.store.addQuad(toNamedTerm(subject),
175-
toCachedNamedNode(predicate),
191+
predicate,
176192
toObjectTerm(object, true),
177193
graph ? toNamedTerm(graph) : undefined);
178194
return this;
@@ -194,12 +210,12 @@ export class RepresentationMetadata {
194210
*/
195211
public removeQuad(
196212
subject: NamedNode | BlankNode | string,
197-
predicate: NamedNode | string,
213+
predicate: NamedNode,
198214
object: NamedNode | BlankNode | Literal | string,
199215
graph?: MetadataGraph,
200216
): this {
201217
const quads = this.quads(toNamedTerm(subject),
202-
toCachedNamedNode(predicate),
218+
predicate,
203219
toObjectTerm(object, true),
204220
graph ? toNamedTerm(graph) : undefined);
205221
return this.removeQuads(quads);
@@ -219,7 +235,7 @@ export class RepresentationMetadata {
219235
* @param object - Value(s) to add.
220236
* @param graph - Optional graph of where to add the values to.
221237
*/
222-
public add(predicate: NamedNode | string, object: MetadataValue, graph?: MetadataGraph): this {
238+
public add(predicate: NamedNode, object: MetadataValue, graph?: MetadataGraph): this {
223239
return this.forQuads(predicate, object, (pred, obj): any => this.addQuad(this.id, pred, obj, graph));
224240
}
225241

@@ -229,20 +245,19 @@ export class RepresentationMetadata {
229245
* @param object - Value(s) to remove.
230246
* @param graph - Optional graph of where to remove the values from.
231247
*/
232-
public remove(predicate: NamedNode | string, object: MetadataValue, graph?: MetadataGraph): this {
248+
public remove(predicate: NamedNode, object: MetadataValue, graph?: MetadataGraph): this {
233249
return this.forQuads(predicate, object, (pred, obj): any => this.removeQuad(this.id, pred, obj, graph));
234250
}
235251

236252
/**
237253
* Helper function to simplify add/remove
238254
* Runs the given function on all predicate/object pairs, but only converts the predicate to a named node once.
239255
*/
240-
private forQuads(predicate: NamedNode | string, object: MetadataValue,
256+
private forQuads(predicate: NamedNode, object: MetadataValue,
241257
forFn: (pred: NamedNode, obj: NamedNode | Literal) => void): this {
242-
const predicateNode = toCachedNamedNode(predicate);
243258
const objects = Array.isArray(object) ? object : [ object ];
244259
for (const obj of objects) {
245-
forFn(predicateNode, toObjectTerm(obj, true));
260+
forFn(predicate, toObjectTerm(obj, true));
246261
}
247262
return this;
248263
}
@@ -252,8 +267,8 @@ export class RepresentationMetadata {
252267
* @param predicate - Predicate to remove.
253268
* @param graph - Optional graph where to remove from.
254269
*/
255-
public removeAll(predicate: NamedNode | string, graph?: MetadataGraph): this {
256-
this.removeQuads(this.store.getQuads(this.id, toCachedNamedNode(predicate), null, graph ?? null));
270+
public removeAll(predicate: NamedNode, graph?: MetadataGraph): this {
271+
this.removeQuads(this.store.getQuads(this.id, predicate, null, graph ?? null));
257272
return this;
258273
}
259274

@@ -278,8 +293,8 @@ export class RepresentationMetadata {
278293
*
279294
* @returns An array with all matches.
280295
*/
281-
public getAll(predicate: NamedNode | string, graph?: MetadataGraph): Term[] {
282-
return this.store.getQuads(this.id, toCachedNamedNode(predicate), null, graph ?? null)
296+
public getAll(predicate: NamedNode, graph?: MetadataGraph): Term[] {
297+
return this.store.getQuads(this.id, predicate, null, graph ?? null)
283298
.map((quad): Term => quad.object);
284299
}
285300

@@ -292,15 +307,15 @@ export class RepresentationMetadata {
292307
*
293308
* @returns The corresponding value. Undefined if there is no match
294309
*/
295-
public get(predicate: NamedNode | string, graph?: MetadataGraph): Term | undefined {
310+
public get(predicate: NamedNode, graph?: MetadataGraph): Term | undefined {
296311
const terms = this.getAll(predicate, graph);
297312
if (terms.length === 0) {
298313
return;
299314
}
300315
if (terms.length > 1) {
301-
this.logger.error(`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`);
316+
this.logger.error(`Multiple results for ${predicate.value}`);
302317
throw new InternalServerError(
303-
`Multiple results for ${typeof predicate === 'string' ? predicate : predicate.value}`,
318+
`Multiple results for ${predicate.value}`,
304319
);
305320
}
306321
return terms[0];
@@ -313,7 +328,7 @@ export class RepresentationMetadata {
313328
* @param object - Value(s) to set.
314329
* @param graph - Optional graph where the triple should be stored.
315330
*/
316-
public set(predicate: NamedNode | string, object?: MetadataValue, graph?: MetadataGraph): this {
331+
public set(predicate: NamedNode, object?: MetadataValue, graph?: MetadataGraph): this {
317332
this.removeAll(predicate, graph);
318333
if (object) {
319334
this.add(predicate, object, graph);

src/storage/DataAccessorBasedStore.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929
import { parseQuads } from '../util/QuadUtil';
3030
import { addResourceMetadata, updateModifiedDate } from '../util/ResourceUtil';
3131
import {
32-
CONTENT_TYPE,
3332
DC,
3433
SOLID_HTTP,
3534
LDP,
@@ -39,6 +38,7 @@ import {
3938
XSD,
4039
SOLID_META,
4140
PREFERRED_PREFIX_TERM,
41+
CONTENT_TYPE_TERM,
4242
} from '../util/Vocabularies';
4343
import type { DataAccessor } from './accessors/DataAccessor';
4444
import type { Conditions } from './Conditions';
@@ -435,7 +435,7 @@ export class DataAccessorBasedStore implements ResourceStore {
435435
}
436436

437437
// Input content type doesn't matter anymore
438-
representation.metadata.removeAll(CONTENT_TYPE);
438+
representation.metadata.removeAll(CONTENT_TYPE_TERM);
439439

440440
// Container data is stored in the metadata
441441
representation.metadata.addQuads(quads);
@@ -516,8 +516,8 @@ export class DataAccessorBasedStore implements ResourceStore {
516516
Promise<ResourceIdentifier> {
517517
// Get all values needed for naming the resource
518518
const isContainer = this.isContainerType(metadata);
519-
const slug = metadata.get(SOLID_HTTP.slug)?.value;
520-
metadata.removeAll(SOLID_HTTP.slug);
519+
const slug = metadata.get(SOLID_HTTP.terms.slug)?.value;
520+
metadata.removeAll(SOLID_HTTP.terms.slug);
521521

522522
let newID: ResourceIdentifier = this.createURI(container, isContainer, slug);
523523

@@ -544,7 +544,7 @@ export class DataAccessorBasedStore implements ResourceStore {
544544
* @param metadata - Metadata of the (new) resource.
545545
*/
546546
protected isContainerType(metadata: RepresentationMetadata): boolean {
547-
return this.hasContainerType(metadata.getAll(RDF.type));
547+
return this.hasContainerType(metadata.getAll(RDF.terms.type));
548548
}
549549

550550
/**
@@ -558,7 +558,7 @@ export class DataAccessorBasedStore implements ResourceStore {
558558
* Verifies if this is the metadata of a root storage container.
559559
*/
560560
protected isRootStorage(metadata: RepresentationMetadata): boolean {
561-
return metadata.getAll(RDF.type).some((term): boolean => term.value === PIM.Storage);
561+
return metadata.getAll(RDF.terms.type).some((term): boolean => term.value === PIM.Storage);
562562
}
563563

564564
/**

src/storage/accessors/FileDataAccessor.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil';
1616
import { parseQuads, serializeQuads } from '../../util/QuadUtil';
1717
import { addResourceMetadata, updateModifiedDate } from '../../util/ResourceUtil';
1818
import { toLiteral, toNamedTerm } from '../../util/TermUtil';
19-
import { CONTENT_TYPE, DC, IANA, LDP, POSIX, RDF, SOLID_META, XSD } from '../../util/Vocabularies';
19+
import { CONTENT_TYPE_TERM, DC, IANA, LDP, POSIX, RDF, SOLID_META, XSD } from '../../util/Vocabularies';
2020
import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper';
2121
import type { DataAccessor } from './DataAccessor';
2222

@@ -174,7 +174,7 @@ export class FileDataAccessor implements DataAccessor {
174174
private async getFileMetadata(link: ResourceLink, stats: Stats):
175175
Promise<RepresentationMetadata> {
176176
return (await this.getBaseMetadata(link, stats, false))
177-
.set(CONTENT_TYPE, link.contentType);
177+
.set(CONTENT_TYPE_TERM, link.contentType);
178178
}
179179

180180
/**
@@ -202,7 +202,7 @@ export class FileDataAccessor implements DataAccessor {
202202
metadata.remove(RDF.terms.type, LDP.terms.Container);
203203
metadata.remove(RDF.terms.type, LDP.terms.BasicContainer);
204204
metadata.removeAll(DC.terms.modified);
205-
metadata.removeAll(CONTENT_TYPE);
205+
metadata.removeAll(CONTENT_TYPE_TERM);
206206
const quads = metadata.quads();
207207
const metadataLink = await this.resourceMapper.mapUrlToFilePath(link.identifier, true);
208208
let wroteMetadata: boolean;

src/storage/accessors/SparqlDataAccessor.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { guardStream } from '../../util/GuardedStream';
2727
import type { Guarded } from '../../util/GuardedStream';
2828
import type { IdentifierStrategy } from '../../util/identifiers/IdentifierStrategy';
2929
import { isContainerIdentifier } from '../../util/PathUtil';
30-
import { CONTENT_TYPE, LDP } from '../../util/Vocabularies';
30+
import { LDP, CONTENT_TYPE_TERM } from '../../util/Vocabularies';
3131
import type { DataAccessor } from './DataAccessor';
3232

3333
const { defaultGraph, namedNode, quad, variable } = DataFactory;
@@ -132,7 +132,7 @@ export class SparqlDataAccessor implements DataAccessor {
132132
}
133133

134134
// Not relevant since all content is triples
135-
metadata.removeAll(CONTENT_TYPE);
135+
metadata.removeAll(CONTENT_TYPE_TERM);
136136

137137
return this.sendSparqlUpdate(this.sparqlInsert(name, metadata, parent, triples));
138138
}

src/storage/quota/PodQuotaStrategy.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class PodQuotaStrategy extends QuotaStrategy {
5757
throw error;
5858
}
5959

60-
const hasPimStorageMetadata = metadata!.getAll(RDF.type)
60+
const hasPimStorageMetadata = metadata!.getAll(RDF.terms.type)
6161
.some((term): boolean => term.value === PIM.Storage);
6262

6363
return hasPimStorageMetadata ? identifier : this.searchPimStorage(parent);

src/util/TermUtil.ts

-27
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,8 @@
11
import { DataFactory } from 'n3';
22
import type { NamedNode, Literal, Term } from 'rdf-js';
3-
import { CONTENT_TYPE_TERM } from './Vocabularies';
43

54
const { namedNode, literal } = DataFactory;
65

7-
// Shorthands for commonly used predicates
8-
const shorthands: Record<string, NamedNode> = {
9-
contentType: CONTENT_TYPE_TERM,
10-
};
11-
12-
// Caches named node conversions
13-
const cachedNamedNodes: Record<string, NamedNode> = {
14-
...shorthands,
15-
};
16-
17-
/**
18-
* Converts the incoming name (URI or shorthand) to a named node.
19-
* The generated terms get cached to reduce the number of created nodes,
20-
* so only use this for internal constants!
21-
* @param name - Predicate to potentially transform.
22-
*/
23-
export function toCachedNamedNode(name: NamedNode | string): NamedNode {
24-
if (typeof name !== 'string') {
25-
return name;
26-
}
27-
if (!(name in cachedNamedNodes)) {
28-
cachedNamedNodes[name] = namedNode(name);
29-
}
30-
return cachedNamedNodes[name];
31-
}
32-
336
/**
347
* @param input - Checks if this is a {@link Term}.
358
*/

test/integration/LockingResourceStore.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('A LockingResourceStore', (): void => {
4545

4646
// Initialize store
4747
const metadata = new RepresentationMetadata({ path: base }, TEXT_TURTLE);
48-
metadata.add(RDF.type, PIM.terms.Storage);
48+
metadata.add(RDF.terms.type, PIM.terms.Storage);
4949
await source.setRepresentation({ path: base }, new BasicRepresentation([], metadata));
5050

5151
locker = new EqualReadWriteLocker(new SingleThreadedResourceLocker());

0 commit comments

Comments
 (0)