@lde/fastify-rdf 0.2.12 → 0.4.0

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 CHANGED
@@ -43,8 +43,8 @@ app.get('/resource', async (request, reply) => {
43
43
  quad(
44
44
  namedNode('http://example.org/subject'),
45
45
  namedNode('http://example.org/predicate'),
46
- literal('object')
47
- )
46
+ literal('object'),
47
+ ),
48
48
  );
49
49
  return reply.sendRdf(store);
50
50
  });
@@ -66,13 +66,61 @@ app.get('/resource', async () => {
66
66
  quad(
67
67
  namedNode('http://example.org/subject'),
68
68
  namedNode('http://example.org/predicate'),
69
- literal('object')
70
- )
69
+ literal('object'),
70
+ ),
71
71
  );
72
72
  return store;
73
73
  });
74
74
  ```
75
75
 
76
+ ### Parsing RDF Request Bodies
77
+
78
+ The plugin registers content type parsers for all RDF formats supported by [rdf-parse](https://github.com/rubensworks/rdf-parse.js). Individual routes opt in via `config: { parseRdf: true }` — the body is then parsed into a `DatasetCore`:
79
+
80
+ ```typescript
81
+ app.post('/data', { config: { parseRdf: true } }, async (request) => {
82
+ const dataset = request.body as DatasetCore; // N3 Store
83
+ console.log(`Received ${dataset.size} quads`);
84
+ });
85
+ ```
86
+
87
+ To parse RDF bodies on **all** routes, enable `parseRdf` at the plugin level:
88
+
89
+ ```typescript
90
+ await app.register(fastifyRdf, { parseRdf: true });
91
+ ```
92
+
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
+
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
+
76
124
  ### Custom Default Content Type
77
125
 
78
126
  By default, the plugin uses `text/turtle` when no `Accept` header is provided. You can change this:
@@ -126,6 +174,12 @@ interface FastifyRdfOptions {
126
174
  * @default false
127
175
  */
128
176
  overrideSend?: boolean;
177
+
178
+ /**
179
+ * Parse RDF request bodies into a DatasetCore on all routes.
180
+ * @default false
181
+ */
182
+ parseRdf?: boolean;
129
183
  }
130
184
  ```
131
185
 
@@ -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
@@ -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
@@ -1,3 +1,4 @@
1
1
  export { fastifyRdf } from './plugin.js';
2
2
  export { DEFAULT_CONTENT_TYPE, } from './types.js';
3
+ export { createHydraErrorDataset } from './hydra-error.js';
3
4
  export { fastifyRdf as default } from './plugin.js';
package/dist/plugin.d.ts CHANGED
@@ -8,6 +8,7 @@ import { type FastifyRdfOptions } from './types.js';
8
8
  * - Optionally overrides reply.send() to serialize all responses as RDF
9
9
  * - Handles content negotiation via Accept headers
10
10
  * - Defaults to Turtle when no Accept header is provided
11
+ * - Registers content type parsers for RDF request bodies
11
12
  */
12
13
  declare function fastifyRdfPlugin(server: FastifyInstance, options: FastifyRdfOptions): Promise<void>;
13
14
  /**
@@ -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;AAM7E,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,YAAY,CAAC;AA+CpB;;;;;;;;GAQG;AACH,iBAAe,gBAAgB,CAC7B,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CA0Df;AAED;;GAEG;AACH,eAAO,MAAM,UAAU,yBAGrB,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;AAgIpB;;;;;;;;;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
@@ -1,8 +1,11 @@
1
1
  import { fastifyPlugin } from 'fastify-plugin';
2
2
  import { fastifyAccepts } from '@fastify/accepts';
3
3
  import { rdfSerializer } from 'rdf-serialize';
4
+ import { rdfParser } from 'rdf-parse';
5
+ import { Store } from 'n3';
4
6
  import { Readable } from 'node:stream';
5
7
  import { DEFAULT_CONTENT_TYPE, } from './types.js';
8
+ import { serializeHydraErrorAsJsonLd, createHydraErrorDataset, } from './hydra-error.js';
6
9
  /**
7
10
  * Collect a readable stream into a string.
8
11
  */
@@ -35,6 +38,69 @@ function negotiateContentType(request, supportedTypes, defaultType) {
35
38
  }
36
39
  return (request.accepts().type(supportedTypes) || defaultType);
37
40
  }
