javascript-solid-server 0.0.9 → 0.0.10
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 +95 -6
- package/benchmark.js +145 -249
- package/package.json +15 -3
- package/src/handlers/resource.js +100 -33
- package/src/patch/sparql-update.js +401 -0
- package/src/utils/conditional.js +153 -0
- package/test/conditional.test.js +250 -0
- package/test/sparql-update.test.js +219 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional Request Utilities
|
|
3
|
+
*
|
|
4
|
+
* Implements HTTP conditional request headers:
|
|
5
|
+
* - If-Match: Proceed only if ETag matches (for safe updates)
|
|
6
|
+
* - If-None-Match: Proceed only if ETag doesn't match (for caching/create-only)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalize an ETag value (remove weak prefix and quotes)
|
|
11
|
+
* @param {string} etag
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
function normalizeEtag(etag) {
|
|
15
|
+
if (!etag) return '';
|
|
16
|
+
// Remove weak prefix W/
|
|
17
|
+
let normalized = etag.replace(/^W\//, '');
|
|
18
|
+
// Remove surrounding quotes
|
|
19
|
+
normalized = normalized.replace(/^"(.*)"$/, '$1');
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse an If-Match or If-None-Match header value
|
|
25
|
+
* @param {string} headerValue
|
|
26
|
+
* @returns {string[]} Array of ETags, or ['*'] for wildcard
|
|
27
|
+
*/
|
|
28
|
+
function parseEtagHeader(headerValue) {
|
|
29
|
+
if (!headerValue) return [];
|
|
30
|
+
if (headerValue.trim() === '*') return ['*'];
|
|
31
|
+
|
|
32
|
+
// Split by comma and normalize each ETag
|
|
33
|
+
return headerValue.split(',').map(etag => normalizeEtag(etag.trim()));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check If-Match header
|
|
38
|
+
* Returns true if the request should proceed, false if it should be rejected (412)
|
|
39
|
+
*
|
|
40
|
+
* @param {string} ifMatchHeader - The If-Match header value
|
|
41
|
+
* @param {string|null} currentEtag - Current ETag of the resource (null if doesn't exist)
|
|
42
|
+
* @returns {{ ok: boolean, status?: number, error?: string }}
|
|
43
|
+
*/
|
|
44
|
+
export function checkIfMatch(ifMatchHeader, currentEtag) {
|
|
45
|
+
if (!ifMatchHeader) {
|
|
46
|
+
return { ok: true }; // No If-Match header, proceed
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const etags = parseEtagHeader(ifMatchHeader);
|
|
50
|
+
|
|
51
|
+
// If resource doesn't exist, If-Match always fails
|
|
52
|
+
if (currentEtag === null) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
status: 412,
|
|
56
|
+
error: 'Precondition Failed: Resource does not exist'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wildcard matches any existing resource
|
|
61
|
+
if (etags.includes('*')) {
|
|
62
|
+
return { ok: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if any ETag matches
|
|
66
|
+
const normalizedCurrent = normalizeEtag(currentEtag);
|
|
67
|
+
const matches = etags.some(etag => etag === normalizedCurrent);
|
|
68
|
+
|
|
69
|
+
if (!matches) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
status: 412,
|
|
73
|
+
error: 'Precondition Failed: ETag mismatch'
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { ok: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check If-None-Match header for GET/HEAD (caching)
|
|
82
|
+
* Returns true if the request should proceed, false if 304 Not Modified
|
|
83
|
+
*
|
|
84
|
+
* @param {string} ifNoneMatchHeader - The If-None-Match header value
|
|
85
|
+
* @param {string|null} currentEtag - Current ETag of the resource
|
|
86
|
+
* @returns {{ ok: boolean, notModified?: boolean }}
|
|
87
|
+
*/
|
|
88
|
+
export function checkIfNoneMatchForGet(ifNoneMatchHeader, currentEtag) {
|
|
89
|
+
if (!ifNoneMatchHeader || currentEtag === null) {
|
|
90
|
+
return { ok: true }; // No header or no resource, proceed
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const etags = parseEtagHeader(ifNoneMatchHeader);
|
|
94
|
+
|
|
95
|
+
// Wildcard matches any existing resource
|
|
96
|
+
if (etags.includes('*')) {
|
|
97
|
+
return { ok: false, notModified: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if any ETag matches
|
|
101
|
+
const normalizedCurrent = normalizeEtag(currentEtag);
|
|
102
|
+
const matches = etags.some(etag => etag === normalizedCurrent);
|
|
103
|
+
|
|
104
|
+
if (matches) {
|
|
105
|
+
return { ok: false, notModified: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { ok: true };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check If-None-Match header for PUT/POST (create-only semantics)
|
|
113
|
+
* Returns true if the request should proceed, false if 412 Precondition Failed
|
|
114
|
+
*
|
|
115
|
+
* @param {string} ifNoneMatchHeader - The If-None-Match header value
|
|
116
|
+
* @param {string|null} currentEtag - Current ETag of the resource (null if doesn't exist)
|
|
117
|
+
* @returns {{ ok: boolean, status?: number, error?: string }}
|
|
118
|
+
*/
|
|
119
|
+
export function checkIfNoneMatchForWrite(ifNoneMatchHeader, currentEtag) {
|
|
120
|
+
if (!ifNoneMatchHeader) {
|
|
121
|
+
return { ok: true }; // No header, proceed
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const etags = parseEtagHeader(ifNoneMatchHeader);
|
|
125
|
+
|
|
126
|
+
// If-None-Match: * means "only if resource doesn't exist"
|
|
127
|
+
if (etags.includes('*')) {
|
|
128
|
+
if (currentEtag !== null) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
status: 412,
|
|
132
|
+
error: 'Precondition Failed: Resource already exists'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if any ETag matches (if so, fail)
|
|
139
|
+
if (currentEtag !== null) {
|
|
140
|
+
const normalizedCurrent = normalizeEtag(currentEtag);
|
|
141
|
+
const matches = etags.some(etag => etag === normalizedCurrent);
|
|
142
|
+
|
|
143
|
+
if (matches) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
status: 412,
|
|
147
|
+
error: 'Precondition Failed: ETag matches'
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { ok: true };
|
|
153
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional Request Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests If-Match and If-None-Match header support.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, before, after } from 'node:test';
|
|
8
|
+
import assert from 'node:assert';
|
|
9
|
+
import {
|
|
10
|
+
startTestServer,
|
|
11
|
+
stopTestServer,
|
|
12
|
+
request,
|
|
13
|
+
createTestPod,
|
|
14
|
+
assertStatus
|
|
15
|
+
} from './helpers.js';
|
|
16
|
+
|
|
17
|
+
describe('Conditional Requests', () => {
|
|
18
|
+
before(async () => {
|
|
19
|
+
await startTestServer();
|
|
20
|
+
await createTestPod('condtest');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
after(async () => {
|
|
24
|
+
await stopTestServer();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('If-None-Match on GET', () => {
|
|
28
|
+
it('should return 304 Not Modified when ETag matches', async () => {
|
|
29
|
+
// Create a resource
|
|
30
|
+
await request('/condtest/public/etag-test.json', {
|
|
31
|
+
method: 'PUT',
|
|
32
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
33
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/value': 1 }),
|
|
34
|
+
auth: 'condtest'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Get the ETag
|
|
38
|
+
const res1 = await request('/condtest/public/etag-test.json');
|
|
39
|
+
const etag = res1.headers.get('ETag');
|
|
40
|
+
assert.ok(etag, 'Response should have ETag');
|
|
41
|
+
|
|
42
|
+
// Request with matching If-None-Match
|
|
43
|
+
const res2 = await request('/condtest/public/etag-test.json', {
|
|
44
|
+
headers: { 'If-None-Match': etag }
|
|
45
|
+
});
|
|
46
|
+
assertStatus(res2, 304);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return 200 when ETag does not match', async () => {
|
|
50
|
+
const res = await request('/condtest/public/etag-test.json', {
|
|
51
|
+
headers: { 'If-None-Match': '"different-etag"' }
|
|
52
|
+
});
|
|
53
|
+
assertStatus(res, 200);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return 304 with If-None-Match: *', async () => {
|
|
57
|
+
const res = await request('/condtest/public/etag-test.json', {
|
|
58
|
+
headers: { 'If-None-Match': '*' }
|
|
59
|
+
});
|
|
60
|
+
assertStatus(res, 304);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('If-Match on PUT', () => {
|
|
65
|
+
it('should succeed when ETag matches', async () => {
|
|
66
|
+
// Create resource
|
|
67
|
+
await request('/condtest/public/match-test.json', {
|
|
68
|
+
method: 'PUT',
|
|
69
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
70
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/v': 1 }),
|
|
71
|
+
auth: 'condtest'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Get ETag
|
|
75
|
+
const res1 = await request('/condtest/public/match-test.json');
|
|
76
|
+
const etag = res1.headers.get('ETag');
|
|
77
|
+
|
|
78
|
+
// Update with matching If-Match
|
|
79
|
+
const res2 = await request('/condtest/public/match-test.json', {
|
|
80
|
+
method: 'PUT',
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/ld+json',
|
|
83
|
+
'If-Match': etag
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/v': 2 }),
|
|
86
|
+
auth: 'condtest'
|
|
87
|
+
});
|
|
88
|
+
assertStatus(res2, 204);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return 412 when ETag does not match', async () => {
|
|
92
|
+
const res = await request('/condtest/public/match-test.json', {
|
|
93
|
+
method: 'PUT',
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/ld+json',
|
|
96
|
+
'If-Match': '"wrong-etag"'
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/v': 3 }),
|
|
99
|
+
auth: 'condtest'
|
|
100
|
+
});
|
|
101
|
+
assertStatus(res, 412);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should succeed with If-Match: * on existing resource', async () => {
|
|
105
|
+
const res = await request('/condtest/public/match-test.json', {
|
|
106
|
+
method: 'PUT',
|
|
107
|
+
headers: {
|
|
108
|
+
'Content-Type': 'application/ld+json',
|
|
109
|
+
'If-Match': '*'
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/v': 4 }),
|
|
112
|
+
auth: 'condtest'
|
|
113
|
+
});
|
|
114
|
+
assertStatus(res, 204);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return 412 with If-Match: * on non-existent resource', async () => {
|
|
118
|
+
const res = await request('/condtest/public/nonexistent.json', {
|
|
119
|
+
method: 'PUT',
|
|
120
|
+
headers: {
|
|
121
|
+
'Content-Type': 'application/ld+json',
|
|
122
|
+
'If-Match': '*'
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({ '@id': '#new' }),
|
|
125
|
+
auth: 'condtest'
|
|
126
|
+
});
|
|
127
|
+
assertStatus(res, 412);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('If-None-Match on PUT', () => {
|
|
132
|
+
it('should succeed with If-None-Match: * on new resource', async () => {
|
|
133
|
+
const res = await request('/condtest/public/create-only.json', {
|
|
134
|
+
method: 'PUT',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/ld+json',
|
|
137
|
+
'If-None-Match': '*'
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify({ '@id': '#new' }),
|
|
140
|
+
auth: 'condtest'
|
|
141
|
+
});
|
|
142
|
+
assertStatus(res, 201);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return 412 with If-None-Match: * on existing resource', async () => {
|
|
146
|
+
const res = await request('/condtest/public/create-only.json', {
|
|
147
|
+
method: 'PUT',
|
|
148
|
+
headers: {
|
|
149
|
+
'Content-Type': 'application/ld+json',
|
|
150
|
+
'If-None-Match': '*'
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({ '@id': '#update' }),
|
|
153
|
+
auth: 'condtest'
|
|
154
|
+
});
|
|
155
|
+
assertStatus(res, 412);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('If-Match on DELETE', () => {
|
|
160
|
+
it('should succeed when ETag matches', async () => {
|
|
161
|
+
// Create resource
|
|
162
|
+
await request('/condtest/public/delete-match.json', {
|
|
163
|
+
method: 'PUT',
|
|
164
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
165
|
+
body: JSON.stringify({ '@id': '#delete' }),
|
|
166
|
+
auth: 'condtest'
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Get ETag
|
|
170
|
+
const res1 = await request('/condtest/public/delete-match.json');
|
|
171
|
+
const etag = res1.headers.get('ETag');
|
|
172
|
+
|
|
173
|
+
// Delete with matching If-Match
|
|
174
|
+
const res2 = await request('/condtest/public/delete-match.json', {
|
|
175
|
+
method: 'DELETE',
|
|
176
|
+
headers: { 'If-Match': etag },
|
|
177
|
+
auth: 'condtest'
|
|
178
|
+
});
|
|
179
|
+
assertStatus(res2, 204);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should return 412 when ETag does not match', async () => {
|
|
183
|
+
// Create resource
|
|
184
|
+
await request('/condtest/public/delete-nomatch.json', {
|
|
185
|
+
method: 'PUT',
|
|
186
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
187
|
+
body: JSON.stringify({ '@id': '#delete' }),
|
|
188
|
+
auth: 'condtest'
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const res = await request('/condtest/public/delete-nomatch.json', {
|
|
192
|
+
method: 'DELETE',
|
|
193
|
+
headers: { 'If-Match': '"wrong-etag"' },
|
|
194
|
+
auth: 'condtest'
|
|
195
|
+
});
|
|
196
|
+
assertStatus(res, 412);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('If-Match on PATCH', () => {
|
|
201
|
+
it('should succeed when ETag matches', async () => {
|
|
202
|
+
// Create resource
|
|
203
|
+
await request('/condtest/public/patch-match.json', {
|
|
204
|
+
method: 'PUT',
|
|
205
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
206
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/name': 'Original' }),
|
|
207
|
+
auth: 'condtest'
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Get ETag
|
|
211
|
+
const res1 = await request('/condtest/public/patch-match.json');
|
|
212
|
+
const etag = res1.headers.get('ETag');
|
|
213
|
+
|
|
214
|
+
// Patch with matching If-Match
|
|
215
|
+
const patch = `
|
|
216
|
+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
|
217
|
+
_:patch a solid:InsertDeletePatch;
|
|
218
|
+
solid:inserts { <#test> <http://example.org/updated> "true" }.
|
|
219
|
+
`;
|
|
220
|
+
const res2 = await request('/condtest/public/patch-match.json', {
|
|
221
|
+
method: 'PATCH',
|
|
222
|
+
headers: {
|
|
223
|
+
'Content-Type': 'text/n3',
|
|
224
|
+
'If-Match': etag
|
|
225
|
+
},
|
|
226
|
+
body: patch,
|
|
227
|
+
auth: 'condtest'
|
|
228
|
+
});
|
|
229
|
+
assertStatus(res2, 204);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should return 412 when ETag does not match', async () => {
|
|
233
|
+
const patch = `
|
|
234
|
+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
|
|
235
|
+
_:patch a solid:InsertDeletePatch;
|
|
236
|
+
solid:inserts { <#test> <http://example.org/bad> "true" }.
|
|
237
|
+
`;
|
|
238
|
+
const res = await request('/condtest/public/patch-match.json', {
|
|
239
|
+
method: 'PATCH',
|
|
240
|
+
headers: {
|
|
241
|
+
'Content-Type': 'text/n3',
|
|
242
|
+
'If-Match': '"wrong-etag"'
|
|
243
|
+
},
|
|
244
|
+
body: patch,
|
|
245
|
+
auth: 'condtest'
|
|
246
|
+
});
|
|
247
|
+
assertStatus(res, 412);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPARQL Update Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests SPARQL Update support via PATCH method.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, before, after } from 'node:test';
|
|
8
|
+
import assert from 'node:assert';
|
|
9
|
+
import {
|
|
10
|
+
startTestServer,
|
|
11
|
+
stopTestServer,
|
|
12
|
+
request,
|
|
13
|
+
createTestPod,
|
|
14
|
+
assertStatus
|
|
15
|
+
} from './helpers.js';
|
|
16
|
+
|
|
17
|
+
describe('SPARQL Update', () => {
|
|
18
|
+
before(async () => {
|
|
19
|
+
await startTestServer();
|
|
20
|
+
await createTestPod('sparqltest');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
after(async () => {
|
|
24
|
+
await stopTestServer();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('INSERT DATA', () => {
|
|
28
|
+
it('should insert a triple into existing resource', async () => {
|
|
29
|
+
// Create a resource
|
|
30
|
+
await request('/sparqltest/public/insert-test.json', {
|
|
31
|
+
method: 'PUT',
|
|
32
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
33
|
+
body: JSON.stringify({ '@id': '#item', 'http://example.org/name': 'Original' }),
|
|
34
|
+
auth: 'sparqltest'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Insert new data via SPARQL Update
|
|
38
|
+
const sparql = `
|
|
39
|
+
PREFIX ex: <http://example.org/>
|
|
40
|
+
INSERT DATA {
|
|
41
|
+
<#item> ex:status "active" .
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const res = await request('/sparqltest/public/insert-test.json', {
|
|
46
|
+
method: 'PATCH',
|
|
47
|
+
headers: { 'Content-Type': 'application/sparql-update' },
|
|
48
|
+
body: sparql,
|
|
49
|
+
auth: 'sparqltest'
|
|
50
|
+
});
|
|
51
|
+
assertStatus(res, 204);
|
|
52
|
+
|
|
53
|
+
// Verify the data was inserted
|
|
54
|
+
const getRes = await request('/sparqltest/public/insert-test.json');
|
|
55
|
+
const data = await getRes.json();
|
|
56
|
+
assert.strictEqual(data['http://example.org/status'], 'active');
|
|
57
|
+
assert.strictEqual(data['http://example.org/name'], 'Original');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should insert multiple triples', async () => {
|
|
61
|
+
await request('/sparqltest/public/multi-insert.json', {
|
|
62
|
+
method: 'PUT',
|
|
63
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
64
|
+
body: JSON.stringify({ '@id': '#thing' }),
|
|
65
|
+
auth: 'sparqltest'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const sparql = `
|
|
69
|
+
PREFIX ex: <http://example.org/>
|
|
70
|
+
INSERT DATA {
|
|
71
|
+
<#thing> ex:prop1 "value1" .
|
|
72
|
+
<#thing> ex:prop2 "value2" .
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const res = await request('/sparqltest/public/multi-insert.json', {
|
|
77
|
+
method: 'PATCH',
|
|
78
|
+
headers: { 'Content-Type': 'application/sparql-update' },
|
|
79
|
+
body: sparql,
|
|
80
|
+
auth: 'sparqltest'
|
|
81
|
+
});
|
|
82
|
+
assertStatus(res, 204);
|
|
83
|
+
|
|
84
|
+
const getRes = await request('/sparqltest/public/multi-insert.json');
|
|
85
|
+
const data = await getRes.json();
|
|
86
|
+
assert.strictEqual(data['http://example.org/prop1'], 'value1');
|
|
87
|
+
assert.strictEqual(data['http://example.org/prop2'], 'value2');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('DELETE DATA', () => {
|
|
92
|
+
it('should delete a triple from existing resource', async () => {
|
|
93
|
+
await request('/sparqltest/public/delete-test.json', {
|
|
94
|
+
method: 'PUT',
|
|
95
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
'@id': '#item',
|
|
98
|
+
'http://example.org/keep': 'yes',
|
|
99
|
+
'http://example.org/remove': 'this'
|
|
100
|
+
}),
|
|
101
|
+
auth: 'sparqltest'
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const sparql = `
|
|
105
|
+
PREFIX ex: <http://example.org/>
|
|
106
|
+
DELETE DATA {
|
|
107
|
+
<#item> ex:remove "this" .
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
const res = await request('/sparqltest/public/delete-test.json', {
|
|
112
|
+
method: 'PATCH',
|
|
113
|
+
headers: { 'Content-Type': 'application/sparql-update' },
|
|
114
|
+
body: sparql,
|
|
115
|
+
auth: 'sparqltest'
|
|
116
|
+
});
|
|
117
|
+
assertStatus(res, 204);
|
|
118
|
+
|
|
119
|
+
const getRes = await request('/sparqltest/public/delete-test.json');
|
|
120
|
+
const data = await getRes.json();
|
|
121
|
+
assert.strictEqual(data['http://example.org/keep'], 'yes');
|
|
122
|
+
assert.strictEqual(data['http://example.org/remove'], undefined);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('DELETE/INSERT WHERE', () => {
|
|
127
|
+
it('should delete and insert in single operation', async () => {
|
|
128
|
+
await request('/sparqltest/public/update-test.json', {
|
|
129
|
+
method: 'PUT',
|
|
130
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
'@id': '#item',
|
|
133
|
+
'http://example.org/version': '1'
|
|
134
|
+
}),
|
|
135
|
+
auth: 'sparqltest'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const sparql = `
|
|
139
|
+
PREFIX ex: <http://example.org/>
|
|
140
|
+
DELETE { <#item> ex:version "1" }
|
|
141
|
+
INSERT { <#item> ex:version "2" }
|
|
142
|
+
WHERE { <#item> ex:version "1" }
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const res = await request('/sparqltest/public/update-test.json', {
|
|
146
|
+
method: 'PATCH',
|
|
147
|
+
headers: { 'Content-Type': 'application/sparql-update' },
|
|
148
|
+
body: sparql,
|
|
149
|
+
auth: 'sparqltest'
|
|
150
|
+
});
|
|
151
|
+
assertStatus(res, 204);
|
|
152
|
+
|
|
153
|
+
const getRes = await request('/sparqltest/public/update-test.json');
|
|
154
|
+
const data = await getRes.json();
|
|
155
|
+
assert.strictEqual(data['http://example.org/version'], '2');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('Error handling', () => {
|
|
160
|
+
it('should return 404 for non-existent resource', async () => {
|
|
161
|
+
const sparql = `INSERT DATA { <#x> <http://example.org/p> "v" }`;
|
|
162
|
+
const res = await request('/sparqltest/public/nonexistent.json', {
|
|
163
|
+
method: 'PATCH',
|
|
164
|
+
headers: { 'Content-Type': 'application/sparql-update' },
|
|
165
|
+
body: sparql,
|
|
166
|
+
auth: 'sparqltest'
|
|
167
|
+
});
|
|
168
|
+
assertStatus(res, 404);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should return 415 for unsupported content type', async () => {
|
|
172
|
+
await request('/sparqltest/public/content-type-test.json', {
|
|
173
|
+
method: 'PUT',
|
|
174
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
175
|
+
body: JSON.stringify({ '@id': '#test' }),
|
|
176
|
+
auth: 'sparqltest'
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const res = await request('/sparqltest/public/content-type-test.json', {
|
|
180
|
+
method: 'PATCH',
|
|
181
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
182
|
+
body: 'not a valid patch',
|
|
183
|
+
auth: 'sparqltest'
|
|
184
|
+
});
|
|
185
|
+
assertStatus(res, 415);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Typed literals', () => {
|
|
190
|
+
it('should handle integer literals', async () => {
|
|
191
|
+
await request('/sparqltest/public/typed-test.json', {
|
|
192
|
+
method: 'PUT',
|
|
193
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
194
|
+
body: JSON.stringify({ '@id': '#data' }),
|
|
195
|
+
auth: 'sparqltest'
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const sparql = `
|
|
199
|
+
PREFIX ex: <http://example.org/>
|
|
200
|
+
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
|
|
201
|
+
INSERT DATA {
|
|
202
|
+
<#data> ex:count "42"^^xsd:integer .
|
|
203
|
+
}
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
const res = await request('/sparqltest/public/typed-test.json', {
|
|
207
|
+
method: 'PATCH',
|
|
208
|
+
headers: { 'Content-Type': 'application/sparql-update' },
|
|
209
|
+
body: sparql,
|
|
210
|
+
auth: 'sparqltest'
|
|
211
|
+
});
|
|
212
|
+
assertStatus(res, 204);
|
|
213
|
+
|
|
214
|
+
const getRes = await request('/sparqltest/public/typed-test.json');
|
|
215
|
+
const data = await getRes.json();
|
|
216
|
+
assert.ok(data['http://example.org/count']);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|