diff --git a/docs/swagger/paths/v1_tags.json b/docs/swagger/paths/v1_tags.json index 6f5be49b..92005b12 100644 --- a/docs/swagger/paths/v1_tags.json +++ b/docs/swagger/paths/v1_tags.json @@ -31,7 +31,65 @@ }, "examples": { "application/json": { - "shortUrls": { + "tags": { + "data": [ + "games", + "php", + "shlink", + "tech" + ] + } + } + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + }, + "post": { + "tags": [ + "Tags" + ], + "summary": "Create tags", + "description": "Provided a list of tags, creates all that do not yet exist", + "parameters": [ + { + "$ref": "../parameters/Authorization.json" + }, + { + "name": "tags[]", + "in": "formData", + "description": "The list of tag names to create", + "required": true, + "type": "array" + } + ], + "responses": { + "200": { + "description": "The list of tags", + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "examples": { + "application/json": { + "tags": { "data": [ "games", "php", diff --git a/module/Core/src/Entity/Tag.php b/module/Core/src/Entity/Tag.php index ba212ebc..7537b8c0 100644 --- a/module/Core/src/Entity/Tag.php +++ b/module/Core/src/Entity/Tag.php @@ -3,13 +3,14 @@ namespace Shlinkio\Shlink\Core\Entity; use Doctrine\ORM\Mapping as ORM; use Shlinkio\Shlink\Common\Entity\AbstractEntity; +use Shlinkio\Shlink\Core\Repository\TagRepository; /** * Class Tag * @author * @link * - * @ORM\Entity() + * @ORM\Entity(repositoryClass=TagRepository::class) * @ORM\Table(name="tags") */ class Tag extends AbstractEntity implements \JsonSerializable diff --git a/module/Core/src/Repository/TagRepository.php b/module/Core/src/Repository/TagRepository.php new file mode 100644 index 00000000..a4254cad --- /dev/null +++ b/module/Core/src/Repository/TagRepository.php @@ -0,0 +1,29 @@ +getEntityManager()->createQueryBuilder(); + $qb->delete(Tag::class, 't') + ->where($qb->expr()->in('t.name', $names)); + + return $qb->getQuery()->execute(); + } +} diff --git a/module/Core/src/Repository/TagRepositoryInterface.php b/module/Core/src/Repository/TagRepositoryInterface.php new file mode 100644 index 00000000..23eb8128 --- /dev/null +++ b/module/Core/src/Repository/TagRepositoryInterface.php @@ -0,0 +1,17 @@ +em->getRepository(Tag::class)->findBy([], ['name' => 'ASC']); } + + /** + * @param array $tagNames + * @return void + */ + public function deleteTags(array $tagNames) + { + /** @var TagRepository $repo */ + $repo = $this->em->getRepository(Tag::class); + $repo->deleteByName($tagNames); + } + + /** + * Provided a list of tag names, creates all that do not exist yet + * + * @param string[] $tagNames + * @return Collection|Tag[] + */ + public function createTags(array $tagNames) + { + $tags = $this->tagNamesToEntities($this->em, $tagNames); + $this->em->flush(); + + return $tags; + } } diff --git a/module/Core/src/Service/Tag/TagServiceInterface.php b/module/Core/src/Service/Tag/TagServiceInterface.php index cf1914f4..996a914c 100644 --- a/module/Core/src/Service/Tag/TagServiceInterface.php +++ b/module/Core/src/Service/Tag/TagServiceInterface.php @@ -1,6 +1,7 @@ AnnotatedFactory::class, Action\EditShortcodeTagsAction::class => AnnotatedFactory::class, Action\Tag\ListTagsAction::class => AnnotatedFactory::class, + Action\Tag\DeleteTagsAction::class => AnnotatedFactory::class, + Action\Tag\CreateTagsAction::class => AnnotatedFactory::class, Middleware\BodyParserMiddleware::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 6212e6a8..648008c5 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -1,5 +1,6 @@ Action\AuthenticateAction::class, 'path' => '/rest/v{version:1}/authenticate', 'middleware' => Action\AuthenticateAction::class, - 'allowed_methods' => ['POST'], + 'allowed_methods' => [RequestMethod::METHOD_POST], ], // Short codes @@ -16,25 +17,25 @@ return [ 'name' => Action\CreateShortcodeAction::class, 'path' => '/rest/v{version:1}/short-codes', 'middleware' => Action\CreateShortcodeAction::class, - 'allowed_methods' => ['POST'], + 'allowed_methods' => [RequestMethod::METHOD_POST], ], [ 'name' => Action\ResolveUrlAction::class, 'path' => '/rest/v{version:1}/short-codes/{shortCode}', 'middleware' => Action\ResolveUrlAction::class, - 'allowed_methods' => ['GET'], + 'allowed_methods' => [RequestMethod::METHOD_GET], ], [ 'name' => Action\ListShortcodesAction::class, 'path' => '/rest/v{version:1}/short-codes', 'middleware' => Action\ListShortcodesAction::class, - 'allowed_methods' => ['GET'], + 'allowed_methods' => [RequestMethod::METHOD_GET], ], [ 'name' => Action\EditShortcodeTagsAction::class, 'path' => '/rest/v{version:1}/short-codes/{shortCode}/tags', 'middleware' => Action\EditShortcodeTagsAction::class, - 'allowed_methods' => ['PUT'], + 'allowed_methods' => [RequestMethod::METHOD_PUT], ], // Visits @@ -42,15 +43,27 @@ return [ 'name' => Action\GetVisitsAction::class, 'path' => '/rest/v{version:1}/short-codes/{shortCode}/visits', 'middleware' => Action\GetVisitsAction::class, - 'allowed_methods' => ['GET'], + 'allowed_methods' => [RequestMethod::METHOD_GET], ], // Tags [ - 'name' => Action\ListTagsAction::class, + 'name' => Action\Tag\ListTagsAction::class, 'path' => '/rest/v{version:1}/tags', - 'middleware' => Action\ListTagsAction::class, - 'allowed_methods' => ['GET'], + 'middleware' => Action\Tag\ListTagsAction::class, + 'allowed_methods' => [RequestMethod::METHOD_GET], + ], + [ + 'name' => Action\Tag\DeleteTagsAction::class, + 'path' => '/rest/v{version:1}/tags', + 'middleware' => Action\Tag\DeleteTagsAction::class, + 'allowed_methods' => [RequestMethod::METHOD_DELETE], + ], + [ + 'name' => Action\Tag\CreateTagsAction::class, + 'path' => '/rest/v{version:1}/tags', + 'middleware' => Action\Tag\CreateTagsAction::class, + 'allowed_methods' => [RequestMethod::METHOD_POST], ], ], diff --git a/module/Rest/src/Action/Tag/CreateTagsAction.php b/module/Rest/src/Action/Tag/CreateTagsAction.php new file mode 100644 index 00000000..b5496f75 --- /dev/null +++ b/module/Rest/src/Action/Tag/CreateTagsAction.php @@ -0,0 +1,57 @@ +tagService = $tagService; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * to the next middleware component to create the response. + * + * @param ServerRequestInterface $request + * @param DelegateInterface $delegate + * + * @return ResponseInterface + * @throws \InvalidArgumentException + */ + public function process(ServerRequestInterface $request, DelegateInterface $delegate) + { + $body = $request->getParsedBody(); + $tags = isset($body['tags']) ? $body['tags'] : []; + + return new JsonResponse([ + 'tags' => [ + 'data' => $this->tagService->createTags($tags)->toArray(), + ], + ]); + } +} diff --git a/module/Rest/src/Action/Tag/DeleteTagsAction.php b/module/Rest/src/Action/Tag/DeleteTagsAction.php new file mode 100644 index 00000000..f579e9b8 --- /dev/null +++ b/module/Rest/src/Action/Tag/DeleteTagsAction.php @@ -0,0 +1,53 @@ +tagService = $tagService; + } + + /** + * Process an incoming server request and return a response, optionally delegating + * to the next middleware component to create the response. + * + * @param ServerRequestInterface $request + * @param DelegateInterface $delegate + * + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, DelegateInterface $delegate) + { + $query = $request->getQueryParams(); + $tags = isset($query['tags']) ? $query['tags'] : []; + + $this->tagService->deleteTags($tags); + return new EmptyResponse(); + } +}