Skip to content

Commit c463d73

Browse files
committed
Add WHERE support to N3 patches.
1 parent 1d55d0e commit c463d73

File tree

2 files changed

+277
-23
lines changed

2 files changed

+277
-23
lines changed

lib/handlers/patch/n3-patcher.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,22 @@ function parsePatchDocument (targetURI, patchURI, patchText, patchKB) {
3232

3333
// Query the N3 document for insertions and deletions
3434
.then(patchGraph => queryForFirstResult(patchGraph, `${PREFIXES}
35-
SELECT ?insert ?delete WHERE {
35+
SELECT ?insert ?delete ?where WHERE {
3636
?patch p:patches <${targetURI}>.
3737
OPTIONAL { ?patch p:insert ?insert. }
3838
OPTIONAL { ?patch p:delete ?delete. }
39+
OPTIONAL { ?patch p:where ?where. }
3940
}`)
4041
.catch(err => { throw error(400, `No patch for ${targetURI} found.`, err) })
4142
)
4243

4344
// Return the insertions and deletions as an rdflib patch document
4445
.then(result => {
45-
const inserts = result['?insert']
46-
const deletes = result['?delete']
47-
if (!inserts && !deletes) {
46+
const {'?insert': insert, '?delete': deleted, '?where': where} = result
47+
if (!insert && !deleted) {
4848
throw error(400, 'Patch should at least contain inserts or deletes.')
4949
}
50-
return {insert: inserts, delete: deletes}
50+
return {insert, delete: deleted, where}
5151
})
5252
}
5353

test/integration/patch.js

Lines changed: 272 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,158 @@ describe('PATCH', () => {
180180
})
181181
})
182182

