@lde/fastify-rdf 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/hydra-error.d.ts +10 -0
- package/dist/hydra-error.d.ts.map +1 -0
- package/dist/hydra-error.js +33 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +49 -27
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,35 @@ await app.register(fastifyRdf, { parseRdf: true });
|
|
|
92
92
|
|
|
93
93
|
Routes without per-route or plugin-level `parseRdf` get JSON fallback for `application/ld+json` (parsed as plain JSON) and 415 Unsupported Media Type for other RDF content types.
|
|
94
94
|
|
|
95
|
+
### Hydra Error Responses with `reply.sendHydraError()`
|
|
96
|
+
|
|
97
|
+
Send [Hydra](https://www.hydra-cg.com/spec/latest/core/) error responses with content negotiation. This maps `error.message` to `hydra:title` and `error.cause` (when it is a string) to `hydra:description`:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
app.get('/resource', async (request, reply) => {
|
|
101
|
+
const error = new Error('Not Found', {
|
|
102
|
+
cause: 'The requested dataset was not found',
|
|
103
|
+
}) as Error & { statusCode: number };
|
|
104
|
+
error.statusCode = 404;
|
|
105
|
+
return reply.sendHydraError(error);
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The status code is taken from `error.statusCode` (standard in Fastify and http-errors), defaulting to 500.
|
|
110
|
+
|
|
111
|
+
For `Accept: application/ld+json`, the response is compact JSON-LD — no `jsonld` dependency needed:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"@context": "http://www.w3.org/ns/hydra/core#",
|
|
116
|
+
"@type": "Error",
|
|
117
|
+
"title": "Not Found",
|
|
118
|
+
"description": "The requested dataset was not found"
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For all other RDF formats (Turtle, N-Triples, etc.), the error is serialised through the standard RDF pipeline.
|
|
123
|
+
|
|
95
124
|
### Custom Default Content Type
|
|
96
125
|
|
|
97
126
|
By default, the plugin uses `text/turtle` when no `Accept` header is provided. You can change this:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Store } from 'n3';
|
|
2
|
+
/**
|
|
3
|
+
* Serialize a Hydra error as compact JSON-LD without needing the `jsonld` dependency.
|
|
4
|
+
*/
|
|
5
|
+
export declare function serializeHydraErrorAsJsonLd(title: string, description?: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Create an N3 Store with Hydra error triples.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createHydraErrorDataset(title: string, description?: string): Store;
|
|
10
|
+
//# sourceMappingURL=hydra-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydra-error.d.ts","sourceRoot":"","sources":["../src/hydra-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,EAAE,MAAM,IAAI,CAAC;AAWxC;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAUR;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,KAAK,CAWP"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DataFactory, Store } from 'n3';
|
|
2
|
+
const { namedNode, blankNode, literal } = DataFactory;
|
|
3
|
+
const RDF_TYPE = namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type');
|
|
4
|
+
const HYDRA_ERROR = namedNode('http://www.w3.org/ns/hydra/core#Error');
|
|
5
|
+
const HYDRA_TITLE = namedNode('http://www.w3.org/ns/hydra/core#title');
|
|
6
|
+
const HYDRA_DESCRIPTION = namedNode('http://www.w3.org/ns/hydra/core#description');
|
|
7
|
+
/**
|
|
8
|
+
* Serialize a Hydra error as compact JSON-LD without needing the `jsonld` dependency.
|
|
9
|
+
*/
|
|
10
|
+
export function serializeHydraErrorAsJsonLd(title, description) {
|
|
11
|
+
const obj = {
|
|
12
|
+
'@context': 'http://www.w3.org/ns/hydra/core#',
|
|
13
|
+
'@type': 'Error',
|
|
14
|
+
title,
|
|
15
|
+
};
|
|
16
|
+
if (description !== undefined) {
|
|
17
|
+
obj['description'] = description;
|
|
18
|
+
}
|
|
19
|
+
return JSON.stringify(obj);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create an N3 Store with Hydra error triples.
|
|
23
|
+
*/
|
|
24
|
+
export function createHydraErrorDataset(title, description) {
|
|
25
|
+
const store = new Store();
|
|
26
|
+
const subject = blankNode();
|
|
27
|
+
store.add(DataFactory.quad(subject, RDF_TYPE, HYDRA_ERROR));
|
|
28
|
+
store.add(DataFactory.quad(subject, HYDRA_TITLE, literal(title)));
|
|
29
|
+
if (description !== undefined) {
|
|
30
|
+
store.add(DataFactory.quad(subject, HYDRA_DESCRIPTION, literal(description)));
|
|
31
|
+
}
|
|
32
|
+
return store;
|
|
33
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { fastifyRdf } from './plugin.js';
|
|
2
2
|
export { DEFAULT_CONTENT_TYPE, type FastifyRdfOptions, type RdfData, } from './types.js';
|
|
3
|
+
export { createHydraErrorDataset } from './hydra-error.js';
|
|
3
4
|
export { fastifyRdf as default } from './plugin.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,OAAO,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,OAAO,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAQ7E,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAQ7E,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,YAAY,CAAC;AAwIpB;;;;;;;;;GASG;AACH,iBAAe,gBAAgB,CAC7B,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CAqFf;AAED;;GAEG;AACH,eAAO,MAAM,UAAU,yBAGrB,CAAC"}
|
package/dist/plugin.js
CHANGED
|
@@ -5,6 +5,7 @@ import { rdfParser } from 'rdf-parse';
|
|
|
5
5
|
import { Store } from 'n3';
|
|
6
6
|
import { Readable } from 'node:stream';
|
|
7
7
|
import { DEFAULT_CONTENT_TYPE, } from './types.js';
|
|
8
|
+
import { serializeHydraErrorAsJsonLd, createHydraErrorDataset, } from './hydra-error.js';
|
|
8
9
|
/**
|
|
9
10
|
* Collect a readable stream into a string.
|
|
10
11
|
*/
|
|
@@ -57,45 +58,53 @@ async function streamToDataset(stream) {
|
|
|
57
58
|
*/
|
|
58
59
|
async function registerRdfParsers(server, parseAll) {
|
|
59
60
|
const contentTypes = (await rdfParser.getContentTypes()).filter((type) => type !== 'application/json');
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const jsonLdType = 'application/ld+json';
|
|
62
|
+
// JSON-LD is JSON.parse'd eagerly so the body is a plain object when
|
|
63
|
+
// Fastify runs preValidation (schema checks happen before preHandler).
|
|
64
|
+
// Other RDF types are stored as a raw Buffer for the preHandler hook.
|
|
65
|
+
server.addContentTypeParser(contentTypes, function (request, payload, done) {
|
|
66
|
+
const isJsonLd = request.headers['content-type']?.split(';')[0].trim() === jsonLdType;
|
|
63
67
|
const chunks = [];
|
|
64
68
|
payload.on('data', (chunk) => chunks.push(chunk));
|
|
65
|
-
payload.on('end', () =>
|
|
69
|
+
payload.on('end', () => {
|
|
70
|
+
if (!isJsonLd)
|
|
71
|
+
return done(null, Buffer.concat(chunks));
|
|
72
|
+
try {
|
|
73
|
+
done(null, JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
done(err);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
66
79
|
payload.on('error', done);
|
|
67
80
|
});
|
|
68
81
|
server.addHook('preHandler', async (request) => {
|
|
69
|
-
|
|
70
|
-
if (!request.body ||
|
|
71
|
-
!Buffer.isBuffer(request.body) ||
|
|
72
|
-
!request.headers['content-type']) {
|
|
82
|
+
if (!request.body || !request.headers['content-type'])
|
|
73
83
|
return;
|
|
74
|
-
}
|
|
75
84
|
const contentType = request.headers['content-type'].split(';')[0].trim();
|
|
76
|
-
if (!contentTypes.includes(contentType))
|
|
85
|
+
if (!contentTypes.includes(contentType))
|
|
77
86
|
return;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
request.body = await streamToDataset(quadStream);
|
|
84
|
-
}
|
|
85
|
-
catch (cause) {
|
|
86
|
-
const error = new Error('Invalid RDF body', {
|
|
87
|
-
cause,
|
|
88
|
-
});
|
|
89
|
-
error.statusCode = 400;
|
|
87
|
+
if (!(parseAll || request.routeOptions.config.parseRdf)) {
|
|
88
|
+
// Not a parseRdf route: JSON-LD is already parsed; reject other RDF.
|
|
89
|
+
if (contentType !== jsonLdType) {
|
|
90
|
+
const error = new Error(`Unsupported Media Type: ${contentType}`);
|
|
91
|
+
error.statusCode = 415;
|
|
90
92
|
throw error;
|
|
91
93
|
}
|
|
94
|
+
return;
|
|
92
95
|
}
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
try {
|
|
97
|
+
// JSON-LD body is already an object; re-stringify for rdf-parse.
|
|
98
|
+
const raw = Buffer.isBuffer(request.body)
|
|
99
|
+
? request.body
|
|
100
|
+
: Buffer.from(JSON.stringify(request.body));
|
|
101
|
+
request.body = await streamToDataset(rdfParser.parse(Readable.from(raw), { contentType }));
|
|
95
102
|
}
|
|
96
|
-
|
|
97
|
-
const error = new Error(
|
|
98
|
-
|
|
103
|
+
catch (cause) {
|
|
104
|
+
const error = new Error('Invalid RDF body', {
|
|
105
|
+
cause,
|
|
106
|
+
});
|
|
107
|
+
error.statusCode = 400;
|
|
99
108
|
throw error;
|
|
100
109
|
}
|
|
101
110
|
});
|
|
@@ -144,6 +153,19 @@ async function fastifyRdfPlugin(server, options) {
|
|
|
144
153
|
return this.send(serialized);
|
|
145
154
|
});
|
|
146
155
|
}
|
|
156
|
+
server.decorateReply('sendHydraError', async function (error) {
|
|
157
|
+
this.status(error.statusCode ?? 500);
|
|
158
|
+
const title = error.message;
|
|
159
|
+
const description = typeof error.cause === 'string' ? error.cause : undefined;
|
|
160
|
+
const contentType = negotiateContentType(this.request, supportedContentTypes, defaultContentType);
|
|
161
|
+
if (contentType === 'application/ld+json') {
|
|
162
|
+
this.type('application/ld+json');
|
|
163
|
+
return this.send(serializeHydraErrorAsJsonLd(title, description));
|
|
164
|
+
}
|
|
165
|
+
const dataset = createHydraErrorDataset(title, description);
|
|
166
|
+
this.type(contentType);
|
|
167
|
+
return this.send(await serializeRdfToString(dataset, contentType));
|
|
168
|
+
});
|
|
147
169
|
await registerRdfParsers(server, options.parseRdf ?? false);
|
|
148
170
|
}
|
|
149
171
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,14 @@ declare module 'fastify' {
|
|
|
38
38
|
* @returns The data (when overrideSend is enabled) or Promise<FastifyReply>
|
|
39
39
|
*/
|
|
40
40
|
sendRdf(data: RdfData): RdfData | Promise<FastifyReply>;
|
|
41
|
+
/**
|
|
42
|
+
* Send a Hydra error response with content negotiation.
|
|
43
|
+
* Uses `error.message` as `hydra:title` and `error.cause` (if a string) as `hydra:description`.
|
|
44
|
+
* Status code defaults to `error.statusCode` or 500.
|
|
45
|
+
*/
|
|
46
|
+
sendHydraError(error: Error & {
|
|
47
|
+
statusCode?: number;
|
|
48
|
+
}): Promise<FastifyReply>;
|
|
41
49
|
}
|
|
42
50
|
interface FastifyContextConfig {
|
|
43
51
|
parseRdf?: boolean;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,oBAAoB,gBAAgB,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;AAE3C,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,YAAY;QACpB;;;;WAIG;QACH,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,oBAAoB,gBAAgB,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;AAE3C,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,YAAY;QACpB;;;;WAIG;QACH,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;QAExD;;;;WAIG;QACH,cAAc,CACZ,KAAK,EAAE,KAAK,GAAG;YAAE,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,GACrC,OAAO,CAAC,YAAY,CAAC,CAAC;KAC1B;IAED,UAAU,oBAAoB;QAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;CACF"}
|