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.
- package/README.md +167 -146
- package/package.json +3 -2
- package/src/handlers/container.js +34 -3
- package/src/handlers/resource.js +184 -7
- package/src/ldp/headers.js +11 -9
- package/src/patch/n3-patch.js +522 -0
- package/src/rdf/conneg.js +215 -0
- package/src/rdf/turtle.js +411 -0
- package/src/server.js +14 -1
- package/test/conneg.test.js +289 -0
- package/test/helpers.js +4 -2
- package/test/patch.test.js +295 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* N3 Patch Parser and Applier
|
|
3
|
+
*
|
|
4
|
+
* Implements Solid's N3 Patch format for updating RDF resources.
|
|
5
|
+
* https://solid.github.io/specification/protocol#n3-patch
|
|
6
|
+
*
|
|
7
|
+
* Supported format:
|
|
8
|
+
* @prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
|
9
|
+
* _:patch a solid:InsertDeletePatch;
|
|
10
|
+
* solid:inserts { <subject> <predicate> "object" };
|
|
11
|
+
* solid:deletes { <subject> <predicate> "old" }.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SOLID_NS = 'http://www.w3.org/ns/solid/terms#';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse an N3 Patch document
|
|
18
|
+
* @param {string} patchText - The N3 patch content
|
|
19
|
+
* @param {string} baseUri - Base URI for resolving relative references
|
|
20
|
+
* @returns {{inserts: Array, deletes: Array, where: Array}}
|
|
21
|
+
*/
|
|
22
|
+
export function parseN3Patch(patchText, baseUri) {
|
|
23
|
+
const result = {
|
|
24
|
+
inserts: [],
|
|
25
|
+
deletes: [],
|
|
26
|
+
where: []
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Extract prefixes
|
|
30
|
+
const prefixes = {};
|
|
31
|
+
const prefixRegex = /@prefix\s+(\w*):?\s*<([^>]+)>\s*\./g;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = prefixRegex.exec(patchText)) !== null) {
|
|
34
|
+
prefixes[match[1]] = match[2];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Find solid:inserts block
|
|
38
|
+
const insertsMatch = patchText.match(/solid:inserts\s*\{([^}]*)\}/s);
|
|
39
|
+
if (insertsMatch) {
|
|
40
|
+
result.inserts = parseTriples(insertsMatch[1], prefixes, baseUri);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find solid:deletes block
|
|
44
|
+
const deletesMatch = patchText.match(/solid:deletes\s*\{([^}]*)\}/s);
|
|
45
|
+
if (deletesMatch) {
|
|
46
|
+
result.deletes = parseTriples(deletesMatch[1], prefixes, baseUri);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Find solid:where block (for conditions - simplified support)
|
|
50
|
+
const whereMatch = patchText.match(/solid:where\s*\{([^}]*)\}/s);
|
|
51
|
+
if (whereMatch) {
|
|
52
|
+
result.where = parseTriples(whereMatch[1], prefixes, baseUri);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse triples from N3 block content
|
|
60
|
+
*/
|
|
61
|
+
function parseTriples(content, prefixes, baseUri) {
|
|
62
|
+
const triples = [];
|
|
63
|
+
|
|
64
|
+
// Clean up content
|
|
65
|
+
content = content.trim();
|
|
66
|
+
if (!content) return triples;
|
|
67
|
+
|
|
68
|
+
// Split by '.' but be careful with strings containing '.'
|
|
69
|
+
const statements = splitStatements(content);
|
|
70
|
+
|
|
71
|
+
for (const stmt of statements) {
|
|
72
|
+
const triple = parseStatement(stmt.trim(), prefixes, baseUri);
|
|
73
|
+
if (triple) {
|
|
74
|
+
triples.push(triple);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return triples;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Split content into statements (handling quoted strings)
|
|
83
|
+
*/
|
|
84
|
+
function splitStatements(content) {
|
|
85
|
+
const statements = [];
|
|
86
|
+
let current = '';
|
|
87
|
+
let inString = false;
|
|
88
|
+
let stringChar = null;
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < content.length; i++) {
|
|
91
|
+
const char = content[i];
|
|
92
|
+
|
|
93
|
+
if (!inString && (char === '"' || char === "'")) {
|
|
94
|
+
inString = true;
|
|
95
|
+
stringChar = char;
|
|
96
|
+
current += char;
|
|
97
|
+
} else if (inString && char === stringChar && content[i - 1] !== '\\') {
|
|
98
|
+
inString = false;
|
|
99
|
+
stringChar = null;
|
|
100
|
+
current += char;
|
|
101
|
+
} else if (!inString && char === '.') {
|
|
102
|
+
if (current.trim()) {
|
|
103
|
+
statements.push(current);
|
|
104
|
+
}
|
|
105
|
+
current = '';
|
|
106
|
+
} else if (!inString && char === ';') {
|
|
107
|
+
// Turtle shorthand - same subject, different predicate
|
|
108
|
+
if (current.trim()) {
|
|
109
|
+
statements.push(current);
|
|
110
|
+
}
|
|
111
|
+
current = '';
|
|
112
|
+
} else {
|
|
113
|
+
current += char;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (current.trim()) {
|
|
118
|
+
statements.push(current);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return statements;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse a single N3 statement into a triple
|
|
126
|
+
*/
|
|
127
|
+
function parseStatement(stmt, prefixes, baseUri) {
|
|
128
|
+
if (!stmt) return null;
|
|
129
|
+
|
|
130
|
+
// Tokenize - split by whitespace but respect quotes
|
|
131
|
+
const tokens = tokenize(stmt);
|
|
132
|
+
if (tokens.length < 3) return null;
|
|
133
|
+
|
|
134
|
+
const subject = resolveValue(tokens[0], prefixes, baseUri);
|
|
135
|
+
const predicate = resolveValue(tokens[1], prefixes, baseUri);
|
|
136
|
+
const object = resolveValue(tokens.slice(2).join(' '), prefixes, baseUri);
|
|
137
|
+
|
|
138
|
+
return { subject, predicate, object };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Tokenize a statement respecting quoted strings
|
|
143
|
+
*/
|
|
144
|
+
function tokenize(stmt) {
|
|
145
|
+
const tokens = [];
|
|
146
|
+
let current = '';
|
|
147
|
+
let inString = false;
|
|
148
|
+
let stringChar = null;
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < stmt.length; i++) {
|
|
151
|
+
const char = stmt[i];
|
|
152
|
+
|
|
153
|
+
if (!inString && (char === '"' || char === "'")) {
|
|
154
|
+
inString = true;
|
|
155
|
+
stringChar = char;
|
|
156
|
+
current += char;
|
|
157
|
+
} else if (inString && char === stringChar && stmt[i - 1] !== '\\') {
|
|
158
|
+
inString = false;
|
|
159
|
+
stringChar = null;
|
|
160
|
+
current += char;
|
|
161
|
+
} else if (!inString && /\s/.test(char)) {
|
|
162
|
+
if (current) {
|
|
163
|
+
tokens.push(current);
|
|
164
|
+
current = '';
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
current += char;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (current) {
|
|
172
|
+
tokens.push(current);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return tokens;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolve a value (URI, prefixed name, or literal)
|
|
180
|
+
*/
|
|
181
|
+
function resolveValue(value, prefixes, baseUri) {
|
|
182
|
+
value = value.trim();
|
|
183
|
+
|
|
184
|
+
// Full URI
|
|
185
|
+
if (value.startsWith('<') && value.endsWith('>')) {
|
|
186
|
+
const uri = value.slice(1, -1);
|
|
187
|
+
// Resolve relative URIs
|
|
188
|
+
if (!uri.includes('://') && !uri.startsWith('#')) {
|
|
189
|
+
return new URL(uri, baseUri).href;
|
|
190
|
+
}
|
|
191
|
+
if (uri.startsWith('#')) {
|
|
192
|
+
return baseUri + uri;
|
|
193
|
+
}
|
|
194
|
+
return uri;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Prefixed name
|
|
198
|
+
if (value.includes(':') && !value.startsWith('"')) {
|
|
199
|
+
const [prefix, local] = value.split(':', 2);
|
|
200
|
+
if (prefixes[prefix]) {
|
|
201
|
+
return prefixes[prefix] + local;
|
|
202
|
+
}
|
|
203
|
+
// Common prefixes
|
|
204
|
+
const commonPrefixes = {
|
|
205
|
+
'solid': SOLID_NS,
|
|
206
|
+
'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
207
|
+
'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
|
|
208
|
+
'foaf': 'http://xmlns.com/foaf/0.1/',
|
|
209
|
+
'dc': 'http://purl.org/dc/terms/',
|
|
210
|
+
'schema': 'http://schema.org/',
|
|
211
|
+
'ldp': 'http://www.w3.org/ns/ldp#'
|
|
212
|
+
};
|
|
213
|
+
if (commonPrefixes[prefix]) {
|
|
214
|
+
return commonPrefixes[prefix] + local;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// String literal
|
|
219
|
+
if (value.startsWith('"')) {
|
|
220
|
+
// Handle typed literals "value"^^<type>
|
|
221
|
+
const typedMatch = value.match(/^"(.*)"\^\^<([^>]+)>$/);
|
|
222
|
+
if (typedMatch) {
|
|
223
|
+
return { value: typedMatch[1], type: typedMatch[2] };
|
|
224
|
+
}
|
|
225
|
+
// Handle language tags "value"@en
|
|
226
|
+
const langMatch = value.match(/^"(.*)"@(\w+)$/);
|
|
227
|
+
if (langMatch) {
|
|
228
|
+
return { value: langMatch[1], language: langMatch[2] };
|
|
229
|
+
}
|
|
230
|
+
// Plain string
|
|
231
|
+
const plainMatch = value.match(/^"(.*)"$/);
|
|
232
|
+
if (plainMatch) {
|
|
233
|
+
return plainMatch[1];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Variable (for WHERE patterns)
|
|
238
|
+
if (value.startsWith('?')) {
|
|
239
|
+
return { variable: value.slice(1) };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Blank node
|
|
243
|
+
if (value.startsWith('_:')) {
|
|
244
|
+
return { blankNode: value.slice(2) };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Apply an N3 Patch to a JSON-LD document
|
|
252
|
+
* @param {object} document - The JSON-LD document
|
|
253
|
+
* @param {object} patch - Parsed patch {inserts, deletes}
|
|
254
|
+
* @param {string} baseUri - Base URI of the document
|
|
255
|
+
* @returns {object} Updated JSON-LD document
|
|
256
|
+
*/
|
|
257
|
+
export function applyN3Patch(document, patch, baseUri) {
|
|
258
|
+
// Clone the document
|
|
259
|
+
let doc = JSON.parse(JSON.stringify(document));
|
|
260
|
+
|
|
261
|
+
// Handle @graph array or single object
|
|
262
|
+
const isGraph = Array.isArray(doc['@graph']);
|
|
263
|
+
let nodes = isGraph ? doc['@graph'] : [doc];
|
|
264
|
+
|
|
265
|
+
// Apply deletes first
|
|
266
|
+
for (const triple of patch.deletes) {
|
|
267
|
+
nodes = deleteTriple(nodes, triple, baseUri);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Then apply inserts
|
|
271
|
+
for (const triple of patch.inserts) {
|
|
272
|
+
nodes = insertTriple(nodes, triple, baseUri);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Reconstruct document
|
|
276
|
+
if (isGraph) {
|
|
277
|
+
doc['@graph'] = nodes;
|
|
278
|
+
} else {
|
|
279
|
+
doc = nodes[0] || doc;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return doc;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Delete a triple from JSON-LD nodes
|
|
287
|
+
*/
|
|
288
|
+
function deleteTriple(nodes, triple, baseUri) {
|
|
289
|
+
const { subject, predicate, object } = triple;
|
|
290
|
+
|
|
291
|
+
for (const node of nodes) {
|
|
292
|
+
const nodeId = node['@id'] || '';
|
|
293
|
+
const resolvedNodeId = nodeId.startsWith('#') ? baseUri + nodeId : nodeId;
|
|
294
|
+
|
|
295
|
+
// Check if this node matches the subject
|
|
296
|
+
if (resolvedNodeId === subject || nodeId === subject) {
|
|
297
|
+
// Find the predicate
|
|
298
|
+
for (const key of Object.keys(node)) {
|
|
299
|
+
if (key.startsWith('@')) continue;
|
|
300
|
+
|
|
301
|
+
// Check if key matches predicate (could be full URI or prefixed)
|
|
302
|
+
if (key === predicate || expandPredicate(key) === predicate) {
|
|
303
|
+
const values = Array.isArray(node[key]) ? node[key] : [node[key]];
|
|
304
|
+
const newValues = values.filter(v => !valueMatches(v, object));
|
|
305
|
+
|
|
306
|
+
if (newValues.length === 0) {
|
|
307
|
+
delete node[key];
|
|
308
|
+
} else if (newValues.length === 1) {
|
|
309
|
+
node[key] = newValues[0];
|
|
310
|
+
} else {
|
|
311
|
+
node[key] = newValues;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return nodes;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Insert a triple into JSON-LD nodes
|
|
323
|
+
*/
|
|
324
|
+
function insertTriple(nodes, triple, baseUri) {
|
|
325
|
+
const { subject, predicate, object } = triple;
|
|
326
|
+
|
|
327
|
+
// Find or create the subject node
|
|
328
|
+
let subjectNode = nodes.find(n => {
|
|
329
|
+
const nodeId = n['@id'] || '';
|
|
330
|
+
const resolvedNodeId = nodeId.startsWith('#') ? baseUri + nodeId : nodeId;
|
|
331
|
+
return resolvedNodeId === subject || nodeId === subject;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (!subjectNode) {
|
|
335
|
+
// Create new node
|
|
336
|
+
subjectNode = { '@id': subject };
|
|
337
|
+
nodes.push(subjectNode);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Add the predicate-object
|
|
341
|
+
const predicateKey = compactPredicate(predicate);
|
|
342
|
+
const objectValue = convertToJsonLd(object);
|
|
343
|
+
|
|
344
|
+
if (subjectNode[predicateKey]) {
|
|
345
|
+
// Add to existing values
|
|
346
|
+
const existing = Array.isArray(subjectNode[predicateKey])
|
|
347
|
+
? subjectNode[predicateKey]
|
|
348
|
+
: [subjectNode[predicateKey]];
|
|
349
|
+
|
|
350
|
+
// Check if value already exists
|
|
351
|
+
if (!existing.some(v => valueMatches(v, object))) {
|
|
352
|
+
subjectNode[predicateKey] = [...existing, objectValue];
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
subjectNode[predicateKey] = objectValue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return nodes;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Check if a JSON-LD value matches an N3 object
|
|
363
|
+
*/
|
|
364
|
+
function valueMatches(jsonLdValue, n3Object) {
|
|
365
|
+
// Handle null/undefined
|
|
366
|
+
if (jsonLdValue === null || jsonLdValue === undefined) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Number comparison - N3 numbers may come as strings
|
|
371
|
+
if (typeof jsonLdValue === 'number') {
|
|
372
|
+
if (typeof n3Object === 'number') {
|
|
373
|
+
return jsonLdValue === n3Object;
|
|
374
|
+
}
|
|
375
|
+
if (typeof n3Object === 'string') {
|
|
376
|
+
return jsonLdValue === parseFloat(n3Object) || jsonLdValue.toString() === n3Object;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// String comparison
|
|
381
|
+
if (typeof jsonLdValue === 'string' && typeof n3Object === 'string') {
|
|
382
|
+
return jsonLdValue === n3Object;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// @id comparison
|
|
386
|
+
if (jsonLdValue && jsonLdValue['@id']) {
|
|
387
|
+
return jsonLdValue['@id'] === n3Object;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// @value comparison
|
|
391
|
+
if (jsonLdValue && jsonLdValue['@value']) {
|
|
392
|
+
if (typeof n3Object === 'object' && n3Object.value) {
|
|
393
|
+
return jsonLdValue['@value'] === n3Object.value;
|
|
394
|
+
}
|
|
395
|
+
return jsonLdValue['@value'] === n3Object;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Direct equality (for booleans, numbers parsed as numbers, etc.)
|
|
399
|
+
return jsonLdValue === n3Object || String(jsonLdValue) === String(n3Object);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Convert N3 object to JSON-LD value
|
|
404
|
+
*/
|
|
405
|
+
function convertToJsonLd(object) {
|
|
406
|
+
if (typeof object === 'string') {
|
|
407
|
+
// Check if it's a URI
|
|
408
|
+
if (object.startsWith('http://') || object.startsWith('https://')) {
|
|
409
|
+
return { '@id': object };
|
|
410
|
+
}
|
|
411
|
+
return object;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (typeof object === 'object') {
|
|
415
|
+
if (object.value && object.type) {
|
|
416
|
+
return { '@value': object.value, '@type': object.type };
|
|
417
|
+
}
|
|
418
|
+
if (object.value && object.language) {
|
|
419
|
+
return { '@value': object.value, '@language': object.language };
|
|
420
|
+
}
|
|
421
|
+
if (object.value) {
|
|
422
|
+
return object.value;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return object;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Expand a potentially prefixed predicate to full URI
|
|
431
|
+
*/
|
|
432
|
+
function expandPredicate(predicate) {
|
|
433
|
+
const commonPrefixes = {
|
|
434
|
+
'solid': SOLID_NS,
|
|
435
|
+
'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
436
|
+
'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
|
|
437
|
+
'foaf': 'http://xmlns.com/foaf/0.1/',
|
|
438
|
+
'dc': 'http://purl.org/dc/terms/',
|
|
439
|
+
'schema': 'http://schema.org/',
|
|
440
|
+
'ldp': 'http://www.w3.org/ns/ldp#'
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
if (predicate.includes(':')) {
|
|
444
|
+
const [prefix, local] = predicate.split(':', 2);
|
|
445
|
+
if (commonPrefixes[prefix]) {
|
|
446
|
+
return commonPrefixes[prefix] + local;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return predicate;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Compact a full URI predicate to prefixed form if possible
|
|
455
|
+
*/
|
|
456
|
+
function compactPredicate(predicate) {
|
|
457
|
+
const prefixMap = {
|
|
458
|
+
[SOLID_NS]: 'solid:',
|
|
459
|
+
'http://www.w3.org/1999/02/22-rdf-syntax-ns#': 'rdf:',
|
|
460
|
+
'http://www.w3.org/2000/01/rdf-schema#': 'rdfs:',
|
|
461
|
+
'http://xmlns.com/foaf/0.1/': 'foaf:',
|
|
462
|
+
'http://purl.org/dc/terms/': 'dc:',
|
|
463
|
+
'http://schema.org/': 'schema:',
|
|
464
|
+
'http://www.w3.org/ns/ldp#': 'ldp:'
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
for (const [uri, prefix] of Object.entries(prefixMap)) {
|
|
468
|
+
if (predicate.startsWith(uri)) {
|
|
469
|
+
return prefix + predicate.slice(uri.length);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return predicate;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Validate that a patch can be applied
|
|
478
|
+
* @param {object} document - The JSON-LD document
|
|
479
|
+
* @param {object} patch - Parsed patch
|
|
480
|
+
* @param {string} baseUri - Base URI
|
|
481
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
482
|
+
*/
|
|
483
|
+
export function validatePatch(document, patch, baseUri) {
|
|
484
|
+
// Check that all deletes exist in the document
|
|
485
|
+
for (const triple of patch.deletes) {
|
|
486
|
+
if (!tripleExists(document, triple, baseUri)) {
|
|
487
|
+
return {
|
|
488
|
+
valid: false,
|
|
489
|
+
error: `Triple to delete not found: ${JSON.stringify(triple)}`
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return { valid: true, error: null };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Check if a triple exists in a document
|
|
499
|
+
*/
|
|
500
|
+
function tripleExists(document, triple, baseUri) {
|
|
501
|
+
const nodes = document['@graph'] || [document];
|
|
502
|
+
const { subject, predicate, object } = triple;
|
|
503
|
+
|
|
504
|
+
for (const node of nodes) {
|
|
505
|
+
const nodeId = node['@id'] || '';
|
|
506
|
+
const resolvedNodeId = nodeId.startsWith('#') ? baseUri + nodeId : nodeId;
|
|
507
|
+
|
|
508
|
+
if (resolvedNodeId === subject || nodeId === subject) {
|
|
509
|
+
for (const key of Object.keys(node)) {
|
|
510
|
+
if (key.startsWith('@')) continue;
|
|
511
|
+
if (key === predicate || expandPredicate(key) === predicate) {
|
|
512
|
+
const values = Array.isArray(node[key]) ? node[key] : [node[key]];
|
|
513
|
+
if (values.some(v => valueMatches(v, object))) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return false;
|
|
522
|
+
}
|