Skip to content

Commit

Permalink
feat: Update RepresentationMetadata to store triples
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Sep 16, 2020
1 parent 1dd140a commit 76319ba
Show file tree
Hide file tree
Showing 37 changed files with 575 additions and 247 deletions.
37 changes: 33 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"yargs": "^15.4.1"
},
"devDependencies": {
"@microsoft/tsdoc-config": "^0.13.6",
"@types/jest": "^26.0.0",
"@types/rimraf": "^3.0.0",
"@types/supertest": "^2.0.10",
Expand Down
13 changes: 7 additions & 6 deletions src/init/Setup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import streamifyArray from 'streamify-array';
import { AclManager } from '../authorization/AclManager';
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import { ExpressHttpServer } from '../server/ExpressHttpServer';
import { ResourceStore } from '../storage/ResourceStore';
import { TEXT_TURTLE } from '../util/ContentTypes';
import { CONTENT_TYPE } from '../util/MetadataTypes';

/**
* Invokes all logic to setup a server.
Expand Down Expand Up @@ -48,16 +50,15 @@ 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);
await this.store.setRepresentation(
await this.aclManager.getAcl({ path: this.base }),
aclId,
{
binary: true,
data: streamifyArray([ acl ]),
metadata: {
raw: [],
profiles: [],
contentType: TEXT_TURTLE,
},
metadata,
},
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/ldp/http/BasicResponseWriter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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';

Expand Down Expand Up @@ -29,7 +30,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.contentType ?? 'text/plain';
const contentType = input.result.body.metadata.get(CONTENT_TYPE)?.value ?? 'text/plain';
input.response.setHeader('content-type', contentType);
input.result.body.data.pipe(input.response);
}
Expand Down
17 changes: 6 additions & 11 deletions src/ldp/http/RawBodyParser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HttpRequest } from '../../server/HttpRequest';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { CONTENT_TYPE, SLUG, TYPE } from '../../util/MetadataTypes';
import { Representation } from '../representation/Representation';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import { BodyParser } from './BodyParser';
Expand Down Expand Up @@ -39,35 +40,29 @@ export class RawBodyParser extends BodyParser {
private parseMetadata(input: HttpRequest): RepresentationMetadata {
const contentType = /^[^;]*/u.exec(input.headers['content-type']!)![0];

const metadata: RepresentationMetadata = {
raw: [],
contentType,
};
const metadata: RepresentationMetadata = new RepresentationMetadata();
metadata.set(CONTENT_TYPE, contentType);

const { link, slug } = input.headers;

if (slug) {
if (Array.isArray(slug)) {
throw new UnsupportedHttpError('At most 1 slug header is allowed.');
}
metadata.slug = slug;
metadata.set(SLUG, slug);
}

// There are similarities here to Accept header parsing so that library should become more generic probably
if (link) {
metadata.linkRel = {};
const linkArray = Array.isArray(link) ? link : [ link ];
const parsedLinks = linkArray.map((entry): { url: string; rel: string } => {
const [ , url, rest ] = /^<([^>]*)>(.*)$/u.exec(entry) ?? [];
const [ , rel ] = /^ *; *rel="(.*)"$/u.exec(rest) ?? [];
return { url, rel };
});
parsedLinks.forEach((entry): void => {
if (entry.rel) {
if (!metadata.linkRel![entry.rel]) {
metadata.linkRel![entry.rel] = new Set();
}
metadata.linkRel![entry.rel].add(entry.url);
if (entry.rel === 'type') {
metadata.set(TYPE, entry.url);
}
});
}
Expand Down
11 changes: 6 additions & 5 deletions src/ldp/http/SparqlUpdateBodyParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ 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 { readableToString } from '../../util/Util';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import { BodyParser } from './BodyParser';
import { SparqlUpdatePatch } from './SparqlUpdatePatch';

Expand Down Expand Up @@ -33,16 +35,15 @@ 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');

// Prevent body from being requested again
return {
algebra,
binary: true,
data: dataCopy,
metadata: {
raw: [],
profiles: [],
contentType: 'application/sparql-update',
},
metadata,
};
} catch (error) {
throw new UnsupportedHttpError(error);
Expand Down
133 changes: 108 additions & 25 deletions src/ldp/representation/RepresentationMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,130 @@
/**
* Contains metadata relevant to a representation.
*/
import type { Quad } from 'rdf-js';
import { quad as createQuad, literal, namedNode } from '@rdfjs/data-model';
import { Store } from 'n3';
import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';

/**
* Metadata corresponding to a {@link Representation}.
* Stores the metadata triples and provides methods for easy access.
*/
export interface RepresentationMetadata {
export class RepresentationMetadata {
private store: Store;
private id: NamedNode | BlankNode;

/**
* All metadata triples of the resource.
* @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
*
* `@ignored` tags are necessary for Components-Generator.js
*/
raw: Quad[];
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;
}
} else {
this.id = this.store.createBlankNode();
}
}

/**
* Optional metadata profiles.
* @returns All metadata quads.
*/
profiles?: string[];
public quads(): Quad[] {
return this.store.getQuads(null, null, null, null);
}

/**
* Optional size of the representation.
* Identifier of the resource this metadata is relevant to.
* Will update all relevant triples if this value gets changed.
*/
byteSize?: number;
public get identifier(): NamedNode | BlankNode {
return this.id;
}

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;
}

/**
* Optional content type of the representation.
* @param quads - Quads to add to the metadata.
*/
contentType?: string;
public addQuads(quads: Quad[]): void {
this.store.addQuads(quads);
}

/**
* Optional encoding of the representation.
* @param quads - Quads to remove from the metadata.
*/
encoding?: string;
public removeQuads(quads: Quad[]): void {
this.store.removeQuads(quads);
}

/**
* Optional language of the representation.
* Adds a value linked to the identifier. Strings get converted to literals.
* @param predicate - Predicate linking identifier to value.
* @param object - Value to add.
*/
language?: string;
public add(predicate: NamedNode, object: NamedNode | Literal | string): void {
this.store.addQuad(this.id, predicate, typeof object === 'string' ? literal(object) : object);
}

/**
* Optional timestamp of the representation.
* Removes the given value from the metadata. Strings get converted to literals.
* @param predicate - Predicate linking identifier to value.
* @param object - Value to remove.
*/
dateTime?: Date;
public remove(predicate: NamedNode, object: NamedNode | Literal | string): void {
this.store.removeQuad(this.id, predicate, typeof object === 'string' ? literal(object) : object);
}

/**
* Optional link relationships of the representation.
* Removes all values linked through the given predicate.
* @param predicate - Predicate to remove.
*/
linkRel?: { [id: string]: Set<string> };
public removeAll(predicate: NamedNode): void {
this.removeQuads(this.store.getQuads(this.id, predicate, null, null));
}

/**
* @param predicate - Predicate to get the value for.
*
* @throws Error
* If there are multiple matching values.
*
* @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) {
return;
}
if (quads.length > 1) {
throw new Error(`Multiple results for ${predicate.value}`);
}
return quads[0].object;
}

/**
* Optional slug of the representation.
* Used to suggest the URI for the resource created.
* Sets the value for the given predicate, removing all other instances.
* @param predicate - Predicate linking to the value.
* @param object - Value to set.
*/
slug?: string;
public set(predicate: NamedNode, object: NamedNode | Literal | string): void {
this.removeAll(predicate);
this.add(predicate, object);
}
}
Loading

0 comments on commit 76319ba

Please sign in to comment.