javascript-solid-server 0.0.6 → 0.0.8

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.
@@ -2,6 +2,15 @@ import * as storage from '../storage/filesystem.js';
2
2
  import { getAllHeaders } from '../ldp/headers.js';
3
3
  import { generateContainerJsonLd, serializeJsonLd } from '../ldp/container.js';
4
4
  import { isContainer, getContentType, isRdfContentType } from '../utils/url.js';
5
+ import { parseN3Patch, applyN3Patch, validatePatch } from '../patch/n3-patch.js';
6
+ import {
7
+ selectContentType,
8
+ canAcceptInput,
9
+ toJsonLd,
10
+ fromJsonLd,
11
+ getVaryHeader,
12
+ RDF_TYPES
13
+ } from '../rdf/conneg.js';
5
14
 
6
15
  /**
7
16
  * Handle GET request
@@ -19,6 +28,8 @@ export async function handleGet(request, reply) {
19
28
 
20
29
  // Handle container
21
30
  if (stats.isDirectory) {
31
+ const connegEnabled = request.connegEnabled || false;
32
+
22
33
  // Check for index.html (serves as both profile and container representation)
23
34
  const indexPath = urlPath.endsWith('/') ? `${urlPath}index.html` : `${urlPath}/index.html`;
24
35
  const indexExists = await storage.exists(indexPath);
@@ -33,7 +44,8 @@ export async function handleGet(request, reply) {
33
44
  etag: indexStats?.etag || stats.etag,
34
45
  contentType: 'text/html',
35
46
  origin,
36
- resourceUrl
47
+ resourceUrl,
48
+ connegEnabled
37
49
  });
38
50
 
39
51
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
@@ -49,7 +61,8 @@ export async function handleGet(request, reply) {
49
61
  etag: stats.etag,
50
62
  contentType: 'application/ld+json',
51
63
  origin,
52
- resourceUrl
64
+ resourceUrl,
65
+ connegEnabled
53
66
  });
54
67
 
55
68
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
@@ -62,14 +75,54 @@ export async function handleGet(request, reply) {
62
75
  return reply.code(500).send({ error: 'Read error' });
63
76
  }
64
77
 
65
- const contentType = getContentType(urlPath);
78
+ const storedContentType = getContentType(urlPath);
79
+ const connegEnabled = request.connegEnabled || false;
80
+
81
+ // Content negotiation for RDF resources
82
+ if (connegEnabled && isRdfContentType(storedContentType)) {
83
+ try {
84
+ // Parse stored content as JSON-LD
85
+ const jsonLd = JSON.parse(content.toString());
86
+
87
+ // Select output format based on Accept header
88
+ const acceptHeader = request.headers.accept;
89
+ const targetType = selectContentType(acceptHeader, connegEnabled);
90
+
91
+ // Convert to requested format
92
+ const { content: outputContent, contentType: outputType } = await fromJsonLd(
93
+ jsonLd,
94
+ targetType,
95
+ resourceUrl,
96
+ connegEnabled
97
+ );
98
+
99
+ const headers = getAllHeaders({
100
+ isContainer: false,
101
+ etag: stats.etag,
102
+ contentType: outputType,
103
+ origin,
104
+ resourceUrl,
105
+ connegEnabled
106
+ });
107
+ headers['Vary'] = getVaryHeader(connegEnabled);
108
+
109
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
110
+ return reply.send(outputContent);
111
+ } catch (e) {
112
+ // If not valid JSON-LD, serve as-is
113
+ }
114
+ }
115
+
116
+ // Serve content as-is (no conneg or non-RDF resource)
66
117
  const headers = getAllHeaders({
67
118
  isContainer: false,
68
119
  etag: stats.etag,
69
- contentType,
120
+ contentType: storedContentType,
70
121
  origin,
71
- resourceUrl
122
+ resourceUrl,
123
+ connegEnabled
72
124
  });
125
+ headers['Vary'] = getVaryHeader(connegEnabled);
73
126
 
74
127
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
75
128
  return reply.send(content);
@@ -117,6 +170,20 @@ export async function handlePut(request, reply) {
117
170
  return reply.code(409).send({ error: 'Cannot PUT to container. Use POST instead.' });
118
171
  }
119
172
 
173
+ const connegEnabled = request.connegEnabled || false;
174
+ const contentType = request.headers['content-type'] || '';
175
+ const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
176
+
177
+ // Check if we can accept this input type
178
+ if (!canAcceptInput(contentType, connegEnabled)) {
179
+ return reply.code(415).send({
180
+ error: 'Unsupported Media Type',
181
+ message: connegEnabled
182
+ ? 'Supported types: application/ld+json, text/turtle, text/n3'
183
+ : 'Supported type: application/ld+json (enable conneg for Turtle support)'
184
+ });
185
+ }
186
+
120
187
  // Check if resource already exists
121
188
  const existed = await storage.exists(urlPath);
122
189
 
@@ -134,15 +201,29 @@ export async function handlePut(request, reply) {
134
201
  content = Buffer.from('');
135
202
  }
136
203
 
204
+ // Convert Turtle/N3 to JSON-LD if conneg enabled
205
+ const inputType = contentType.split(';')[0].trim().toLowerCase();
206
+ if (connegEnabled && (inputType === RDF_TYPES.TURTLE || inputType === RDF_TYPES.N3)) {
207
+ try {
208
+ const jsonLd = await toJsonLd(content, contentType, resourceUrl, connegEnabled);
209
+ content = Buffer.from(JSON.stringify(jsonLd, null, 2));
210
+ } catch (e) {
211
+ return reply.code(400).send({
212
+ error: 'Bad Request',
213
+ message: 'Invalid Turtle/N3 format: ' + e.message
214
+ });
215
+ }
216
+ }
217
+
137
218
  const success = await storage.write(urlPath, content);
138
219
  if (!success) {
139
220
  return reply.code(500).send({ error: 'Write failed' });
140
221
  }
141
222
 
142
223
  const origin = request.headers.origin;
143
- const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
144
- const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
224
+ const headers = getAllHeaders({ isContainer: false, origin, resourceUrl, connegEnabled });
145
225
  headers['Location'] = resourceUrl;
226
+ headers['Vary'] = getVaryHeader(connegEnabled);
146
227
 
147
228
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
148
229
  return reply.code(existed ? 204 : 201).send();
@@ -190,3 +271,99 @@ export async function handleOptions(request, reply) {
190
271
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
191
272
  return reply.code(204).send();
192
273
  }
274
+
275
+ /**
276
+ * Handle PATCH request
277
+ * Supports N3 Patch format (text/n3) for updating RDF resources
278
+ */
279
+ export async function handlePatch(request, reply) {
280
+ const urlPath = request.url.split('?')[0];
281
+
282
+ // Don't allow PATCH to containers
283
+ if (isContainer(urlPath)) {
284
+ return reply.code(409).send({ error: 'Cannot PATCH containers' });
285
+ }
286
+
287
+ // Check content type
288
+ const contentType = request.headers['content-type'] || '';
289
+ const isN3Patch = contentType.includes('text/n3') ||
290
+ contentType.includes('application/n3') ||
291
+ contentType.includes('application/sparql-update');
292
+
293
+ if (!isN3Patch) {
294
+ return reply.code(415).send({
295
+ error: 'Unsupported Media Type',
296
+ message: 'PATCH requires Content-Type: text/n3 for N3 Patch format'
297
+ });
298
+ }
299
+
300
+ // Check if resource exists
301
+ const stats = await storage.stat(urlPath);
302
+ if (!stats) {
303
+ return reply.code(404).send({ error: 'Not Found' });
304
+ }
305
+
306
+ // Read existing content
307
+ const existingContent = await storage.read(urlPath);
308
+ if (existingContent === null) {
309
+ return reply.code(500).send({ error: 'Read error' });
310
+ }
311
+
312
+ // Parse existing document as JSON-LD
313
+ let document;
314
+ try {
315
+ document = JSON.parse(existingContent.toString());
316
+ } catch (e) {
317
+ return reply.code(409).send({
318
+ error: 'Conflict',
319
+ message: 'Resource is not valid JSON-LD and cannot be patched'
320
+ });
321
+ }
322
+
323
+ // Parse the patch
324
+ const patchContent = Buffer.isBuffer(request.body)
325
+ ? request.body.toString()
326
+ : request.body;
327
+
328
+ const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
329
+ let patch;
330
+ try {
331
+ patch = parseN3Patch(patchContent, resourceUrl);
332
+ } catch (e) {
333
+ return reply.code(400).send({
334
+ error: 'Bad Request',
335
+ message: 'Invalid N3 Patch format: ' + e.message
336
+ });
337
+ }
338
+
339
+ // Validate that deletes exist (optional strict mode)
340
+ // const validation = validatePatch(document, patch, resourceUrl);
341
+ // if (!validation.valid) {
342
+ // return reply.code(409).send({ error: 'Conflict', message: validation.error });
343
+ // }
344
+
345
+ // Apply the patch
346
+ let updatedDocument;
347
+ try {
348
+ updatedDocument = applyN3Patch(document, patch, resourceUrl);
349
+ } catch (e) {
350
+ return reply.code(409).send({
351
+ error: 'Conflict',
352
+ message: 'Failed to apply patch: ' + e.message
353
+ });
354
+ }
355
+
356
+ // Write updated document
357
+ const updatedContent = JSON.stringify(updatedDocument, null, 2);
358
+ const success = await storage.write(urlPath, Buffer.from(updatedContent));
359
+
360
+ if (!success) {
361
+ return reply.code(500).send({ error: 'Write failed' });
362
+ }
363
+
364
+ const origin = request.headers.origin;
365
+ const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
366
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
367
+
368
+ return reply.code(204).send();
369
+ }
@@ -2,6 +2,8 @@
2
2
  * LDP (Linked Data Platform) header utilities