41
+ /**
42
+ * Collect quads from a readable stream into an N3 Store.
43
+ */
44
+ async function streamToDataset(stream) {
45
+ const store = new Store();
46
+ for await (const quad of stream) {
47
+ store.add(quad);
48
+ }
49
+ return store;
50
+ }
51
+ /**
52
+ * Register Fastify content type parsers for all RDF formats.
53
+ *
54
+ * When `parseAll` is true (from the plugin-level `parseRdf` option), every
55
+ * route gets RDF body parsing. Otherwise, routes opt in individually via
56
+ * `config: { parseRdf: true }`. Non-opted-in routes get JSON fallback for
57
+ * `application/ld+json` and 415 for other RDF types.
58
+ */
59
+ async function registerRdfParsers(server, parseAll) {
60
+ const contentTypes = (await rdfParser.getContentTypes()).filter((type) => type !== 'application/json');
61
+ server.addContentTypeParser(contentTypes, function (_request, payload, done) {
62
+ // Collect the raw body; the preParsing hook is not needed because
63
+ // Fastify passes the raw payload stream here.
64
+ const chunks = [];
65
+ payload.on('data', (chunk) => chunks.push(chunk));
66
+ payload.on('end', () => done(null, Buffer.concat(chunks)));
67
+ payload.on('error', done);
68
+ });
69
+ server.addHook('preHandler', async (request) => {
70
+ // Only act on requests that matched an RDF content type parser.
71
+ if (!request.body ||
72
+ !Buffer.isBuffer(request.body) ||
73
+ !request.headers['content-type']) {
74
+ return;
75
+ }
76
+ const contentType = request.headers['content-type'].split(';')[0].trim();
77
+ if (!contentTypes.includes(contentType)) {
78
+ return;
79
+ }
80
+ if (parseAll || request.routeOptions.config.parseRdf) {
81
+ try {
82
+ const bodyStream = Readable.from(request.body);
83
+ const quadStream = rdfParser.parse(bodyStream, { contentType });
84
+ request.body = await streamToDataset(quadStream);
85
+ }
86
+ catch (cause) {
87
+ const error = new Error('Invalid RDF body', {
88
+ cause,
89
+ });
90
+ error.statusCode = 400;
91
+ throw error;
92
+ }
93
+ }
94
+ else if (contentType === 'application/ld+json') {
95
+ request.body = JSON.parse(request.body.toString('utf8'));
96
+ }
97
+ else {
98
+ const error = new Error(`Unsupported Media Type: ${contentType}`);
99
+ error.statusCode = 415;
100
+ throw error;
101
+ }
102
+ });
103
+ }
38
104
  /**
39
105
  * Fastify plugin for serving RDF data with content negotiation.
40
106
  *
@@ -43,6 +109,7 @@ function negotiateContentType(request, supportedTypes, defaultType) {
43
109
  * - Optionally overrides reply.send() to serialize all responses as RDF
44
110
  * - Handles content negotiation via Accept headers
45
111
  * - Defaults to Turtle when no Accept header is provided
112
+ * - Registers content type parsers for RDF request bodies
46
113
  */
47
114
  async function fastifyRdfPlugin(server, options) {
48
115
  const defaultContentType = options.defaultContentType ?? DEFAULT_CONTENT_TYPE;
@@ -78,6 +145,20 @@ async function fastifyRdfPlugin(server, options) {
78
145
  return this.send(serialized);
79
146
  });
80
147
  }
148
+ server.decorateReply('sendHydraError', async function (error) {
149
+ this.status(error.statusCode ?? 500);
150
+ const title = error.message;
151
+ const description = typeof error.cause === 'string' ? error.cause : undefined;
152
+ const contentType = negotiateContentType(this.request, supportedContentTypes, defaultContentType);
153
+ if (contentType === 'application/ld+json') {
154
+ this.type('application/ld+json');
155
+ return this.send(serializeHydraErrorAsJsonLd(title, description));
156
+ }
157
+ const dataset = createHydraErrorDataset(title, description);
158
+ this.type(contentType);
159
+ return this.send(await serializeRdfToString(dataset, contentType));
160
+ });
161
+ await registerRdfParsers(server, options.parseRdf ?? false);
81
162
  }
82
163
  /**
83
164
  * Fastify plugin for serving RDF data with content negotiation.
package/dist/types.d.ts CHANGED
@@ -19,6 +19,12 @@ export interface FastifyRdfOptions {
19
19
  * @default false
20
20
  */
21
21
  overrideSend?: boolean;
22
+ /**
23
+ * Parse RDF request bodies into a DatasetCore on all routes.
24
+ * When not set, individual routes can opt in via `config: { parseRdf: true }`.
25
+ * @default false
26
+ */
27
+ parseRdf?: boolean;
22
28
  }
23
29
  /**
24
30
  * RDF data that can be serialized: either a DatasetCore or an RDF.js Stream of quads.
@@ -32,6 +38,17 @@ declare module 'fastify' {
32
38
  * @returns The data (when overrideSend is enabled) or Promise<FastifyReply>
33
39
  */
34
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>;
49
+ }
50
+ interface FastifyContextConfig {
51
+ parseRdf?: boolean;
35
52
  }
36
53
  }
37
54
  //# sourceMappingURL=types.d.ts.map
@@ -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;CACxB;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;KACzD;CACF"}
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"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@lde/fastify-rdf",
3
- "version": "0.2.12",
3
+ "version": "0.4.0",
4
4
  "description": "Fastify plugin for serving RDF data with content negotiation",
5
5
  "repository": {
6
- "url": "https://github.com/ldengine/lde",
6
+ "url": "git+https://github.com/ldengine/lde.git",
7
7
  "directory": "packages/fastify-rdf"
8
8
  },
9
9
  "type": "module",
@@ -26,13 +26,14 @@
26
26
  "dependencies": {
27
27
  "@fastify/accepts": "^5.0.0",
28
28
  "fastify-plugin": "^5.0.0",
29
+ "n3": "^2.0.1",
30
+ "rdf-parse": "^5.0.0",
29
31
  "rdf-serialize": "^5.1.0",
30
32
  "tslib": "^2.3.0"
31
33
  },
32
34
  "devDependencies": {
33
35
  "@rdfjs/types": "^2.0.0",
34
- "fastify": "^5.7.4",
35
- "n3": "^1.17.0"
36
+ "fastify": "^5.7.4"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "fastify": "^5.7.4"