javascript-solid-server 0.0.7 → 0.0.9

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.
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Turtle <-> JSON-LD Conversion
3
+ *
4
+ * Provides bidirectional conversion between Turtle and JSON-LD formats.
5
+ * Uses the N3.js library for parsing and serializing Turtle.
6
+ */
7
+
8
+ import { Parser, Writer, DataFactory } from 'n3';
9
+ const { namedNode, literal, blankNode, quad } = DataFactory;
10
+
11
+ // Common prefixes for compact output
12
+ const COMMON_PREFIXES = {
13
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
14
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
15
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
16
+ foaf: 'http://xmlns.com/foaf/0.1/',
17
+ ldp: 'http://www.w3.org/ns/ldp#',
18
+ solid: 'http://www.w3.org/ns/solid/terms#',
19
+ acl: 'http://www.w3.org/ns/auth/acl#',
20
+ pim: 'http://www.w3.org/ns/pim/space#',
21
+ dc: 'http://purl.org/dc/terms/',
22
+ schema: 'http://schema.org/',
23
+ vcard: 'http://www.w3.org/2006/vcard/ns#'
24
+ };
25
+
26
+ /**
27
+ * Parse Turtle to JSON-LD
28
+ * @param {string} turtle - Turtle content
29
+ * @param {string} baseUri - Base URI for relative references
30
+ * @returns {Promise<object>} JSON-LD document
31
+ */
32
+ export async function turtleToJsonLd(turtle, baseUri) {
33
+ return new Promise((resolve, reject) => {
34
+ const parser = new Parser({ baseIRI: baseUri });
35
+ const quads = [];
36
+
37
+ parser.parse(turtle, (error, quad, prefixes) => {
38
+ if (error) {
39
+ reject(error);
40
+ return;
41
+ }
42
+
43
+ if (quad) {
44
+ quads.push(quad);
45
+ } else {
46
+ // Parsing complete
47
+ try {
48
+ const jsonLd = quadsToJsonLd(quads, baseUri, prefixes);
49
+ resolve(jsonLd);
50
+ } catch (e) {
51
+ reject(e);
52
+ }
53
+ }
54
+ });
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Convert JSON-LD to Turtle
60
+ * @param {object} jsonLd - JSON-LD document
61
+ * @param {string} baseUri - Base URI for the document
62
+ * @returns {Promise<string>} Turtle content
63
+ */
64
+ export async function jsonLdToTurtle(jsonLd, baseUri) {
65
+ return new Promise((resolve, reject) => {
66
+ try {
67
+ const quads = jsonLdToQuads(jsonLd, baseUri);
68
+
69
+ const writer = new Writer({
70
+ prefixes: COMMON_PREFIXES,
71
+ baseIRI: baseUri
72
+ });
73
+
74
+ for (const q of quads) {
75
+ writer.addQuad(q);
76
+ }
77
+
78
+ writer.end((error, result) => {
79
+ if (error) {
80
+ reject(error);
81
+ } else {
82
+ resolve(result);
83
+ }
84
+ });
85
+ } catch (e) {
86
+ reject(e);
87
+ }
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Convert N3.js quads to JSON-LD
93
+ */
94
+ function quadsToJsonLd(quads, baseUri, prefixes = {}) {
95
+ if (quads.length === 0) {
96
+ return { '@context': buildContext(prefixes) };
97
+ }
98
+
99
+ // Group quads by subject
100
+ const subjects = new Map();
101
+
102
+ for (const quad of quads) {
103
+ const subjectKey = quad.subject.value;
104
+ if (!subjects.has(subjectKey)) {
105
+ subjects.set(subjectKey, {
106
+ '@id': makeRelative(quad.subject.value, baseUri),
107
+ _quads: []
108
+ });
109
+ }
110
+ subjects.get(subjectKey)._quads.push(quad);
111
+ }
112
+
113
+ // Build nodes
114
+ const nodes = [];
115
+ for (const [subjectUri, node] of subjects) {
116
+ const jsonNode = { '@id': node['@id'] };
117
+
118
+ for (const quad of node._quads) {
119
+ const predicate = quad.predicate.value;
120
+ const predicateKey = compactUri(predicate, prefixes);
121
+
122
+ // Handle rdf:type specially
123
+ if (predicate === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type') {
124
+ const typeValue = compactUri(quad.object.value, prefixes);
125
+ if (jsonNode['@type']) {
126
+ if (Array.isArray(jsonNode['@type'])) {
127
+ jsonNode['@type'].push(typeValue);
128
+ } else {
129
+ jsonNode['@type'] = [jsonNode['@type'], typeValue];
130
+ }
131
+ } else {
132
+ jsonNode['@type'] = typeValue;
133
+ }
134
+ continue;
135
+ }
136
+
137
+ const objectValue = termToJsonLd(quad.object, baseUri, prefixes);
138
+
139
+ if (jsonNode[predicateKey]) {
140
+ // Multiple values - make array
141
+ if (Array.isArray(jsonNode[predicateKey])) {
142
+ jsonNode[predicateKey].push(objectValue);
143
+ } else {
144
+ jsonNode[predicateKey] = [jsonNode[predicateKey], objectValue];
145
+ }
146
+ } else {
147
+ jsonNode[predicateKey] = objectValue;
148
+ }
149
+ }
150
+
151
+ nodes.push(jsonNode);
152
+ }
153
+
154
+ // Build result
155
+ const context = buildContext(prefixes);
156
+
157
+ if (nodes.length === 1) {
158
+ return { '@context': context, ...nodes[0] };
159
+ }
160
+
161
+ return { '@context': context, '@graph': nodes };
162
+ }
163
+
164
+ /**
165
+ * Convert JSON-LD to N3.js quads
166
+ */
167
+ function jsonLdToQuads(jsonLd, baseUri) {
168
+ const quads = [];
169
+ const context = jsonLd['@context'] || {};
170
+
171
+ // Handle @graph or single object
172
+ const nodes = jsonLd['@graph'] || [jsonLd];
173
+
174
+ for (const node of nodes) {
175
+ if (!node['@id']) continue;
176
+
177
+ const subjectUri = resolveUri(node['@id'], baseUri);
178
+ const subject = subjectUri.startsWith('_:')
179
+ ? blankNode(subjectUri.slice(2))
180
+ : namedNode(subjectUri);
181
+
182
+ // Handle @type
183
+ if (node['@type']) {
184
+ const types = Array.isArray(node['@type']) ? node['@type'] : [node['@type']];
185
+ for (const type of types) {
186
+ const typeUri = expandUri(type, context);
187
+ quads.push(quad(
188
+ subject,
189
+ namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
190
+ namedNode(typeUri)
191
+ ));
192
+ }
193
+ }
194
+
195
+ // Handle other properties
196
+ for (const [key, value] of Object.entries(node)) {
197
+ if (key.startsWith('@')) continue;
198
+
199
+ const predicateUri = expandUri(key, context);
200
+ const predicate = namedNode(predicateUri);
201
+
202
+ const values = Array.isArray(value) ? value : [value];
203
+ for (const v of values) {
204
+ const object = valueToTerm(v, baseUri, context);
205
+ if (object) {
206
+ quads.push(quad(subject, predicate, object));
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ return quads;
213
+ }
214
+
215
+ /**
216
+ * Convert N3.js term to JSON-LD value
217
+ */
218
+ function termToJsonLd(term, baseUri, prefixes) {
219
+ if (term.termType === 'NamedNode') {
220
+ const uri = makeRelative(term.value, baseUri);
221
+ // Check if it looks like a URI or should be @id
222
+ if (uri.includes('://') || uri.startsWith('#') || uri.startsWith('/')) {
223
+ return { '@id': uri };
224
+ }
225
+ return { '@id': uri };
226
+ }
227
+
228
+ if (term.termType === 'BlankNode') {
229
+ return { '@id': '_:' + term.value };
230
+ }
231
+
232
+ if (term.termType === 'Literal') {
233
+ // Check for language tag
234
+ if (term.language) {
235
+ return { '@value': term.value, '@language': term.language };
236
+ }
237
+
238
+ // Check for datatype
239
+ const datatype = term.datatype?.value;
240
+ if (datatype) {
241
+ // Handle common XSD types
242
+ if (datatype === 'http://www.w3.org/2001/XMLSchema#integer') {
243
+ return parseInt(term.value, 10);
244
+ }
245
+ if (datatype === 'http://www.w3.org/2001/XMLSchema#decimal' ||
246
+ datatype === 'http://www.w3.org/2001/XMLSchema#double' ||
247
+ datatype === 'http://www.w3.org/2001/XMLSchema#float') {
248
+ return parseFloat(term.value);
249
+ }
250
+ if (datatype === 'http://www.w3.org/2001/XMLSchema#boolean') {
251
+ return term.value === 'true';
252
+ }
253
+ if (datatype === 'http://www.w3.org/2001/XMLSchema#string') {
254
+ return term.value;
255
+ }
256
+ // Other typed literals
257
+ return { '@value': term.value, '@type': compactUri(datatype, prefixes) };
258
+ }
259
+
260
+ return term.value;
261
+ }
262
+
263
+ return term.value;
264
+ }
265
+
266
+ /**
267
+ * Convert JSON-LD value to N3.js term
268
+ */
269
+ function valueToTerm(value, baseUri, context) {
270
+ if (value === null || value === undefined) {
271
+ return null;
272
+ }
273
+
274
+ // Plain values
275
+ if (typeof value === 'string') {
276
+ return literal(value);
277
+ }
278
+ if (typeof value === 'number') {
279
+ if (Number.isInteger(value)) {
280
+ return literal(value.toString(), namedNode('http://www.w3.org/2001/XMLSchema#integer'));
281
+ }
282
+ return literal(value.toString(), namedNode('http://www.w3.org/2001/XMLSchema#decimal'));
283
+ }
284
+ if (typeof value === 'boolean') {
285
+ return literal(value.toString(), namedNode('http://www.w3.org/2001/XMLSchema#boolean'));
286
+ }
287
+
288
+ // Object values
289
+ if (typeof value === 'object') {
290
+ // @id reference
291
+ if (value['@id']) {
292
+ const uri = resolveUri(value['@id'], baseUri);
293
+ return uri.startsWith('_:')
294
+ ? blankNode(uri.slice(2))
295
+ : namedNode(uri);
296
+ }
297
+
298
+ // @value with @language
299
+ if (value['@value'] && value['@language']) {
300
+ return literal(value['@value'], value['@language']);
301
+ }
302
+
303
+ // @value with @type
304
+ if (value['@value'] && value['@type']) {
305
+ const typeUri = expandUri(value['@type'], context);
306
+ return literal(value['@value'], namedNode(typeUri));
307
+ }
308
+
309
+ // Plain @value
310
+ if (value['@value']) {
311
+ return literal(value['@value']);
312
+ }
313
+ }
314
+
315
+ return null;
316
+ }
317
+
318
+ /**
319
+ * Make URI relative to base
320
+ */
321
+ function makeRelative(uri, baseUri) {
322
+ if (uri.startsWith(baseUri)) {
323
+ const relative = uri.slice(baseUri.length);
324
+ if (relative.startsWith('#') || relative === '') {
325
+ return relative || '.';
326
+ }
327
+ return relative;
328
+ }
329
+ return uri;
330
+ }
331
+
332
+ /**
333
+ * Resolve relative URI against base
334
+ */
335
+ function resolveUri(uri, baseUri) {
336
+ if (uri.startsWith('http://') || uri.startsWith('https://') || uri.startsWith('_:')) {
337
+ return uri;
338
+ }
339
+ if (uri.startsWith('#')) {
340
+ return baseUri + uri;
341
+ }
342
+ try {
343
+ return new URL(uri, baseUri).href;
344
+ } catch {
345
+ return uri;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Expand prefixed URI using context
351
+ */
352
+ function expandUri(uri, context) {
353
+ if (uri.includes('://')) {
354
+ return uri;
355
+ }
356
+
357
+ if (uri.includes(':')) {
358
+ const [prefix, local] = uri.split(':', 2);
359
+ const ns = context[prefix] || COMMON_PREFIXES[prefix];
360
+ if (ns) {
361
+ return ns + local;
362
+ }
363
+ }
364
+
365
+ // Check if it's a term in context
366
+ if (context[uri]) {
367
+ const expansion = context[uri];
368
+ if (typeof expansion === 'string') {
369
+ return expansion;
370
+ }
371
+ if (expansion['@id']) {
372
+ return expansion['@id'];
373
+ }
374
+ }
375
+
376
+ return uri;
377
+ }
378
+
379
+ /**
380
+ * Compact URI using prefixes
381
+ */
382
+ function compactUri(uri, prefixes) {
383
+ // Check custom prefixes first
384
+ for (const [prefix, ns] of Object.entries(prefixes)) {
385
+ if (uri.startsWith(ns)) {
386
+ return prefix + ':' + uri.slice(ns.length);
387
+ }
388
+ }
389
+
390
+ // Check common prefixes
391
+ for (const [prefix, ns] of Object.entries(COMMON_PREFIXES)) {
392
+ if (uri.startsWith(ns)) {
393
+ return prefix + ':' + uri.slice(ns.length);
394
+ }
395
+ }
396
+
397
+ return uri;
398
+ }
399
+
400
+ /**
401
+ * Build JSON-LD @context from prefixes
402
+ */
403
+ function buildContext(prefixes) {
404
+ const context = { ...COMMON_PREFIXES };
405
+ for (const [prefix, ns] of Object.entries(prefixes)) {
406
+ if (prefix && ns) {
407
+ context[prefix] = ns;
408
+ }
409
+ }
410
+ return context;
411
+ }
package/src/server.js CHANGED
@@ -3,11 +3,21 @@ import { handleGet, handleHead, handlePut, handleDelete, handleOptions, handlePa
3
3
  import { handlePost, handleCreatePod } from './handlers/container.js';
4
4
  import { getCorsHeaders } from './ldp/headers.js';
5
5
  import { authorize, handleUnauthorized } from './auth/middleware.js';
6
+ import { notificationsPlugin } from './notifications/index.js';
6
7
 
7
8
  /**
8
9
  * Create and configure Fastify server
10
+ * @param {object} options - Server options
11
+ * @param {boolean} options.logger - Enable logging (default true)
12
+ * @param {boolean} options.conneg - Enable content negotiation for RDF (default false)
13
+ * @param {boolean} options.notifications - Enable WebSocket notifications (default false)
9
14
  */
10
15
  export function createServer(options = {}) {
16
+ // Content negotiation is OFF by default - we're a JSON-LD native server
17
+ const connegEnabled = options.conneg ?? false;
18
+ // WebSocket notifications are OFF by default
19
+ const notificationsEnabled = options.notifications ?? false;
20
+
11
21
  const fastify = Fastify({
12
22
  logger: options.logger ?? true,
13
23
  trustProxy: true,
@@ -20,12 +30,31 @@ export function createServer(options = {}) {
20
30
  done(null, body);
21
31
  });
22
32
 
33
+ // Attach server config to requests
34
+ fastify.decorateRequest('connegEnabled', null);
35
+ fastify.decorateRequest('notificationsEnabled', null);
36
+ fastify.addHook('onRequest', async (request) => {
37
+ request.connegEnabled = connegEnabled;
38
+ request.notificationsEnabled = notificationsEnabled;
39
+ });
40
+
41
+ // Register WebSocket notifications plugin if enabled
42
+ if (notificationsEnabled) {
43
+ fastify.register(notificationsPlugin);
44
+ }
45
+
23
46
  // Global CORS preflight
24
47
  fastify.addHook('onRequest', async (request, reply) => {
25
48
  // Add CORS headers to all responses
26
49
  const corsHeaders = getCorsHeaders(request.headers.origin);
27
50
  Object.entries(corsHeaders).forEach(([k, v]) => reply.header(k, v));
28
51
 
52
+ // Add Updates-Via header for WebSocket notification discovery
53
+ if (notificationsEnabled) {
54
+ const wsProtocol = request.protocol === 'https' ? 'wss' : 'ws';
55
+ reply.header('Updates-Via', `${wsProtocol}://${request.hostname}/.notifications`);
56
+ }
57
+
29
58
  // Handle preflight OPTIONS
30
59
  if (request.method === 'OPTIONS') {
31
60
  // Add Allow header for LDP compliance