mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Support writer prefixes.
Closes https://github.com/solid/community-server/issues/470
This commit is contained in:
parent
3b63786ae0
commit
87752ddf20
@ -59,6 +59,8 @@ module.exports = {
|
||||
'new-cap': 'off',
|
||||
// Necessary in constructor overloading
|
||||
'no-param-reassign': 'off',
|
||||
// Checked by @typescript-eslint/no-redeclare
|
||||
'no-redeclare': 'off',
|
||||
// Conflicts with external libraries
|
||||
'no-underscore-dangle': 'off',
|
||||
// Already checked by @typescript-eslint/no-unused-vars
|
||||
|
12
package-lock.json
generated
12
package-lock.json
generated
@ -6803,9 +6803,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"n3": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/n3/-/n3-1.6.4.tgz",
|
||||
"integrity": "sha512-qiiBhW2nJ59cfQzi0nvZs5tSXkXgDXedIy3zNNuKjTwE8Bcvv95DTFJpOY9geg6of5T7z6cg+ZWcaHIij3svrA==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/n3/-/n3-1.7.0.tgz",
|
||||
"integrity": "sha512-8R0Qj545WnVLQxOfxxyFKzOpO13hF3jhSMJfO0FNqvbsPZDiR9ZDmGGjXAlcoZDf/88OsCYd7rHML284vm1h6A==",
|
||||
"requires": {
|
||||
"queue-microtask": "^1.1.2",
|
||||
"readable-stream": "^3.6.0"
|
||||
@ -7629,9 +7629,9 @@
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||
},
|
||||
"queue-microtask": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.0.tgz",
|
||||
"integrity": "sha512-J95OVUiS4b8qqmpqhCodN8yPpHG2mpZUPQ8tDGyIY0VhM+kBHszOuvsMJVGNQ1OH2BnTFbqz45i+2jGpDw9H0w=="
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz",
|
||||
"integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg=="
|
||||
},
|
||||
"quick-lru": {
|
||||
"version": "4.0.1",
|
||||
|
@ -94,7 +94,7 @@
|
||||
"fetch-sparql-endpoint": "^1.8.0",
|
||||
"handlebars": "^4.7.6",
|
||||
"mime-types": "^2.1.27",
|
||||
"n3": "^1.6.4",
|
||||
"n3": "^1.7.0",
|
||||
"rdf-parse": "^1.5.0",
|
||||
"rdf-serialize": "^1.0.0",
|
||||
"rdf-terms": "^1.5.1",
|
||||
|
@ -1,7 +1,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/TermUtil';
|
||||
import { toSubjectTerm, toObjectTerm, toCachedNamedNode, isTerm } from '../../util/TermUtil';
|
||||
import { CONTENT_TYPE_TERM } from '../../util/Vocabularies';
|
||||
import type { ResourceIdentifier } from './ResourceIdentifier';
|
||||
import { isResourceIdentifier } from './ResourceIdentifier';
|
||||
@ -133,6 +133,18 @@ export class RepresentationMetadata {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param quads - Quad to add to the metadata.
|
||||
*/
|
||||
public addQuad(
|
||||
subject: NamedNode | BlankNode | string,
|
||||
predicate: NamedNode | string,
|
||||
object: NamedNode | BlankNode | Literal | string,
|
||||
): this {
|
||||
this.store.addQuad(toSubjectTerm(subject), toCachedNamedNode(predicate), toObjectTerm(object, true));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param quads - Quads to add to the metadata.
|
||||
*/
|
||||
@ -141,6 +153,18 @@ export class RepresentationMetadata {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param quads - Quad to remove from the metadata.
|
||||
*/
|
||||
public removeQuad(
|
||||
subject: NamedNode | BlankNode | string,
|
||||
predicate: NamedNode | string,
|
||||
object: NamedNode | BlankNode | Literal | string,
|
||||
): this {
|
||||
this.store.removeQuad(toSubjectTerm(subject), toCachedNamedNode(predicate), toObjectTerm(object, true));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param quads - Quads to remove from the metadata.
|
||||
*/
|
||||
@ -155,8 +179,7 @@ export class RepresentationMetadata {
|
||||
* @param object - Value to add.
|
||||
*/
|
||||
public add(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
|
||||
this.store.addQuad(this.id, toCachedNamedNode(predicate), toObjectTerm(object, true));
|
||||
return this;
|
||||
return this.addQuad(this.id, predicate, object);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -165,8 +188,7 @@ export class RepresentationMetadata {
|
||||
* @param object - Value to remove.
|
||||
*/
|
||||
public remove(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
|
||||
this.store.removeQuad(this.id, toCachedNamedNode(predicate), toObjectTerm(object, true));
|
||||
return this;
|
||||
return this.removeQuad(this.id, predicate, object);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { Readable } from 'stream';
|
||||
import { StreamWriter } from 'n3';
|
||||
import rdfSerializer from 'rdf-serialize';
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||
@ -8,7 +9,8 @@ import type {
|
||||
} from '../../ldp/representation/RepresentationPreferences';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { guardStream } from '../../util/GuardedStream';
|
||||
import { CONTENT_TYPE } from '../../util/Vocabularies';
|
||||
import { pipeSafely } from '../../util/StreamUtil';
|
||||
import { CONTENT_TYPE, PREFERRED_PREFIX_TERM } from '../../util/Vocabularies';
|
||||
import { matchingMediaTypes } from './ConversionUtil';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
@ -41,9 +43,20 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
|
||||
private async quadsToRdf(quads: Representation, { type }: RepresentationPreferences): Promise<Representation> {
|
||||
const contentType = matchingMediaTypes(type, await this.getOutputTypes())[0];
|
||||
const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType });
|
||||
let data: Readable;
|
||||
|
||||
// Use prefixes if possible (see https://github.com/rubensworks/rdf-serialize.js/issues/1)
|
||||
if (/(?:turtle|trig)$/u.test(contentType)) {
|
||||
const prefixes = Object.fromEntries(metadata.quads(null, PREFERRED_PREFIX_TERM, null)
|
||||
.map(({ subject, object }): [string, string] => [ object.value, subject.value ]));
|
||||
data = pipeSafely(quads.data, new StreamWriter({ format: contentType, prefixes }));
|
||||
// Otherwise, write without prefixes
|
||||
} else {
|
||||
data = rdfSerializer.serialize(quads.data, { contentType }) as Readable;
|
||||
}
|
||||
return {
|
||||
binary: true,
|
||||
data: guardStream(rdfSerializer.serialize(quads.data, { contentType }) as Readable),
|
||||
data: guardStream(data),
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DataFactory } from 'n3';
|
||||
import type { Literal, NamedNode, Term } from 'rdf-js';
|
||||
import type { NamedNode, Literal, Term } from 'rdf-js';
|
||||
import { CONTENT_TYPE_TERM } from './Vocabularies';
|
||||
|
||||
const { namedNode, literal } = DataFactory;
|
||||
@ -41,7 +41,10 @@ export function isTerm(input?: any): input is Term {
|
||||
* Converts a subject to a named node when needed.
|
||||
* @param subject - Subject to potentially transform.
|
||||
*/
|
||||
export function toSubjectTerm(subject: NamedNode | string): NamedNode {
|
||||
export function toSubjectTerm(subject: string): NamedNode;
|
||||
export function toSubjectTerm<T extends Term>(subject: T): T;
|
||||
export function toSubjectTerm<T extends Term>(subject: T | string): T | NamedNode;
|
||||
export function toSubjectTerm(subject: Term | string): Term {
|
||||
return typeof subject === 'string' ? namedNode(subject) : subject;
|
||||
}
|
||||
|
||||
@ -52,7 +55,10 @@ export const toPredicateTerm = toSubjectTerm;
|
||||
* @param object - Object to potentially transform.
|
||||
* @param preferLiteral - Whether strings are converted to literals or named nodes.
|
||||
*/
|
||||
export function toObjectTerm<T extends Term>(object: T | string, preferLiteral = false): T {
|
||||
export function toObjectTerm(object: string, preferLiteral?: boolean): NamedNode;
|
||||
export function toObjectTerm<T extends Term>(object: T, preferLiteral?: boolean): T;
|
||||
export function toObjectTerm<T extends Term>(object: T | string, preferLiteral?: boolean): T | NamedNode;
|
||||
export function toObjectTerm(object: Term | string, preferLiteral = false): Term {
|
||||
if (typeof object === 'string') {
|
||||
return (preferLiteral ? literal(object) : namedNode(object)) as any;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { namedNode } from '@rdfjs/data-model';
|
||||
import type { NamedNode } from 'rdf-js';
|
||||
|
||||
type PrefixResolver<T> = (localName: string) => T;
|
||||
type PrefixResolver<T> = (localName?: string) => T;
|
||||
type RecordOf<TKey extends any[], TValue> = Record<TKey[number], TValue>;
|
||||
|
||||
export type Namespace<TKey extends any[], TValue> =
|
||||
@ -19,7 +19,7 @@ export function createNamespace<TKey extends string, TValue>(
|
||||
Namespace<typeof localNames, TValue> {
|
||||
// Create a function that expands local names
|
||||
const expanded = {} as Record<string, TValue>;
|
||||
const namespace = ((localName: string): TValue => {
|
||||
const namespace = ((localName = ''): TValue => {
|
||||
if (!(localName in expanded)) {
|
||||
expanded[localName] = toValue(`${baseUri}${localName}`);
|
||||
}
|
||||
@ -114,11 +114,17 @@ export const RDF = createUriAndTermNamespace('http://www.w3.org/1999/02/22-rdf-s
|
||||
'type',
|
||||
);
|
||||
|
||||
export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/',
|
||||
'preferredNamespacePrefix',
|
||||
);
|
||||
|
||||
export const XSD = createUriAndTermNamespace('http://www.w3.org/2001/XMLSchema#',
|
||||
'dateTime',
|
||||
'integer',
|
||||
);
|
||||
|
||||
// Alias for most commonly used URI
|
||||
// Alias for commonly used types
|
||||
export const CONTENT_TYPE = MA.format;
|
||||
export const CONTENT_TYPE_TERM = MA.terms.format;
|
||||
export const PREFERRED_PREFIX = VANN.preferredNamespacePrefix;
|
||||
export const PREFERRED_PREFIX_TERM = VANN.terms.preferredNamespacePrefix;
|
||||
|
@ -107,12 +107,12 @@ describe('An integrated AuthenticatedLdpHandler', (): void => {
|
||||
[],
|
||||
);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response._getBuffer().toString()).toContain(
|
||||
expect(response._getData()).toContain(
|
||||
'<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.',
|
||||
);
|
||||
expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`);
|
||||
const parser = new Parser();
|
||||
const triples = parser.parse(response._getBuffer().toString());
|
||||
const triples = parser.parse(response._getData());
|
||||
expect(triples).toBeRdfIsomorphic([
|
||||
quad(
|
||||
namedNode('http://test.com/s2'),
|
||||
|
@ -8,12 +8,16 @@ import type { RepresentationPreferences } from '../../../../src/ldp/representati
|
||||
import type { 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/Vocabularies';
|
||||
import { CONTENT_TYPE, DC, PREFERRED_PREFIX_TERM } from '../../../../src/util/Vocabularies';
|
||||
|
||||
describe('A QuadToRdfConverter', (): void => {
|
||||
const converter = new QuadToRdfConverter();
|
||||
const identifier: ResourceIdentifier = { path: 'path' };
|
||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: INTERNAL_QUADS });
|
||||
let metadata: RepresentationMetadata;
|
||||
|
||||
beforeEach((): void => {
|
||||
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: INTERNAL_QUADS });
|
||||
});
|
||||
|
||||
it('supports parsing quads.', async(): Promise<void> => {
|
||||
await expect(new QuadToRdfConverter().getInputTypes())
|
||||
@ -71,6 +75,33 @@ describe('A QuadToRdfConverter', (): void => {
|
||||
);
|
||||
});
|
||||
|
||||
it('converts quads with prefixes to turtle.', async(): Promise<void> => {
|
||||
metadata.addQuad(DC.terms(), PREFERRED_PREFIX_TERM, 'dc');
|
||||
metadata.addQuad('http://test.com/', PREFERRED_PREFIX_TERM, 'test');
|
||||
const representation = {
|
||||
data: streamifyArray([ triple(
|
||||
namedNode('http://test.com/s'),
|
||||
DC.terms.modified,
|
||||
namedNode('http://test.com/o'),
|
||||
) ]),
|
||||
metadata,
|
||||
} as Representation;
|
||||
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
|
||||
const result = await converter.handle({ identifier, representation, preferences });
|
||||
expect(result).toMatchObject({
|
||||
binary: true,
|
||||
metadata: expect.any(RepresentationMetadata),
|
||||
});
|
||||
expect(result.metadata.contentType).toEqual('text/turtle');
|
||||
await expect(stringifyStream(result.data)).resolves.toEqual(
|
||||
`@prefix dc: <http://purl.org/dc/terms/>.
|
||||
@prefix test: <http://test.com/>.
|
||||
|
||||
test:s dc:modified test:o.
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
it('converts quads to JSON-LD.', async(): Promise<void> => {
|
||||
metadata.contentType = INTERNAL_QUADS;
|
||||
const representation = {
|
||||
|
@ -3,6 +3,10 @@ import { LDP } from '../../../src/util/Vocabularies';
|
||||
|
||||
describe('Vocabularies', (): void => {
|
||||
describe('LDP', (): void => {
|
||||
it('can return its own URI.', (): void => {
|
||||
expect(LDP()).toBe('http://www.w3.org/ns/ldp#');
|
||||
});
|
||||
|
||||
it('can create new properties.', (): void => {
|
||||
expect(LDP('new')).toBe('http://www.w3.org/ns/ldp#new');
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user