183+
describe('inserting (= append with WHERE)', () => {
184+
describe('on a resource with read-only access', () => {
185+
it('returns a 403', () =>
186+
request.patch('/read-only.ttl')
187+
.set('Authorization', `Bearer ${userCredentials}`)
188+
.set('Content-Type', 'text/n3')
189+
.send(n3Patch(`
190+
<> p:patches <https://tim.localhost:7777/read-only.ttl>;
191+
p:insert { ?a <y> <z>. };
192+
p:where { ?a <b> <c>. }.`
193+
))
194+
.expect(403)
195+
.then(response => {
196+
assert.include(response.text, 'Access denied')
197+
})
198+
)
199+
200+
it('does not modify the file', () => {
201+
assert.equal(read('patch/read-only.ttl'),
202+
'<a> <b> <c>.\n<d> <e> <f>.\n')
203+
})
204+
})
205+
206+
describe('on a non-existing file', () => {
207+
after(() => rm('patch/new.ttl'))
208+
209+
it('returns a 409', () =>
210+
request.patch('/new.ttl')
211+
.set('Authorization', `Bearer ${userCredentials}`)
212+
.set('Content-Type', 'text/n3')
213+
.send(n3Patch(`
214+
<> p:patches <https://tim.localhost:7777/new.ttl>;
215+
p:insert { ?a <y> <z>. };
216+
p:where { ?a <b> <c>. }.`
217+
))
218+
.expect(409)
219+
.then(response => {
220+
assert.include(response.text, 'The patch could not be applied')
221+
})
222+
)
223+
224+
it('does not create the file', () => {
225+
assert.isFalse(fs.existsSync('patch/new.ttl'))
226+
})
227+
})
228+
229+
describe.skip('on a resource with append-only access', () => {
230+
before(() => backup('patch/append-only.ttl'))
231+
after(() => restore('patch/append-only.ttl'))
232+
233+
it('returns a 403', () =>
234+
request.patch('/append-only.ttl')
235+
.set('Authorization', `Bearer ${userCredentials}`)
236+
.set('Content-Type', 'text/n3')
237+
.send(n3Patch(`
238+
<> p:patches <https://tim.localhost:7777/append-only.ttl>;
239+
p:insert { ?a <y> <z>. };
240+
p:where { ?a <b> <c>. }.`
241+
))
242+
.expect(403)
243+
.then(response => {
244+
assert.include(response.text, 'Access denied')
245+
})
246+
)
247+
248+
it('does not modify the file', () => {
249+
assert.equal(read('patch/append-only.ttl'),
250+
'<a> <b> <c>.\n<d> <e> <f>.\n')
251+
})
252+
})
253+
254+
describe.skip('on a resource with write-only access', () => {
255+
before(() => backup('patch/write-only.ttl'))
256+
after(() => restore('patch/write-only.ttl'))
257+
258+
// Allowing the delete would either return 200 or 409,
259+
// thereby incorrectly giving the user (guess-based) read access;
260+
// therefore, we need to return 403.
261+
it('returns a 403', () =>
262+
request.patch('/write-only.ttl')
263+
.set('Authorization', `Bearer ${userCredentials}`)
264+
.set('Content-Type', 'text/n3')
265+
.send(n3Patch(`
266+
<> p:patches <https://tim.localhost:7777/write-only.ttl>;
267+
p:insert { ?a <y> <z>. };
268+
p:where { ?a <b> <c>. }.`
269+
))
270+
.expect(403)
271+
.then(response => {
272+
assert.include(response.text, 'Access denied')
273+
})
274+
)
275+
276+
it('does not modify the file', () => {
277+
assert.equal(read('patch/append-only.ttl'),
278+
'<a> <b> <c>.\n<d> <e> <f>.\n')
279+
})
280+
})
281+
282+
describe('on a resource with read-write access', () => {
283+
describe('with a matching WHERE clause', () => {
284+
before(() => backup('patch/read-write.ttl'))
285+
after(() => restore('patch/read-write.ttl'))
286+
287+
it('returns a 200', () =>
288+
request.patch('/read-write.ttl')
289+
.set('Authorization', `Bearer ${userCredentials}`)
290+
.set('Content-Type', 'text/n3')
291+
.send(n3Patch(`
292+
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
293+
p:where { ?a <b> <c>. };
294+
p:insert { ?a <y> <z>. }.`
295+
))
296+
.expect(200)
297+
.then(response => {
298+
assert.include(response.text, 'Patch applied successfully')
299+
})
300+
)
301+
302+
it('patches the file', () => {
303+
assert.equal(read('patch/read-write.ttl'),
304+
'@prefix : </read-write.ttl#>.\n@prefix tim: </>.\n\ntim:a tim:b tim:c; tim:y tim:z.\n\ntim:d tim:e tim:f.\n\n')
305+
})
306+
})
307+
308+
describe('with a non-matching WHERE clause', () => {
309+
before(() => backup('patch/read-write.ttl'))
310+
after(() => restore('patch/read-write.ttl'))
311+
312+
it('returns a 409', () =>
313+
request.patch('/read-write.ttl')
314+
.set('Authorization', `Bearer ${userCredentials}`)
315+
.set('Content-Type', 'text/n3')
316+
.send(n3Patch(`
317+
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
318+
p:where { ?a <y> <z>. };
319+
p:insert { ?a <s> <t>. }.`
320+
))
321+
.expect(409)
322+
.then(response => {
323+
assert.include(response.text, 'The patch could not be applied')
324+
})
325+
)
326+
327+
it('does not change the file', () => {
328+
assert.equal(read('patch/read-write.ttl'),
329+
'<a> <b> <c>.\n<d> <e> <f>.\n')
330+
})
331+
})
332+
})
333+
})
334+
183335
describe('deleting', () => {
184336
describe('on a resource with read-only access', () => {
185337
it('returns a 403', () =>
@@ -323,10 +475,60 @@ describe('PATCH', () => {
323475
'<a> <b> <c>.\n<d> <e> <f>.\n')
324476
})
325477
})
478+
479+
describe('with a matching WHERE clause', () => {
480+
before(() => backup('patch/read-write.ttl'))
481+
after(() => restore('patch/read-write.ttl'))
482+
483+
it('returns a 200', () =>
484+
request.patch('/read-write.ttl')
485+
.set('Authorization', `Bearer ${userCredentials}`)
486+
.set('Content-Type', 'text/n3')
487+
.send(n3Patch(`
488+
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
489+
p:where { ?a <b> <c>. };
490+
p:delete { ?a <b> <c>. }.`
491+
))
492+
.expect(200)
493+
.then(response => {
494+
assert.include(response.text, 'Patch applied successfully')
495+
})
496+
)
497+
498+
it('patches the file', () => {
499+
assert.equal(read('patch/read-write.ttl'),
500+
'@prefix : </read-write.ttl#>.\n@prefix tim: </>.\n\ntim:d tim:e tim:f.\n\n')
501+
})
502+
})
503+
504+
describe('with a non-matching WHERE clause', () => {
505+
before(() => backup('patch/read-write.ttl'))
506+
after(() => restore('patch/read-write.ttl'))
507+
508+
it('returns a 409', () =>
509+
request.patch('/read-write.ttl')
510+
.set('Authorization', `Bearer ${userCredentials}`)
511+
.set('Content-Type', 'text/n3')
512+
.send(n3Patch(`
513+
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
514+
p:where { ?a <y> <z>. };
515+
p:delete { ?a <b> <c>. }.`
516+
))
517+
.expect(409)
518+
.then(response => {
519+
assert.include(response.text, 'The patch could not be applied')
520+
})
521+
)
522+
523+
it('does not change the file', () => {
524+
assert.equal(read('patch/read-write.ttl'),
525+
'<a> <b> <c>.\n<d> <e> <f>.\n')
526+
})
527+
})
326528
})
327529
})
328530

