From 8b7a30ce4ff80a1b9b13f110fb3103e2cc68ccd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Sat, 29 Aug 2020 12:01:46 +0200 Subject: [PATCH 1/6] Expose ETag on single object get methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Controller/BoardApiController.php | 4 +++- lib/Controller/CardApiController.php | 4 +++- lib/Controller/StackApiController.php | 4 +++- lib/Db/Board.php | 4 ++++ lib/Db/Card.php | 4 ++++ lib/Db/Stack.php | 4 ++++ 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/Controller/BoardApiController.php b/lib/Controller/BoardApiController.php index 647072353..d7afc6dcc 100644 --- a/lib/Controller/BoardApiController.php +++ b/lib/Controller/BoardApiController.php @@ -85,7 +85,9 @@ class BoardApiController extends ApiController { */ public function get() { $board = $this->boardService->find($this->request->getParam('boardId')); - return new DataResponse($board, HTTP::STATUS_OK); + $response = new DataResponse($board, HTTP::STATUS_OK); + $response->setETag($board->getEtag()); + return $response; } /** diff --git a/lib/Controller/CardApiController.php b/lib/Controller/CardApiController.php index c9f47fb93..04d0fe4c9 100644 --- a/lib/Controller/CardApiController.php +++ b/lib/Controller/CardApiController.php @@ -64,7 +64,9 @@ class CardApiController extends ApiController { */ public function get() { $card = $this->cardService->find($this->request->getParam('cardId')); - return new DataResponse($card, HTTP::STATUS_OK); + $response = new DataResponse($card, HTTP::STATUS_OK); + $response->setETag($card->getEtag()); + return $response; } /** diff --git a/lib/Controller/StackApiController.php b/lib/Controller/StackApiController.php index 61d4c6afe..d39bd7f5b 100644 --- a/lib/Controller/StackApiController.php +++ b/lib/Controller/StackApiController.php @@ -83,7 +83,9 @@ class StackApiController extends ApiController { */ public function get() { $stack = $this->stackService->find($this->request->getParam('stackId')); - return new DataResponse($stack, HTTP::STATUS_OK); + $response = new DataResponse($stack, HTTP::STATUS_OK); + $response->setETag($stack->getETag()); + return $response; } /** diff --git a/lib/Db/Board.php b/lib/Db/Board.php index eb5ec1d07..31caa5b27 100644 --- a/lib/Db/Board.php +++ b/lib/Db/Board.php @@ -81,4 +81,8 @@ class Board extends RelationalEntity { $this->acl[] = $a; } } + + public function getETag() { + return md5((string)$this->getLastModified()); + } } diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 4e24719ba..fe91387ae 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -155,4 +155,8 @@ class Card extends RelationalEntity { public function getCalendarPrefix(): string { return 'card'; } + + public function getETag() { + return md5((string)$this->getLastModified()); + } } diff --git a/lib/Db/Stack.php b/lib/Db/Stack.php index 9790e6b7c..c414f52a8 100644 --- a/lib/Db/Stack.php +++ b/lib/Db/Stack.php @@ -65,4 +65,8 @@ class Stack extends RelationalEntity { public function getCalendarPrefix(): string { return 'stack'; } + + public function getETag() { + return md5((string)$this->getLastModified()); + } } From d916ef191af28ef85129d3ab7076ef923d29fd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Sat, 29 Aug 2020 12:02:17 +0200 Subject: [PATCH 2/6] Update card modification date on label/user assignment changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Service/AssignmentService.php | 4 ++-- lib/Service/CardService.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Service/AssignmentService.php b/lib/Service/AssignmentService.php index b3210bc44..929a11a70 100644 --- a/lib/Service/AssignmentService.php +++ b/lib/Service/AssignmentService.php @@ -148,7 +148,7 @@ class AssignmentService { $assignment->setParticipant($userId); $assignment->setType($type); $assignment = $this->assignedUsersMapper->insert($assignment); - $this->changeHelper->cardChanged($cardId, false); + $this->changeHelper->cardChanged($cardId); $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_ASSIGN, ['assigneduser' => $userId]); $this->eventDispatcher->dispatch( @@ -185,7 +185,7 @@ class AssignmentService { $assignment = $this->assignedUsersMapper->delete($assignment); $card = $this->cardMapper->find($cardId); $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_UNASSIGN, ['assigneduser' => $userId]); - $this->changeHelper->cardChanged($cardId, false); + $this->changeHelper->cardChanged($cardId); $this->eventDispatcher->dispatch( '\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card]) diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 030c10e5c..a94df2e07 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -538,7 +538,7 @@ class CardService { } $label = $this->labelMapper->find($labelId); $this->cardMapper->assignLabel($cardId, $labelId); - $this->changeHelper->cardChanged($cardId, false); + $this->changeHelper->cardChanged($cardId); $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_ASSIGN, ['label' => $label]); $this->eventDispatcher->dispatch( @@ -574,7 +574,7 @@ class CardService { } $label = $this->labelMapper->find($labelId); $this->cardMapper->removeLabel($cardId, $labelId); - $this->changeHelper->cardChanged($cardId, false); + $this->changeHelper->cardChanged($cardId); $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_UNASSING, ['label' => $label]); $this->eventDispatcher->dispatch( From 6a8e607134c071d6ac6a97b9e8e2b309c6e69d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 10 Nov 2020 17:14:35 +0100 Subject: [PATCH 3/6] Add documentation for ETags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- docs/API.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/API.md b/docs/API.md index c87ba22c6..11f661145 100644 --- a/docs/API.md +++ b/docs/API.md @@ -69,6 +69,31 @@ curl -u admin:admin -X GET \ -H "If-Modified-Since: Mon, 05 Nov 2018 09:28:00 GMT" ``` +### ETag + +An ETag header is returned in order to determine if further child elements have been updated for the following endpoints: + +- Fetch all user board `GET /api/v1.0/boards` +- Fetch a single board `GET /api/v1.0/boards/{boardId}` +- Fetch all stacks of a board `GET /api/v1.0/boards/{boardId}/stacks` +- Fetch a single stacks of a board `GET /api/v1.0/boards/{boardId}/stacks/{stackId}` +- Fetch a single card of a board `GET /api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}` +- Fetch attachments of a card `GET /api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments` + +If a `If-None-Match` header is provided and the requested element has not changed a `304` Not Modified response will be returned. + +Changes of child elements will propagate to their parents and also cause an update of the ETag which will be useful for determining if a sync is necessary on any client integration side. As an example, if a label is added to a card, the ETag of all related entities (the card, stack and board) will change. + +If available the ETag will also be part of JSON response objects as shown below for a card: + +```json +{ + "id": 81, + "ETag": "bdb10fa2d2aeda092a2b6b469454dc90", + "title": "Test card" +} +``` + # Changelog ## 1.0.0 (unreleased) From a46d31caf2002b99b76090d56e21cbaf183d0c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 10 Nov 2020 17:15:05 +0100 Subject: [PATCH 4/6] Expose etag through JSON responses for child elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Controller/BoardApiController.php | 7 ++++++- lib/Db/Label.php | 4 ++++ lib/Db/RelationalEntity.php | 3 +++ tests/unit/Db/BoardTest.php | 3 +++ tests/unit/Db/CardTest.php | 3 +++ tests/unit/Db/LabelTest.php | 6 ++++-- tests/unit/Db/StackTest.php | 12 +++++++----- tests/unit/controller/BoardApiControllerTest.php | 3 ++- tests/unit/controller/CardApiControllerTest.php | 1 + tests/unit/controller/StackApiControllerTest.php | 2 ++ 10 files changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/Controller/BoardApiController.php b/lib/Controller/BoardApiController.php index d7afc6dcc..cc974f083 100644 --- a/lib/Controller/BoardApiController.php +++ b/lib/Controller/BoardApiController.php @@ -24,6 +24,7 @@ namespace OCA\Deck\Controller; +use OCA\Deck\Db\Board; use OCA\Deck\StatusException; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; @@ -72,7 +73,11 @@ class BoardApiController extends ApiController { } $boards = $this->boardService->findAll($date->getTimestamp(), $details); } - return new DataResponse($boards, HTTP::STATUS_OK); + $response = new DataResponse($boards, HTTP::STATUS_OK); + $response->setETag(md5(json_encode(array_map(function (Board $board) { + return $board->getId() . '-' . $board->getETag(); + }, $boards)))); + return $response; } /** diff --git a/lib/Db/Label.php b/lib/Db/Label.php index 830a66d9f..b6e6b414f 100644 --- a/lib/Db/Label.php +++ b/lib/Db/Label.php @@ -36,4 +36,8 @@ class Label extends RelationalEntity { $this->addType('cardId', 'integer'); $this->addType('lastModified', 'integer'); } + + public function getETag() { + return md5((string)$this->getLastModified()); + } } diff --git a/lib/Db/RelationalEntity.php b/lib/Db/RelationalEntity.php index 5da532daa..7f21a9a7e 100644 --- a/lib/Db/RelationalEntity.php +++ b/lib/Db/RelationalEntity.php @@ -80,6 +80,9 @@ class RelationalEntity extends Entity implements \JsonSerializable { $json[$property] = $value; } } + if ($reflection->hasMethod('getETag')) { + $json['ETag'] = $this->getETag(); + } return $json; } diff --git a/tests/unit/Db/BoardTest.php b/tests/unit/Db/BoardTest.php index 369e13a61..e7bef3328 100644 --- a/tests/unit/Db/BoardTest.php +++ b/tests/unit/Db/BoardTest.php @@ -32,6 +32,7 @@ class BoardTest extends TestCase { 'archived' => false, 'users' => ['user1', 'user2'], 'settings' => [], + 'ETag' => $board->getETag(), ], $board->jsonSerialize()); } @@ -52,6 +53,7 @@ class BoardTest extends TestCase { 'archived' => false, 'users' => [], 'settings' => [], + 'ETag' => $board->getETag(), ], $board->jsonSerialize()); } public function testSetAcl() { @@ -80,6 +82,7 @@ class BoardTest extends TestCase { 'shared' => 1, 'users' => [], 'settings' => [], + 'ETag' => $board->getETag(), ], $board->jsonSerialize()); } } diff --git a/tests/unit/Db/CardTest.php b/tests/unit/Db/CardTest.php index 995ff2d54..01f638a07 100644 --- a/tests/unit/Db/CardTest.php +++ b/tests/unit/Db/CardTest.php @@ -84,6 +84,7 @@ class CardTest extends TestCase { 'deletedAt' => 0, 'commentsUnread' => 0, 'lastEditor' => null, + 'ETag' => $card->getETag(), ], $card->jsonSerialize()); } public function testJsonSerializeLabels() { @@ -109,6 +110,7 @@ class CardTest extends TestCase { 'deletedAt' => 0, 'commentsUnread' => 0, 'lastEditor' => null, + 'ETag' => $card->getETag(), ], $card->jsonSerialize()); } @@ -144,6 +146,7 @@ class CardTest extends TestCase { 'deletedAt' => 0, 'commentsUnread' => 0, 'lastEditor' => null, + 'ETag' => $card->getETag(), ], $card->jsonSerialize()); } } diff --git a/tests/unit/Db/LabelTest.php b/tests/unit/Db/LabelTest.php index b129c29a3..ee8a370a1 100644 --- a/tests/unit/Db/LabelTest.php +++ b/tests/unit/Db/LabelTest.php @@ -42,7 +42,8 @@ class LabelTest extends TestCase { 'boardId' => 123, 'cardId' => null, 'lastModified' => null, - 'color' => '000000' + 'color' => '000000', + 'ETag' => $label->getETag(), ], $label->jsonSerialize()); } public function testJsonSerializeCard() { @@ -54,7 +55,8 @@ class LabelTest extends TestCase { 'boardId' => null, 'cardId' => 123, 'lastModified' => null, - 'color' => '000000' + 'color' => '000000', + 'ETag' => $label->getETag(), ], $label->jsonSerialize()); } } diff --git a/tests/unit/Db/StackTest.php b/tests/unit/Db/StackTest.php index 76016d555..3ec9688e5 100644 --- a/tests/unit/Db/StackTest.php +++ b/tests/unit/Db/StackTest.php @@ -33,7 +33,7 @@ class StackTest extends \Test\TestCase { return $board; } public function testJsonSerialize() { - $board = $this->createStack(); + $stack = $this->createStack(); $this->assertEquals([ 'id' => 1, 'title' => "My Stack", @@ -41,12 +41,13 @@ class StackTest extends \Test\TestCase { 'boardId' => 1, 'deletedAt' => 0, 'lastModified' => 0, - ], $board->jsonSerialize()); + 'ETag' => $stack->getETag(), + ], $stack->jsonSerialize()); } public function testJsonSerializeWithCards() { $cards = ["foo", "bar"]; - $board = $this->createStack(); - $board->setCards($cards); + $stack = $this->createStack(); + $stack->setCards($cards); $this->assertEquals([ 'id' => 1, 'title' => "My Stack", @@ -55,6 +56,7 @@ class StackTest extends \Test\TestCase { 'cards' => ["foo", "bar"], 'deletedAt' => 0, 'lastModified' => 0, - ], $board->jsonSerialize()); + 'ETag' => $stack->getETag(), + ], $stack->jsonSerialize()); } } diff --git a/tests/unit/controller/BoardApiControllerTest.php b/tests/unit/controller/BoardApiControllerTest.php index 1f63acf4f..3fd9198f0 100644 --- a/tests/unit/controller/BoardApiControllerTest.php +++ b/tests/unit/controller/BoardApiControllerTest.php @@ -72,7 +72,7 @@ class BoardApiControllerTest extends \Test\TestCase { $expected = new DataResponse($boards, HTTP::STATUS_OK); $actual = $this->controller->index(); - + $actual->setETag(null); $this->assertEquals($expected, $actual); } @@ -90,6 +90,7 @@ class BoardApiControllerTest extends \Test\TestCase { ->will($this->returnValue($boardId)); $expected = new DataResponse($board, HTTP::STATUS_OK); + $expected->setETag($board->getETag()); $actual = $this->controller->get(); $this->assertEquals($expected, $actual); } diff --git a/tests/unit/controller/CardApiControllerTest.php b/tests/unit/controller/CardApiControllerTest.php index e4980a834..4ef894f16 100644 --- a/tests/unit/controller/CardApiControllerTest.php +++ b/tests/unit/controller/CardApiControllerTest.php @@ -72,6 +72,7 @@ class CardApiControllerTest extends \Test\TestCase { ->willReturn($card); $expected = new DataResponse($card, HTTP::STATUS_OK); + $expected->setETag($card->getETag()); $actual = $this->controller->get(); $this->assertEquals($expected, $actual); } diff --git a/tests/unit/controller/StackApiControllerTest.php b/tests/unit/controller/StackApiControllerTest.php index 5878e560f..4d30176a9 100644 --- a/tests/unit/controller/StackApiControllerTest.php +++ b/tests/unit/controller/StackApiControllerTest.php @@ -78,6 +78,7 @@ class StackApiControllerTest extends \Test\TestCase { $expected = new DataResponse($stacks, HTTP::STATUS_OK); $actual = $this->controller->index(); + $actual->setETag(null); $this->assertEquals($expected, $actual); } @@ -97,6 +98,7 @@ class StackApiControllerTest extends \Test\TestCase { ->willReturn($this->exampleStack['id']); $expected = new DataResponse($stack, HTTP::STATUS_OK); + $expected->setETag($stack->getETag()); $actual = $this->controller->get(); $this->assertEquals($expected, $actual); } From 3fe849b93c89d747e34a3dae58753e2806e749be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 10 Nov 2020 18:00:47 +0100 Subject: [PATCH 5/6] Propagate ETag when comments are made MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Activity/CommentEventHandler.php | 9 ++++++++- tests/unit/Activity/CommentEventHandlerTest.php | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/Activity/CommentEventHandler.php b/lib/Activity/CommentEventHandler.php index 530fde2a4..dc68c3239 100644 --- a/lib/Activity/CommentEventHandler.php +++ b/lib/Activity/CommentEventHandler.php @@ -24,6 +24,7 @@ namespace OCA\Deck\Activity; use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\ChangeHelper; use OCA\Deck\Notification\NotificationHelper; use OCP\Comments\CommentsEvent; use OCP\Comments\IComment; @@ -40,10 +41,14 @@ class CommentEventHandler implements ICommentsEventHandler { /** @var CardMapper */ private $cardMapper; - public function __construct(ActivityManager $activityManager, NotificationHelper $notificationHelper, CardMapper $cardMapper) { + /** @var ChangeHelper */ + private $changeHelper; + + public function __construct(ActivityManager $activityManager, NotificationHelper $notificationHelper, CardMapper $cardMapper, ChangeHelper $changeHelper) { $this->notificationHelper = $notificationHelper; $this->activityManager = $activityManager; $this->cardMapper = $cardMapper; + $this->changeHelper = $changeHelper; } /** @@ -54,6 +59,8 @@ class CommentEventHandler implements ICommentsEventHandler { return; } + $this->changeHelper->cardChanged($event->getComment()->getObjectId()); + $eventType = $event->getEvent(); if ($eventType === CommentsEvent::EVENT_ADD ) { diff --git a/tests/unit/Activity/CommentEventHandlerTest.php b/tests/unit/Activity/CommentEventHandlerTest.php index 522bab28c..107ba10cf 100644 --- a/tests/unit/Activity/CommentEventHandlerTest.php +++ b/tests/unit/Activity/CommentEventHandlerTest.php @@ -25,6 +25,7 @@ namespace OCA\Deck\Activity; use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\ChangeHelper; use OCA\Deck\Notification\NotificationHelper; use OCP\Comments\CommentsEvent; use OCP\Comments\IComment; @@ -45,10 +46,12 @@ class CommentEventHandlerTest extends TestCase { $this->activityManager = $this->createMock(ActivityManager::class); $this->notificationHelper = $this->createMock(NotificationHelper::class); $this->cardMapper = $this->createMock(CardMapper::class); + $this->changeHelper = $this->createMock(ChangeHelper::class); $this->commentEventHandler = new CommentEventHandler( $this->activityManager, $this->notificationHelper, - $this->cardMapper + $this->cardMapper, + $this->changeHelper ); } From e16c561d68178b0a1904dc732cc1419e818a48b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 10 Nov 2020 18:02:08 +0100 Subject: [PATCH 6/6] Propagate ETag when an attachment is being marked as deleted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Service/AttachmentService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Service/AttachmentService.php b/lib/Service/AttachmentService.php index e80210329..42f913439 100644 --- a/lib/Service/AttachmentService.php +++ b/lib/Service/AttachmentService.php @@ -320,6 +320,7 @@ class AttachmentService { if ($service->allowUndo()) { $service->markAsDeleted($attachment); $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE); + $this->changeHelper->cardChanged($attachment->getCardId()); return $this->attachmentMapper->update($attachment); } $service->delete($attachment);