3
3
  */
4
4
 
5
+ import { getAcceptHeaders } from '../rdf/conneg.js';
6
+
5
7
  const LDP = 'http://www.w3.org/ns/ldp#';
6
8
 
7
9
  /**
@@ -47,21 +49,21 @@ export function getAclUrl(resourceUrl, isContainer) {
47
49
  * @param {object} options
48
50
  * @returns {object}
49
51
  */
50
- export function getResponseHeaders({ isContainer = false, etag = null, contentType = null, resourceUrl = null, wacAllow = null }) {
52
+ export function getResponseHeaders({ isContainer = false, etag = null, contentType = null, resourceUrl = null, wacAllow = null, connegEnabled = false }) {
51
53
  // Calculate ACL URL if resource URL provided
52
54
  const aclUrl = resourceUrl ? getAclUrl(resourceUrl, isContainer) : null;
53
55
 
54
56
  const headers = {
55
57
  'Link': getLinkHeader(isContainer, aclUrl),
56
58
  'WAC-Allow': wacAllow || 'user="read write append control", public="read write append"',
57
- 'Accept-Patch': 'application/sparql-update',
58
- 'Allow': 'GET, HEAD, PUT, DELETE, OPTIONS' + (isContainer ? ', POST' : ''),
59
- 'Vary': 'Accept, Authorization, Origin'
59
+ 'Accept-Patch': 'text/n3, application/sparql-update',
60
+ 'Allow': 'GET, HEAD, PUT, DELETE, PATCH, OPTIONS' + (isContainer ? ', POST' : ''),
61
+ 'Vary': connegEnabled ? 'Accept, Authorization, Origin' : 'Authorization, Origin'
60
62
  };
61
63
 
62
- if (isContainer) {
63
- headers['Accept-Post'] = '*/*';
64
- }
64
+ // Add Accept-* headers (conneg-aware)
65
+ const acceptHeaders = getAcceptHeaders(connegEnabled, isContainer);
66
+ Object.assign(headers, acceptHeaders);
65
67
 
66
68
  if (etag) {
67
69
  headers['ETag'] = etag;
@@ -95,9 +97,9 @@ export function getCorsHeaders(origin) {
95
97
  * @param {object} options
96
98
  * @returns {object}
97
99
  */
98
- export function getAllHeaders({ isContainer = false, etag = null, contentType = null, origin = null, resourceUrl = null, wacAllow = null }) {
100
+ export function getAllHeaders({ isContainer = false, etag = null, contentType = null, origin = null, resourceUrl = null, wacAllow = null, connegEnabled = false }) {
99
101
  return {
100
- ...getResponseHeaders({ isContainer, etag, contentType, resourceUrl, wacAllow }),
102
+ ...getResponseHeaders({ isContainer, etag, contentType, resourceUrl, wacAllow, connegEnabled }),
101
103
  ...getCorsHeaders(origin)
102
104
  };
103
105
  }