javascript-solid-server 0.0.7 → 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.
- package/README.md +167 -146
- package/package.json +3 -2
- package/src/handlers/container.js +34 -3
- package/src/handlers/resource.js +87 -7
- package/src/ldp/headers.js +11 -9
- package/src/rdf/conneg.js +215 -0
- package/src/rdf/turtle.js +411 -0
- package/src/server.js +12 -0
- package/test/conneg.test.js +289 -0
- package/test/helpers.js +4 -2
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Negotiation for RDF Resources
|
|
3
|
+
*
|
|
4
|
+
* Handles Accept header parsing and format selection.
|
|
5
|
+
* OFF by default - this is a JSON-LD native implementation.
|
|
6
|
+
* Enable with { conneg: true } in server options.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { turtleToJsonLd, jsonLdToTurtle } from './turtle.js';
|
|
10
|
+
|
|
11
|
+
// RDF content types we support
|
|
12
|
+
export const RDF_TYPES = {
|
|
13
|
+
JSON_LD: 'application/ld+json',
|
|
14
|
+
TURTLE: 'text/turtle',
|
|
15
|
+
N3: 'text/n3',
|
|
16
|
+
NTRIPLES: 'application/n-triples',
|
|
17
|
+
RDF_XML: 'application/rdf+xml' // Not supported, but recognized
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Content types we can serve (when conneg enabled)
|
|
21
|
+
const SUPPORTED_OUTPUT = [RDF_TYPES.JSON_LD, RDF_TYPES.TURTLE];
|
|
22
|
+
|
|
23
|
+
// Content types we can accept for input (when conneg enabled)
|
|
24
|
+
const SUPPORTED_INPUT = [RDF_TYPES.JSON_LD, RDF_TYPES.TURTLE, RDF_TYPES.N3];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse Accept header and select best content type
|
|
28
|
+
* @param {string} acceptHeader - Accept header value
|
|
29
|
+
* @param {boolean} connegEnabled - Whether content negotiation is enabled
|
|
30
|
+
* @returns {string} Selected content type
|
|
31
|
+
*/
|
|
32
|
+
export function selectContentType(acceptHeader, connegEnabled = false) {
|
|
33
|
+
// If conneg disabled, always return JSON-LD
|
|
34
|
+
if (!connegEnabled) {
|
|
35
|
+
return RDF_TYPES.JSON_LD;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!acceptHeader) {
|
|
39
|
+
return RDF_TYPES.JSON_LD;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse Accept header
|
|
43
|
+
const accepts = parseAcceptHeader(acceptHeader);
|
|
44
|
+
|
|
45
|
+
// Find best match
|
|
46
|
+
for (const { type } of accepts) {
|
|
47
|
+
if (type === '*/*' || type === 'application/*') {
|
|
48
|
+
return RDF_TYPES.JSON_LD;
|
|
49
|
+
}
|
|
50
|
+
if (SUPPORTED_OUTPUT.includes(type)) {
|
|
51
|
+
return type;
|
|
52
|
+
}
|
|
53
|
+
// Handle text/* preference
|
|
54
|
+
if (type === 'text/*') {
|
|
55
|
+
return RDF_TYPES.TURTLE;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Default to JSON-LD
|
|
60
|
+
return RDF_TYPES.JSON_LD;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse Accept header into sorted list
|
|
65
|
+
*/
|
|
66
|
+
function parseAcceptHeader(header) {
|
|
67
|
+
const types = header.split(',').map(part => {
|
|
68
|
+
const [type, ...params] = part.trim().split(';');
|
|
69
|
+
let q = 1;
|
|
70
|
+
|
|
71
|
+
for (const param of params) {
|
|
72
|
+
const [key, value] = param.trim().split('=');
|
|
73
|
+
if (key === 'q') {
|
|
74
|
+
q = parseFloat(value) || 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { type: type.trim().toLowerCase(), q };
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Sort by q value descending
|
|
82
|
+
return types.sort((a, b) => b.q - a.q);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if content type is RDF
|
|
87
|
+
*/
|
|
88
|
+
export function isRdfType(contentType) {
|
|
89
|
+
if (!contentType) return false;
|
|
90
|
+
const type = contentType.split(';')[0].trim().toLowerCase();
|
|
91
|
+
return Object.values(RDF_TYPES).includes(type) ||
|
|
92
|
+
type === 'application/json'; // Treat as JSON-LD
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if we can accept this input type for RDF resources
|
|
97
|
+
* Non-RDF content types are always accepted (passthrough)
|
|
98
|
+
*/
|
|
99
|
+
export function canAcceptInput(contentType, connegEnabled = false) {
|
|
100
|
+
if (!contentType) return true; // No content type = accept
|
|
101
|
+
|
|
102
|
+
const type = contentType.split(';')[0].trim().toLowerCase();
|
|
103
|
+
|
|
104
|
+
// Always accept JSON-LD and JSON
|
|
105
|
+
if (type === RDF_TYPES.JSON_LD || type === 'application/json') {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check if it's an RDF type we need to handle
|
|
110
|
+
const isRdf = Object.values(RDF_TYPES).includes(type);
|
|
111
|
+
|
|
112
|
+
// Non-RDF types are accepted as-is (passthrough)
|
|
113
|
+
if (!isRdf) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// RDF types other than JSON-LD only if conneg enabled
|
|
118
|
+
if (connegEnabled) {
|
|
119
|
+
return SUPPORTED_INPUT.includes(type);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// RDF type but conneg disabled - reject (should use JSON-LD)
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Convert content to JSON-LD (internal storage format)
|
|
128
|
+
* @param {Buffer|string} content - Input content
|
|
129
|
+
* @param {string} contentType - Content-Type header
|
|
130
|
+
* @param {string} baseUri - Base URI
|
|
131
|
+
* @param {boolean} connegEnabled - Whether conneg is enabled
|
|
132
|
+
* @returns {Promise<object>} JSON-LD document
|
|
133
|
+
*/
|
|
134
|
+
export async function toJsonLd(content, contentType, baseUri, connegEnabled = false) {
|
|
135
|
+
const type = (contentType || '').split(';')[0].trim().toLowerCase();
|
|
136
|
+
const text = Buffer.isBuffer(content) ? content.toString() : content;
|
|
137
|
+
|
|
138
|
+
// JSON-LD or JSON
|
|
139
|
+
if (type === RDF_TYPES.JSON_LD || type === 'application/json' || !type) {
|
|
140
|
+
return JSON.parse(text);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Turtle/N3 - only if conneg enabled
|
|
144
|
+
if (connegEnabled && (type === RDF_TYPES.TURTLE || type === RDF_TYPES.N3)) {
|
|
145
|
+
return turtleToJsonLd(text, baseUri);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new Error(`Unsupported content type: ${type}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Convert JSON-LD to requested format
|
|
153
|
+
* @param {object} jsonLd - JSON-LD document
|
|
154
|
+
* @param {string} targetType - Target content type
|
|
155
|
+
* @param {string} baseUri - Base URI
|
|
156
|
+
* @param {boolean} connegEnabled - Whether conneg is enabled
|
|
157
|
+
* @returns {Promise<{content: string, contentType: string}>}
|
|
158
|
+
*/
|
|
159
|
+
export async function fromJsonLd(jsonLd, targetType, baseUri, connegEnabled = false) {
|
|
160
|
+
// If conneg disabled, always output JSON-LD
|
|
161
|
+
if (!connegEnabled) {
|
|
162
|
+
return {
|
|
163
|
+
content: JSON.stringify(jsonLd, null, 2),
|
|
164
|
+
contentType: RDF_TYPES.JSON_LD
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// JSON-LD
|
|
169
|
+
if (targetType === RDF_TYPES.JSON_LD || !targetType) {
|
|
170
|
+
return {
|
|
171
|
+
content: JSON.stringify(jsonLd, null, 2),
|
|
172
|
+
contentType: RDF_TYPES.JSON_LD
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Turtle
|
|
177
|
+
if (targetType === RDF_TYPES.TURTLE) {
|
|
178
|
+
const turtle = await jsonLdToTurtle(jsonLd, baseUri);
|
|
179
|
+
return { content: turtle, contentType: RDF_TYPES.TURTLE };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Fallback to JSON-LD
|
|
183
|
+
return {
|
|
184
|
+
content: JSON.stringify(jsonLd, null, 2),
|
|
185
|
+
contentType: RDF_TYPES.JSON_LD
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get Vary header value for content negotiation
|
|
191
|
+
*/
|
|
192
|
+
export function getVaryHeader(connegEnabled) {
|
|
193
|
+
return connegEnabled ? 'Accept, Origin' : 'Origin';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get Accept-* headers for responses
|
|
198
|
+
*/
|
|
199
|
+
export function getAcceptHeaders(connegEnabled, isContainer = false) {
|
|
200
|
+
const headers = {};
|
|
201
|
+
|
|
202
|
+
if (isContainer) {
|
|
203
|
+
headers['Accept-Post'] = connegEnabled
|
|
204
|
+
? `${RDF_TYPES.JSON_LD}, ${RDF_TYPES.TURTLE}, */*`
|
|
205
|
+
: `${RDF_TYPES.JSON_LD}, */*`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
headers['Accept-Put'] = connegEnabled
|
|
209
|
+
? `${RDF_TYPES.JSON_LD}, ${RDF_TYPES.TURTLE}, */*`
|
|
210
|
+
: `${RDF_TYPES.JSON_LD}, */*`;
|
|
211
|
+
|
|
212
|
+
headers['Accept-Patch'] = 'text/n3, application/sparql-update';
|
|
213
|
+
|
|
214
|
+
return headers;
|
|
215
|
+
}
|
|
@@ -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
|
@@ -6,8 +6,14 @@ import { authorize, handleUnauthorized } from './auth/middleware.js';
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Create and configure Fastify server
|
|
9
|
+
* @param {object} options - Server options
|
|
10
|
+
* @param {boolean} options.logger - Enable logging (default true)
|
|
11
|
+
* @param {boolean} options.conneg - Enable content negotiation for RDF (default false)
|
|
9
12
|
*/
|
|
10
13
|
export function createServer(options = {}) {
|
|
14
|
+
// Content negotiation is OFF by default - we're a JSON-LD native server
|
|
15
|
+
const connegEnabled = options.conneg ?? false;
|
|
16
|
+
|
|
11
17
|
const fastify = Fastify({
|
|
12
18
|
logger: options.logger ?? true,
|
|
13
19
|
trustProxy: true,
|
|
@@ -20,6 +26,12 @@ export function createServer(options = {}) {
|
|
|
20
26
|
done(null, body);
|
|
21
27
|
});
|
|
22
28
|
|
|
29
|
+
// Attach server config to requests
|
|
30
|
+
fastify.decorateRequest('connegEnabled', null);
|
|
31
|
+
fastify.addHook('onRequest', async (request) => {
|
|
32
|
+
request.connegEnabled = connegEnabled;
|
|
33
|
+
});
|
|
34
|
+
|
|
23
35
|
// Global CORS preflight
|
|
24
36
|
fastify.addHook('onRequest', async (request, reply) => {
|
|
25
37
|
// Add CORS headers to all responses
|