javascript-solid-server 0.0.29 → 0.0.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -192,38 +192,70 @@ export async function handleGet(request, reply) {
192
192
  return reply.code(500).send({ error: 'Read error' });
193
193
  }
194
194
 
195
- // Content negotiation for RDF resources
196
- if (connegEnabled && isRdfContentType(storedContentType)) {
197
- try {
198
- // Parse stored content as JSON-LD
199
- const jsonLd = JSON.parse(content.toString());
200
-
201
- // Select output format based on Accept header
202
- const acceptHeader = request.headers.accept;
203
- const targetType = selectContentType(acceptHeader, connegEnabled);
204
-
205
- // Convert to requested format
206
- const { content: outputContent, contentType: outputType } = await fromJsonLd(
207
- jsonLd,
208
- targetType,
209
- resourceUrl,
210
- connegEnabled
211
- );
212
-
213
- const headers = getAllHeaders({
214
- isContainer: false,
215
- etag: stats.etag,
216
- contentType: outputType,
217
- origin,
218
- resourceUrl,
219
- connegEnabled
220
- });
221
- headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
195
+ // Content negotiation for RDF resources (including HTML with JSON-LD data islands)
196
+ if (connegEnabled) {
197
+ const contentStr = content.toString();
198
+ const acceptHeader = request.headers.accept || '';
199
+ const wantsTurtle = acceptHeader.includes('text/turtle') ||
200
+ acceptHeader.includes('text/n3') ||
201
+ acceptHeader.includes('application/n-triples');
202
+
203
+ // Check if this is HTML with JSON-LD data island
204
+ const isHtmlWithDataIsland = contentStr.trimStart().startsWith('<!DOCTYPE') ||
205
+ contentStr.trimStart().startsWith('<html');
206
+
207
+ if (isHtmlWithDataIsland && wantsTurtle) {
208
+ // Extract JSON-LD from HTML data island and convert to Turtle
209
+ try {
210
+ const jsonLdMatch = contentStr.match(/<script\s+type=["']application\/ld\+json["']\s*>([\s\S]*?)<\/script>/i);
211
+ if (jsonLdMatch) {
212
+ const jsonLd = JSON.parse(jsonLdMatch[1]);
213
+ const { content: turtleContent } = await fromJsonLd(jsonLd, 'text/turtle', resourceUrl, true);
214
+
215
+ const headers = getAllHeaders({
216
+ isContainer: false,
217
+ etag: stats.etag,
218
+ contentType: 'text/turtle',
219
+ origin,
220
+ resourceUrl,
221
+ connegEnabled
222
+ });
223
+ headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
224
+
225
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
226
+ return reply.send(turtleContent);
227
+ }
228
+ } catch (err) {
229
+ // Fall through to serve HTML if conversion fails
230
+ console.error('Failed to convert HTML data island to Turtle:', err.message);
231
+ }
232
+ } else if (isRdfContentType(storedContentType)) {
233
+ // Plain JSON-LD file
234
+ try {
235
+ const jsonLd = JSON.parse(contentStr);
236
+ const targetType = selectContentType(acceptHeader, connegEnabled);
237
+ const { content: outputContent, contentType: outputType } = await fromJsonLd(
238
+ jsonLd,
239
+ targetType,
240
+ resourceUrl,
241
+ connegEnabled
242
+ );
243
+
244
+ const headers = getAllHeaders({
245
+ isContainer: false,
246
+ etag: stats.etag,
247
+ contentType: outputType,
248
+ origin,
249
+ resourceUrl,
250
+ connegEnabled
251
+ });
252
+ headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
222
253
 
223
- Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
224
- return reply.send(outputContent);
225
- } catch (e) {
226
- // If not valid JSON-LD, serve as-is
254
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
255
+ return reply.send(outputContent);
256
+ } catch (e) {
257
+ // If not valid JSON-LD, serve as-is
258
+ }
227
259
  }
228
260
  }
229
261
 
@@ -533,10 +565,86 @@ export async function handlePatch(request, reply) {
533
565
  });
534
566
  }
535
567
  } else {
536
- // Parse as plain JSON-LD
568
+ // Try to parse as JSON-LD first
537
569
  try {
538
570
  document = JSON.parse(contentStr);
539
571
  } catch (e) {
572
+ // Not JSON - might be Turtle, handle with RDF store for SPARQL Update
573
+ if (isSparqlUpdate) {
574
+ // Parse Turtle and apply SPARQL Update directly
575
+ const { Parser, Writer } = await import('n3');
576
+ const parser = new Parser({ baseIRI: resourceUrl });
577
+ let quads;
578
+ try {
579
+ quads = parser.parse(contentStr);
580
+ } catch (parseErr) {
581
+ return reply.code(409).send({
582
+ error: 'Conflict',
583
+ message: 'Resource is not valid Turtle: ' + parseErr.message
584
+ });
585
+ }
586
+
587
+ // Parse the SPARQL Update
588
+ const patchContent = Buffer.isBuffer(request.body) ? request.body.toString() : request.body;
589
+ let update;
590
+ try {
591
+ update = parseSparqlUpdate(patchContent, resourceUrl);
592
+ } catch (parseErr) {
593
+ return reply.code(400).send({
594
+ error: 'Bad Request',
595
+ message: 'Invalid SPARQL Update: ' + parseErr.message
596
+ });
597
+ }
598
+
599
+ // Apply deletes
600
+ for (const triple of update.deletes) {
601
+ quads = quads.filter(q => {
602
+ const matches = q.subject.value === triple.subject &&
603
+ q.predicate.value === triple.predicate &&
604
+ (q.object.value === (triple.object['@id'] || triple.object['@value'] || triple.object));
605
+ return !matches;
606
+ });
607
+ }
608
+
609
+ // Apply inserts
610
+ const { DataFactory } = await import('n3');
611
+ const { namedNode, literal } = DataFactory;
612
+ for (const triple of update.inserts) {
613
+ const subj = namedNode(triple.subject);
614
+ const pred = namedNode(triple.predicate);
615
+ let obj;
616
+ if (triple.object['@id']) {
617
+ obj = namedNode(triple.object['@id']);
618
+ } else if (typeof triple.object === 'string') {
619
+ obj = literal(triple.object);
620
+ } else {
621
+ obj = literal(triple.object['@value'] || triple.object);
622
+ }
623
+ quads.push(DataFactory.quad(subj, pred, obj));
624
+ }
625
+
626
+ // Serialize back to Turtle
627
+ const writer = new Writer({ prefixes: {} });
628
+ quads.forEach(q => writer.addQuad(q));
629
+ let turtleOutput;
630
+ writer.end((err, result) => { turtleOutput = result; });
631
+
632
+ const success = await storage.write(storagePath, Buffer.from(turtleOutput));
633
+ if (!success) {
634
+ return reply.code(500).send({ error: 'Write failed' });
635
+ }
636
+
637
+ const origin = request.headers.origin;
638
+ const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
639
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
640
+
641
+ if (request.notificationsEnabled) {
642
+ emitChange(resourceUrl);
643
+ }
644
+
645
+ return reply.code(resourceExists ? 204 : 201).send();
646
+ }
647
+
540
648
  return reply.code(409).send({
541
649
  error: 'Conflict',
542
650
  message: 'Resource is not valid JSON-LD and cannot be patched'
package/src/rdf/turtle.js CHANGED
@@ -151,14 +151,15 @@ function quadsToJsonLd(quads, baseUri, prefixes = {}) {
151
151
  nodes.push(jsonNode);
152
152
  }
153
153
 
154
- // Build result
154
+ // Build result - return array if multiple nodes, single object otherwise
155
155
  const context = buildContext(prefixes);
156
156
 
157
157
  if (nodes.length === 1) {
158
158
  return { '@context': context, ...nodes[0] };
159
159
  }
160
160
 
161
- return { '@context': context, '@graph': nodes };
161
+ // Multiple nodes: return as array (no @graph)
162
+ return nodes.map((node, i) => i === 0 ? { '@context': context, ...node } : node);
162
163
  }
163
164
 
164
165
  /**
@@ -166,10 +167,25 @@ function quadsToJsonLd(quads, baseUri, prefixes = {}) {
166
167
  */
167
168
  function jsonLdToQuads(jsonLd, baseUri) {
168
169
  const quads = [];
169
- const context = jsonLd['@context'] || {};
170
170
 
171
- // Handle @graph or single object
172
- const nodes = jsonLd['@graph'] || [jsonLd];
171
+ // Handle array of JSON-LD objects (e.g., from multiple PATCH operations)
172
+ const documents = Array.isArray(jsonLd) ? jsonLd : [jsonLd];
173
+
174
+ // Merge all contexts and collect all nodes
175
+ let mergedContext = {};
176
+ let nodes = [];
177
+
178
+ for (const doc of documents) {
179
+ if (doc['@context']) {
180
+ mergedContext = { ...mergedContext, ...doc['@context'] };
181
+ }
182
+ // Each document with @id is a node (no @graph needed)
183
+ if (doc['@id']) {
184
+ nodes.push(doc);
185
+ }
186
+ }
187
+
188
+ const context = mergedContext;
173
189
 
174
190
  for (const node of nodes) {
175
191
  if (!node['@id']) continue;
@@ -33,25 +33,17 @@ export function generateProfileJsonLd({ webId, name, podUri, issuer }) {
33
33
  'inbox': { '@id': 'ldp:inbox', '@type': '@id' },
34
34
  'storage': { '@id': 'pim:storage', '@type': '@id' },
35
35
  'oidcIssuer': { '@id': 'solid:oidcIssuer', '@type': '@id' },
36
- 'preferencesFile': { '@id': 'pim:preferencesFile', '@type': '@id' }
36
+ 'preferencesFile': { '@id': 'pim:preferencesFile', '@type': '@id' },
37
+ 'mainEntityOfPage': { '@id': 'schema:mainEntityOfPage', '@type': '@id' }
37
38
  },
38
- '@graph': [
39
- {
40
- '@id': profileDoc,
41
- '@type': 'foaf:PersonalProfileDocument',
42
- 'foaf:maker': { '@id': webId },
43
- 'foaf:primaryTopic': { '@id': webId }
44
- },
45
- {
46
- '@id': webId,
47
- '@type': ['foaf:Person', 'schema:Person'],
48
- 'foaf:name': name,
49
- 'inbox': `${pod}inbox/`,
50
- 'storage': pod,
51
- 'oidcIssuer': issuer,
52
- 'preferencesFile': `${pod}settings/prefs`
53
- }
54
- ]
39
+ '@id': webId,
40
+ '@type': ['foaf:Person', 'schema:Person'],
41
+ 'foaf:name': name,
42
+ 'mainEntityOfPage': profileDoc,
43
+ 'inbox': `${pod}inbox/`,
44
+ 'storage': pod,
45
+ 'oidcIssuer': issuer,
46
+ 'preferencesFile': `${pod}settings/prefs`
55
47
  };
56
48
  }
57
49