329-
describe('deleting and inserting', () => {
531+
describe('deleting and appending/inserting', () => {
330532
describe('on a resource with read-only access', () => {
331533
it('returns a 403', () =>
332534
request.patch('/read-only.ttl')
@@ -426,6 +628,21 @@ describe('PATCH', () => {
426628
})
427629

428630
describe('on a resource with read-write access', () => {
631+
it('executes deletes before inserts', () =>
632+
request.patch('/read-write.ttl')
633+
.set('Authorization', `Bearer ${userCredentials}`)
634+
.set('Content-Type', 'text/n3')
635+
.send(n3Patch(`
636+
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
637+
p:insert { <x> <y> <z>. };
638+
p:delete { <x> <y> <z>. }.`
639+
))
640+
.expect(409)
641+
.then(response => {
642+
assert.include(response.text, 'The patch could not be applied')
643+
})
644+
)
645+
429646
describe('with a patch for existing data', () => {
430647
before(() => backup('patch/read-write.ttl'))
431648
after(() => restore('patch/read-write.ttl'))
@@ -436,8 +653,8 @@ describe('PATCH', () => {
436653
.set('Content-Type', 'text/n3')
437654
.send(n3Patch(`
438655
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
439-
p:insert { <x> <y> <z>. };
440-
p:delete { <a> <b> <c>. }.`
656+
p:insert { <x> <y> <z>. };
657+
p:delete { <a> <b> <c>. }.`
441658
))
442659
.expect(200)
443660
.then(response => {
@@ -461,8 +678,8 @@ describe('PATCH', () => {
461678
.set('Content-Type', 'text/n3')
462679
.send(n3Patch(`
463680
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
464-
p:insert { <x> <y> <z>. };
465-
p:delete { <q> <s> <s>. }.`
681+
p:insert { <x> <y> <z>. };
682+
p:delete { <q> <s> <s>. }.`
466683
))
467684
.expect(409)
468685
.then(response => {
@@ -476,20 +693,57 @@ describe('PATCH', () => {
476693
})
477694
})
478695

479-
it('executes deletes before inserts', () =>
480-
request.patch('/read-write.ttl')
481-
.set('Authorization', `Bearer ${userCredentials}`)
482-
.set('Content-Type', 'text/n3')
483-
.send(n3Patch(`
484-
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
485-
p:insert { <x> <y> <z>. };
486-
p:delete { <x> <y> <z>. }.`
487-
))
488-
.expect(409)
489-
.then(response => {
490-
assert.include(response.text, 'The patch could not be applied')
491-
})
696+
describe('with a matching WHERE clause', () => {
697+
before(() => backup('patch/read-write.ttl'))
698+
after(() => restore('patch/read-write.ttl'))
699+
700+
it('returns a 200', () =>
701+
request.patch('/read-write.ttl')
702+
.set('Authorization', `Bearer ${userCredentials}`)
703+
.set('Content-Type', 'text/n3')
704+
.send(n3Patch(`
705+
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
706+
p:where { ?a <b> <c>. };
707+
p:insert { ?a <y> <z>. };
708+
p:delete { ?a <b> <c>. }.`
709+
))
710+
.expect(200)
711+
.then(response => {
712+
assert.include(response.text, 'Patch applied successfully')
713+
})
492714
)
715+
716+
it('patches the file', () => {
717+
assert.equal(read('patch/read-write.ttl'),
718+
'@prefix : </read-write.ttl#>.\n@prefix tim: </>.\n\ntim:a tim:y tim:z.\n\ntim:d tim:e tim:f.\n\n')
719+
})
720+
})
721+
722+
describe('with a non-matching WHERE clause', () => {
723+
before(() => backup('patch/read-write.ttl'))
724+
after(() => restore('patch/read-write.ttl'))
725+
726+
it('returns a 409', () =>
727+
request.patch('/read-write.ttl')
728+
.set('Authorization', `Bearer ${userCredentials}`)
729+
.set('Content-Type', 'text/n3')
730+
.send(n3Patch(`
731+
<> p:patches <https://tim.localhost:7777/read-write.ttl>;
732+
p:where { ?a <y> <z>. };
733+
p:insert { ?a <y> <z>. };
734+
p:delete { ?a <b> <c>. }.`
735+
))
736+
.expect(409)
737+
.then(response => {
738+
assert.include(response.text, 'The patch could not be applied')
739+
})
740+
)
741+
742+
it('does not change the file', () => {
743+
assert.equal(read('patch/read-write.ttl'),
744+
'<a> <b> <c>.\n<d> <e> <f>.\n')
745+
})
746+
})
493747
})
494748
})
495749
})

0 commit comments

Comments
 (0)