Merge pull request #2245 from nextcloud/enh/etag-api

This commit is contained in:
Julius Härtl
2020-11-10 21:58:30 +01:00
committed by GitHub
21 changed files with 98 additions and 18 deletions

View File

@@ -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)

View File

@@ -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
) {

View File

@@ -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;
}
/**
@@ -85,7 +90,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;
}
/**

View File

@@ -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;
}
/**

View File

@@ -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;
}
/**

View File

@@ -81,4 +81,8 @@ class Board extends RelationalEntity {
$this->acl[] = $a;
}
}
public function getETag() {
return md5((string)$this->getLastModified());
}
}

View File

@@ -155,4 +155,8 @@ class Card extends RelationalEntity {
public function getCalendarPrefix(): string {
return 'card';
}
public function getETag() {
return md5((string)$this->getLastModified());
}
}

View File

@@ -36,4 +36,8 @@ class Label extends RelationalEntity {
$this->addType('cardId', 'integer');
$this->addType('lastModified', 'integer');
}
public function getETag() {
return md5((string)$this->getLastModified());
}
}

View File

@@ -80,6 +80,9 @@ class RelationalEntity extends Entity implements \JsonSerializable {
$json[$property] = $value;
}
}
if ($reflection->hasMethod('getETag')) {
$json['ETag'] = $this->getETag();
}
return $json;
}

View File

@@ -65,4 +65,8 @@ class Stack extends RelationalEntity {
public function getCalendarPrefix(): string {
return 'stack';
}
public function getETag() {
return md5((string)$this->getLastModified());
}
}

View File

@@ -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])

View File

@@ -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);

View File

@@ -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(

View File

@@ -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
);
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}