Skip to content

Commit

Permalink
refactor: Also create named nodes for vocabularies.
Browse files Browse the repository at this point in the history
  • Loading branch information
RubenVerborgh committed Jan 2, 2021
1 parent 8e138c3 commit ae06e99
Show file tree
Hide file tree
Showing 14 changed files with 128 additions and 88 deletions.
3 changes: 1 addition & 2 deletions src/init/RootContainerInitializer.ts
Expand Up @@ -8,7 +8,6 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { ensureTrailingSlash } from '../util/PathUtil';
import { generateResourceQuads } from '../util/ResourceUtil';
import { guardedStreamFrom } from '../util/StreamUtil';
import { toCachedNamedNode } from '../util/UriUtil';
import { PIM, RDF } from '../util/Vocabularies';
import { Initializer } from './Initializer';
import namedNode = DataFactory.namedNode;
Expand Down Expand Up @@ -60,7 +59,7 @@ export class RootContainerInitializer extends Initializer {

// Make sure the root container is a pim:Storage
// This prevents deletion of the root container as storage root containers can not be deleted
metadata.add(RDF.type, toCachedNamedNode(PIM.Storage));
metadata.add(RDF.type, PIM.terms.Storage);

This comment has been minimized.

Copy link
@RubenVerborgh

RubenVerborgh Jan 2, 2021

Author Member

@joachimvh And this is a thing now too!


metadata.contentType = TEXT_TURTLE;

Expand Down
5 changes: 3 additions & 2 deletions src/ldp/representation/RepresentationMetadata.ts
Expand Up @@ -2,6 +2,7 @@ import { DataFactory, Store } from 'n3';
import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';
import { getLoggerFor } from '../../logging/LogUtil';
import { toObjectTerm, toCachedNamedNode, isTerm } from '../../util/UriUtil';
import { CONTENT_TYPE_TERM } from '../../util/Vocabularies';
import type { ResourceIdentifier } from './ResourceIdentifier';
import { isResourceIdentifier } from './ResourceIdentifier';

Expand Down Expand Up @@ -223,10 +224,10 @@ export class RepresentationMetadata {
* Shorthand for the CONTENT_TYPE predicate.
*/
public get contentType(): string | undefined {
return this.get(toCachedNamedNode('contentType'))?.value;
return this.get(CONTENT_TYPE_TERM)?.value;
}

public set contentType(input) {
this.set(toCachedNamedNode('contentType'), input);
this.set(CONTENT_TYPE_TERM, input);
}
}
18 changes: 9 additions & 9 deletions src/storage/accessors/FileDataAccessor.ts
Expand Up @@ -16,8 +16,8 @@ import type { Guarded } from '../../util/GuardedStream';
import { isContainerIdentifier } from '../../util/PathUtil';
import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
import { toCachedNamedNode, toLiteral } from '../../util/UriUtil';
import { CONTENT_TYPE, DCTERMS, LDP, POSIX, RDF, XSD } from '../../util/Vocabularies';
import { toLiteral } from '../../util/UriUtil';
import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../util/Vocabularies';
import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper';
import type { DataAccessor } from './DataAccessor';

Expand Down Expand Up @@ -210,9 +210,9 @@ export class FileDataAccessor implements DataAccessor {
*/
private async writeMetadata(link: ResourceLink, metadata: RepresentationMetadata): Promise<boolean> {
// These are stored by file system conventions
metadata.remove(RDF.type, toCachedNamedNode(LDP.Resource));
metadata.remove(RDF.type, toCachedNamedNode(LDP.Container));
metadata.remove(RDF.type, toCachedNamedNode(LDP.BasicContainer));
metadata.remove(RDF.type, LDP.terms.Resource);
metadata.remove(RDF.type, LDP.terms.Container);
metadata.remove(RDF.type, LDP.terms.BasicContainer);
metadata.removeAll(CONTENT_TYPE);
const quads = metadata.quads();
const metadataLink = await this.getMetadataLink(link.identifier);
Expand Down Expand Up @@ -329,10 +329,10 @@ export class FileDataAccessor implements DataAccessor {
*/
private generatePosixQuads(subject: NamedNode, stats: Stats): Quad[] {
const quads: Quad[] = [];
pushQuad(quads, subject, toCachedNamedNode(POSIX.size), toLiteral(stats.size, XSD.integer));
pushQuad(quads, subject, toCachedNamedNode(DCTERMS.modified), toLiteral(stats.mtime.toISOString(), XSD.dateTime));
pushQuad(quads, subject, toCachedNamedNode(POSIX.mtime), toLiteral(
Math.floor(stats.mtime.getTime() / 1000), XSD.integer,
pushQuad(quads, subject, POSIX.terms.size, toLiteral(stats.size, XSD.terms.integer));
pushQuad(quads, subject, DC.terms.modified, toLiteral(stats.mtime.toISOString(), XSD.terms.dateTime));
pushQuad(quads, subject, POSIX.terms.mtime, toLiteral(
Math.floor(stats.mtime.getTime() / 1000), XSD.terms.integer,
));
return quads;
}
Expand Down
5 changes: 2 additions & 3 deletions src/storage/accessors/SparqlDataAccessor.ts
Expand Up @@ -27,7 +27,6 @@ import { guardStream } from '../../util/GuardedStream';
import type { Guarded } from '../../util/GuardedStream';
import type { IdentifierStrategy } from '../../util/identifiers/IdentifierStrategy';
import { isContainerIdentifier } from '../../util/PathUtil';
import { toCachedNamedNode } from '../../util/UriUtil';
import { CONTENT_TYPE, LDP } from '../../util/Vocabularies';
import type { DataAccessor } from './DataAccessor';

Expand Down Expand Up @@ -226,7 +225,7 @@ export class SparqlDataAccessor implements DataAccessor {
// Insert new metadata and containment triple
const insert: GraphQuads[] = [ this.sparqlUpdateGraph(metaName, metadata.quads()) ];
if (parent) {
insert.push(this.sparqlUpdateGraph(parent, [ quad(parent, toCachedNamedNode(LDP.contains), name) ]));
insert.push(this.sparqlUpdateGraph(parent, [ quad(parent, LDP.terms.contains, name) ]));
}

// Necessary updates: delete metadata and insert new data
Expand Down Expand Up @@ -272,7 +271,7 @@ export class SparqlDataAccessor implements DataAccessor {
if (parent) {
update.updates.push({
updateType: 'delete',
delete: [ this.sparqlUpdateGraph(parent, [ quad(parent, toCachedNamedNode(LDP.contains), name) ]) ],
delete: [ this.sparqlUpdateGraph(parent, [ quad(parent, LDP.terms.contains, name) ]) ],
});
}

Expand Down
7 changes: 3 additions & 4 deletions src/util/ResourceUtil.ts
Expand Up @@ -2,7 +2,6 @@ import { DataFactory } from 'n3';
import type { NamedNode, Quad } from 'rdf-js';
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import { pushQuad } from './QuadUtil';
import { toCachedNamedNode } from './UriUtil';
import { LDP, RDF } from './Vocabularies';

/**
Expand All @@ -15,10 +14,10 @@ import { LDP, RDF } from './Vocabularies';
export const generateResourceQuads = (subject: NamedNode, isContainer: boolean): Quad[] => {
const quads: Quad[] = [];
if (isContainer) {
pushQuad(quads, subject, toCachedNamedNode(RDF.type), toCachedNamedNode(LDP.Container));
pushQuad(quads, subject, toCachedNamedNode(RDF.type), toCachedNamedNode(LDP.BasicContainer));
pushQuad(quads, subject, RDF.terms.type, LDP.terms.Container);
pushQuad(quads, subject, RDF.terms.type, LDP.terms.BasicContainer);
}
pushQuad(quads, subject, toCachedNamedNode(RDF.type), toCachedNamedNode(LDP.Resource));
pushQuad(quads, subject, RDF.terms.type, LDP.terms.Resource);

return quads;
};
Expand Down
6 changes: 3 additions & 3 deletions src/util/UriUtil.ts
@@ -1,12 +1,12 @@
import { DataFactory } from 'n3';
import type { Literal, NamedNode, Term } from 'rdf-js';
import { CONTENT_TYPE } from './Vocabularies';
import { CONTENT_TYPE_TERM } from './Vocabularies';

const { namedNode, literal } = DataFactory;

// Shorthands for commonly used predicates
const shorthands: Record<string, NamedNode> = {
contentType: DataFactory.namedNode(CONTENT_TYPE),
contentType: CONTENT_TYPE_TERM,
};

// Caches named node conversions
Expand Down Expand Up @@ -63,5 +63,5 @@ export const toObjectTerm = <T extends Term>(object: T | string, preferLiteral =
* @param object - Object value.
* @param dataType - Object data type (as string).
*/
export const toLiteral = (object: string | number, dataType: string | NamedNode): Literal =>
export const toLiteral = (object: string | number, dataType: NamedNode): Literal =>
DataFactory.literal(object, toCachedNamedNode(dataType));
66 changes: 49 additions & 17 deletions src/util/Vocabularies.ts
@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention, function-paren-newline */
import { namedNode } from '@rdfjs/data-model';
import type { NamedNode } from 'rdf-js';

type PrefixResolver<T> = (localName: string) => T;
type RecordOf<TKey extends any[], TValue> = Record<TKey[number], TValue>;
Expand All @@ -10,25 +12,54 @@ export type Namespace<TKey extends any[], TValue> =
* Creates a function that expands local names from the given base URI,
* and exports the given local names as properties on the returned object.
*/
export const createNamespace = <T extends string>(baseUri: string, ...localNames: T[]):
Namespace<typeof localNames, string> => {
export const createNamespace = <TKey extends string, TValue>(
baseUri: string,
toValue: (expanded: string) => TValue,
...localNames: TKey[]):
Namespace<typeof localNames, TValue> => {
// Create a function that expands local names
const expanded = {} as Record<string, string>;
const namespace = ((localName: string): string => {
const expanded = {} as Record<string, TValue>;
const namespace = ((localName: string): TValue => {
if (!(localName in expanded)) {
expanded[localName] = `${baseUri}${localName}`;
expanded[localName] = toValue(`${baseUri}${localName}`);
}
return expanded[localName];
}) as Namespace<typeof localNames, string>;
}) as Namespace<typeof localNames, TValue>;

// Expose the listed local names as properties
for (const localName of localNames) {
(namespace as RecordOf<typeof localNames, string>)[localName] = namespace(localName);
(namespace as RecordOf<typeof localNames, TValue>)[localName] = namespace(localName);
}
return namespace;
};

export const ACL = createNamespace('http://www.w3.org/ns/auth/acl#',
/**
* Creates a function that expands local names from the given base URI into strings,
* and exports the given local names as properties on the returned object.
*/
export const createUriNamespace = <T extends string>(baseUri: string, ...localNames: T[]):
Namespace<typeof localNames, string> =>
createNamespace(baseUri, (expanded): string => expanded, ...localNames);

/**
* Creates a function that expands local names from the given base URI into named nodes,
* and exports the given local names as properties on the returned object.
*/
export const createTermNamespace = <T extends string>(baseUri: string, ...localNames: T[]):
Namespace<typeof localNames, NamedNode> =>
createNamespace(baseUri, namedNode, ...localNames);

/**
* Creates a function that expands local names from the given base URI into string,
* and exports the given local names as properties on the returned object.
* Under the `terms` property, it exposes the expanded local names as named nodes.
*/
export const createUriAndTermNamespace = <T extends string>(baseUri: string, ...localNames: T[]):
Namespace<typeof localNames, string> & { terms: Namespace<typeof localNames, NamedNode> } =>
Object.assign(createUriNamespace(baseUri, ...localNames),
{ terms: createTermNamespace(baseUri, ...localNames) });

export const ACL = createUriAndTermNamespace('http://www.w3.org/ns/auth/acl#',
'accessTo',
'agent',
'agentClass',
Expand All @@ -41,49 +72,50 @@ export const ACL = createNamespace('http://www.w3.org/ns/auth/acl#',
'Control',
);

export const DCTERMS = createNamespace('http://purl.org/dc/terms/',
export const DC = createUriAndTermNamespace('http://purl.org/dc/terms/',
'modified',
);

export const FOAF = createNamespace('http://xmlns.com/foaf/0.1/',
export const FOAF = createUriAndTermNamespace('http://xmlns.com/foaf/0.1/',
'Agent',
'AuthenticatedAgent',
);

export const HTTP = createNamespace('urn:solid:http:',
export const HTTP = createUriAndTermNamespace('urn:solid:http:',
'location',
'slug',
);

export const LDP = createNamespace('http://www.w3.org/ns/ldp#',
export const LDP = createUriAndTermNamespace('http://www.w3.org/ns/ldp#',
'contains',

'BasicContainer',
'Container',
'Resource',
);

export const MA = createNamespace('http://www.w3.org/ns/ma-ont#',
export const MA = createUriAndTermNamespace('http://www.w3.org/ns/ma-ont#',
'format',
);

export const PIM = createNamespace('http://www.w3.org/ns/pim/space#',
export const PIM = createUriAndTermNamespace('http://www.w3.org/ns/pim/space#',
'Storage',
);

export const POSIX = createNamespace('http://www.w3.org/ns/posix/stat#',
export const POSIX = createUriAndTermNamespace('http://www.w3.org/ns/posix/stat#',
'mtime',
'size',
);

export const RDF = createNamespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#',
export const RDF = createUriAndTermNamespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#',
'type',
);

export const XSD = createNamespace('http://www.w3.org/2001/XMLSchema#',
export const XSD = createUriAndTermNamespace('http://www.w3.org/2001/XMLSchema#',
'dateTime',
'integer',
);

// Alias for most commonly used URI
export const CONTENT_TYPE = MA.format;
export const CONTENT_TYPE_TERM = MA.terms.format;
3 changes: 1 addition & 2 deletions test/unit/ldp/http/metadata/LinkRelMetadataWriter.test.ts
@@ -1,15 +1,14 @@
import { createResponse } from 'node-mocks-http';
import { LinkRelMetadataWriter } from '../../../../../src/ldp/http/metadata/LinkRelMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
import { toCachedNamedNode } from '../../../../../src/util/UriUtil';
import { LDP, RDF } from '../../../../../src/util/Vocabularies';

describe('A LinkRelMetadataWriter', (): void => {
const writer = new LinkRelMetadataWriter({ [RDF.type]: 'type', dummy: 'dummy' });

it('adds the correct link headers.', async(): Promise<void> => {
const response = createResponse();
const metadata = new RepresentationMetadata({ [RDF.type]: toCachedNamedNode(LDP.Resource), unused: 'text' });
const metadata = new RepresentationMetadata({ [RDF.type]: LDP.terms.Resource, unused: 'text' });
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ link: `<${LDP.Resource}>; rel="type"` });
});
Expand Down
17 changes: 8 additions & 9 deletions test/unit/storage/DataAccessorBasedStore.test.ts
Expand Up @@ -18,7 +18,6 @@ import type { Guarded } from '../../../src/util/GuardedStream';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
import * as quadUtil from '../../../src/util/QuadUtil';
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
import { toCachedNamedNode } from '../../../src/util/UriUtil';
import { CONTENT_TYPE, HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies';
import quad = DataFactory.quad;
import namedNode = DataFactory.namedNode;
Expand Down Expand Up @@ -160,7 +159,7 @@ describe('A DataAccessorBasedStore', (): void => {

it('errors when trying to create a container with non-RDF data.', async(): Promise<void> => {
const resourceID = { path: root };
representation.metadata.add(RDF.type, toCachedNamedNode(LDP.Container));
representation.metadata.add(RDF.type, LDP.terms.Container);
await expect(store.addResource(resourceID, representation)).rejects.toThrow(BadRequestHttpError);
});

Expand All @@ -169,7 +168,7 @@ describe('A DataAccessorBasedStore', (): void => {
const mock = jest.spyOn(quadUtil, 'parseQuads').mockImplementationOnce(async(): Promise<any> => {
throw 'apple';
});
representation.metadata.add(RDF.type, toCachedNamedNode(LDP.Container));
representation.metadata.add(RDF.type, LDP.terms.Container);
await expect(store.addResource(resourceID, representation)).rejects.toBe('apple');
mock.mockRestore();
});
Expand All @@ -186,7 +185,7 @@ describe('A DataAccessorBasedStore', (): void => {

it('can write containers.', async(): Promise<void> => {
const resourceID = { path: root };
representation.metadata.add(RDF.type, toCachedNamedNode(LDP.Container));
representation.metadata.add(RDF.type, LDP.terms.Container);
representation.metadata.contentType = 'text/turtle';
representation.data = guardedStreamFrom([ `<${`${root}resource/`}> a <coolContainer>.` ]);
const result = await store.addResource(resourceID, representation);
Expand Down Expand Up @@ -269,14 +268,14 @@ describe('A DataAccessorBasedStore', (): void => {
representation.metadata.identifier = DataFactory.namedNode(resourceID.path);
const newRepresentation = { ...representation };
newRepresentation.metadata = new RepresentationMetadata(representation.metadata);
newRepresentation.metadata.add(RDF.type, toCachedNamedNode(LDP.Container));
newRepresentation.metadata.add(RDF.type, LDP.terms.Container);
await expect(store.setRepresentation(resourceID, newRepresentation))
.rejects.toThrow(new ConflictHttpError('Input resource type does not match existing resource type.'));
});

it('will error if the ending slash does not match its resource type.', async(): Promise<void> => {
const resourceID = { path: `${root}resource` };
representation.metadata.add(RDF.type, toCachedNamedNode(LDP.Container));
representation.metadata.add(RDF.type, LDP.terms.Container);
await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(
new BadRequestHttpError('Containers should have a `/` at the end of their path, resources should not.'),
);
Expand All @@ -294,7 +293,7 @@ describe('A DataAccessorBasedStore', (): void => {

it('errors when trying to create a container with non-RDF data.', async(): Promise<void> => {
const resourceID = { path: `${root}container/` };
representation.metadata.add(RDF.type, toCachedNamedNode(LDP.Container));
representation.metadata.add(RDF.type, LDP.terms.Container);
await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(BadRequestHttpError);
});

Expand Down Expand Up @@ -332,7 +331,7 @@ describe('A DataAccessorBasedStore', (): void => {

it('errors when trying to create a container with containment triples.', async(): Promise<void> => {
const resourceID = { path: `${root}container/` };
representation.metadata.add(RDF.type, toCachedNamedNode(LDP.Container));
representation.metadata.add(RDF.type, LDP.terms.Container);
representation.metadata.contentType = 'text/turtle';
representation.metadata.identifier = DataFactory.namedNode(`${root}resource/`);
representation.data = guardedStreamFrom(
Expand Down Expand Up @@ -390,7 +389,7 @@ describe('A DataAccessorBasedStore', (): void => {
});

it('will error when deleting a root storage container.', async(): Promise<void> => {
representation.metadata.add(RDF.type, toCachedNamedNode(PIM.Storage));
representation.metadata.add(RDF.type, PIM.terms.Storage);
accessor.data[`${root}container`] = representation;
await expect(store.deleteResource({ path: `${root}container` }))
.rejects.toThrow(new MethodNotAllowedHttpError('Cannot delete a root storage container.'));
Expand Down

0 comments on commit ae06e99

Please sign in to comment.