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',
|
'new-cap': 'off',
|
||||||
// Necessary in constructor overloading
|
// Necessary in constructor overloading
|
||||||
'no-param-reassign': 'off',
|
'no-param-reassign': 'off',
|
||||||
|
// Checked by @typescript-eslint/no-redeclare
|
||||||
|
'no-redeclare': 'off',
|
||||||
// Conflicts with external libraries
|
// Conflicts with external libraries
|
||||||
'no-underscore-dangle': 'off',
|
'no-underscore-dangle': 'off',
|
||||||
// Already checked by @typescript-eslint/no-unused-vars
|
// Already checked by @typescript-eslint/no-unused-vars
|
||||||
|
12
package-lock.json
generated
12
package-lock.json
generated
@ -6803,9 +6803,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"n3": {
|
"n3": {
|
||||||
"version": "1.6.4",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/n3/-/n3-1.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/n3/-/n3-1.7.0.tgz",
|
||||||
"integrity": "sha512-qiiBhW2nJ59cfQzi0nvZs5tSXkXgDXedIy3zNNuKjTwE8Bcvv95DTFJpOY9geg6of5T7z6cg+ZWcaHIij3svrA==",
|
"integrity": "sha512-8R0Qj545WnVLQxOfxxyFKzOpO13hF3jhSMJfO0FNqvbsPZDiR9ZDmGGjXAlcoZDf/88OsCYd7rHML284vm1h6A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"queue-microtask": "^1.1.2",
|
"queue-microtask": "^1.1.2",
|
||||||
"readable-stream": "^3.6.0"
|
"readable-stream": "^3.6.0"
|
||||||
@ -7629,9 +7629,9 @@
|
|||||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||||
},
|
},
|
||||||
"queue-microtask": {
|
"queue-microtask": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz",
|
||||||
"integrity": "sha512-J95OVUiS4b8qqmpqhCodN8yPpHG2mpZUPQ8tDGyIY0VhM+kBHszOuvsMJVGNQ1OH2BnTFbqz45i+2jGpDw9H0w=="
|
"integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg=="
|
||||||
},
|
},
|
||||||
"quick-lru": {
|
"quick-lru": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
"fetch-sparql-endpoint": "^1.8.0",
|
"fetch-sparql-endpoint": "^1.8.0",
|
||||||
"handlebars": "^4.7.6",
|
"handlebars": "^4.7.6",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"n3": "^1.6.4",
|
"n3": "^1.7.0",
|
||||||
"rdf-parse": "^1.5.0",
|
"rdf-parse": "^1.5.0",
|
||||||
"rdf-serialize": "^1.0.0",
|
"rdf-serialize": "^1.0.0",
|
||||||
"rdf-terms": "^1.5.1",
|
"rdf-terms": "^1.5.1",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { DataFactory, Store } from 'n3';
|
import { DataFactory, Store } from 'n3';
|
||||||
import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';
|
import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
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 { CONTENT_TYPE_TERM } from '../../util/Vocabularies';
|
||||||
import type { ResourceIdentifier } from './ResourceIdentifier';
|
import type { ResourceIdentifier } from './ResourceIdentifier';
|
||||||
import { isResourceIdentifier } from './ResourceIdentifier';
|
import { isResourceIdentifier } from './ResourceIdentifier';
|
||||||
@ -133,6 +133,18 @@ export class RepresentationMetadata {
|
|||||||
return this;
|
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.
|
* @param quads - Quads to add to the metadata.
|
||||||
*/
|
*/
|
||||||
@ -141,6 +153,18 @@ export class RepresentationMetadata {
|
|||||||
return this;
|
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.
|
* @param quads - Quads to remove from the metadata.
|
||||||
*/
|
*/
|
||||||
@ -155,8 +179,7 @@ export class RepresentationMetadata {
|
|||||||
* @param object - Value to add.
|
* @param object - Value to add.
|
||||||
*/
|
*/
|
||||||
public add(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
|
public add(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
|
||||||
this.store.addQuad(this.id, toCachedNamedNode(predicate), toObjectTerm(object, true));
|
return this.addQuad(this.id, predicate, object);
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -165,8 +188,7 @@ export class RepresentationMetadata {
|
|||||||
* @param object - Value to remove.
|
* @param object - Value to remove.
|
||||||
*/
|
*/
|
||||||
public remove(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
|
public remove(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
|
||||||
this.store.removeQuad(this.id, toCachedNamedNode(predicate), toObjectTerm(object, true));
|
return this.removeQuad(this.id, predicate, object);
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
|
import { StreamWriter } from 'n3';
|
||||||
import rdfSerializer from 'rdf-serialize';
|
import rdfSerializer from 'rdf-serialize';
|
||||||
import type { Representation } from '../../ldp/representation/Representation';
|
import type { Representation } from '../../ldp/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||||
@ -8,7 +9,8 @@ import type {
|
|||||||
} from '../../ldp/representation/RepresentationPreferences';
|
} from '../../ldp/representation/RepresentationPreferences';
|
||||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||||
import { guardStream } from '../../util/GuardedStream';
|
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 { matchingMediaTypes } from './ConversionUtil';
|
||||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||||
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||||
@ -41,9 +43,20 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
|
|||||||
private async quadsToRdf(quads: Representation, { type }: RepresentationPreferences): Promise<Representation> {
|
private async quadsToRdf(quads: Representation, { type }: RepresentationPreferences): Promise<Representation> {
|
||||||
const contentType = matchingMediaTypes(type, await this.getOutputTypes())[0];
|
const contentType = matchingMediaTypes(type, await this.getOutputTypes())[0];
|
||||||
const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType });
|
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 {
|
return {
|
||||||
binary: true,
|
binary: true,
|
||||||
data: guardStream(rdfSerializer.serialize(quads.data, { contentType }) as Readable),
|
data: guardStream(data),
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { DataFactory } from 'n3';
|
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';
|
import { CONTENT_TYPE_TERM } from './Vocabularies';
|
||||||
|
|
||||||
const { namedNode, literal } = DataFactory;
|
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.
|
* Converts a subject to a named node when needed.
|
||||||
* @param subject - Subject to potentially transform.
|
* @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;
|
return typeof subject === 'string' ? namedNode(subject) : subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +55,10 @@ export const toPredicateTerm = toSubjectTerm;
|
|||||||
* @param object - Object to potentially transform.
|
* @param object - Object to potentially transform.
|
||||||
* @param preferLiteral - Whether strings are converted to literals or named nodes.
|
* @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') {
|
if (typeof object === 'string') {
|
||||||
return (preferLiteral ? literal(object) : namedNode(object)) as any;
|
return (preferLiteral ? literal(object) : namedNode(object)) as any;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { namedNode } from '@rdfjs/data-model';
|
import { namedNode } from '@rdfjs/data-model';
|
||||||
import type { NamedNode } from 'rdf-js';
|
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>;
|
type RecordOf<TKey extends any[], TValue> = Record<TKey[number], TValue>;
|
||||||
|
|
||||||
export type Namespace<TKey extends any[], TValue> =
|
export type Namespace<TKey extends any[], TValue> =
|
||||||
@ -19,7 +19,7 @@ export function createNamespace<TKey extends string, TValue>(
|
|||||||
Namespace<typeof localNames, TValue> {
|
Namespace<typeof localNames, TValue> {
|
||||||
// Create a function that expands local names
|
// Create a function that expands local names
|
||||||
const expanded = {} as Record<string, TValue>;
|
const expanded = {} as Record<string, TValue>;
|
||||||
const namespace = ((localName: string): TValue => {
|
const namespace = ((localName = ''): TValue => {
|
||||||
if (!(localName in expanded)) {
|
if (!(localName in expanded)) {
|
||||||
expanded[localName] = toValue(`${baseUri}${localName}`);
|
expanded[localName] = toValue(`${baseUri}${localName}`);
|
||||||
}
|
}
|
||||||
@ -114,11 +114,17 @@ export const RDF = createUriAndTermNamespace('http://www.w3.org/1999/02/22-rdf-s
|
|||||||
'type',
|
'type',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/',
|
||||||
|
'preferredNamespacePrefix',
|
||||||
|
);
|
||||||
|
|
||||||
export const XSD = createUriAndTermNamespace('http://www.w3.org/2001/XMLSchema#',
|
export const XSD = createUriAndTermNamespace('http://www.w3.org/2001/XMLSchema#',
|
||||||
'dateTime',
|
'dateTime',
|
||||||
'integer',
|
'integer',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Alias for most commonly used URI
|
// Alias for commonly used types
|
||||||
export const CONTENT_TYPE = MA.format;
|
export const CONTENT_TYPE = MA.format;
|
||||||
export const CONTENT_TYPE_TERM = MA.terms.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.statusCode).toBe(200);
|
||||||
expect(response._getBuffer().toString()).toContain(
|
expect(response._getData()).toContain(
|
||||||
'<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.',
|
'<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.',
|
||||||
);
|
);
|
||||||
expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`);
|
expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`);
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
const triples = parser.parse(response._getBuffer().toString());
|
const triples = parser.parse(response._getData());
|
||||||
expect(triples).toBeRdfIsomorphic([
|
expect(triples).toBeRdfIsomorphic([
|
||||||
quad(
|
quad(
|
||||||
namedNode('http://test.com/s2'),
|
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 type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||||
import { QuadToRdfConverter } from '../../../../src/storage/conversion/QuadToRdfConverter';
|
import { QuadToRdfConverter } from '../../../../src/storage/conversion/QuadToRdfConverter';
|
||||||
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
|
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 => {
|
describe('A QuadToRdfConverter', (): void => {
|
||||||
const converter = new QuadToRdfConverter();
|
const converter = new QuadToRdfConverter();
|
||||||
const identifier: ResourceIdentifier = { path: 'path' };
|
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> => {
|
it('supports parsing quads.', async(): Promise<void> => {
|
||||||
await expect(new QuadToRdfConverter().getInputTypes())
|
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> => {
|
it('converts quads to JSON-LD.', async(): Promise<void> => {
|
||||||
metadata.contentType = INTERNAL_QUADS;
|
metadata.contentType = INTERNAL_QUADS;
|
||||||
const representation = {
|
const representation = {
|
||||||
|
@ -3,6 +3,10 @@ import { LDP } from '../../../src/util/Vocabularies';
|
|||||||
|
|
||||||
describe('Vocabularies', (): void => {
|
describe('Vocabularies', (): void => {
|
||||||
describe('LDP', (): 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 => {
|
it('can create new properties.', (): void => {
|
||||||
expect(LDP('new')).toBe('http://www.w3.org/ns/ldp#new');
|
expect(LDP('new')).toBe('http://www.w3.org/ns/ldp#new');
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user