From 4c76e17178dd21abab58b30c183a99cdb2f7abd2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 12:11:31 +0200 Subject: [PATCH 01/21] Changed swagger file format from yaml to json --- composer.json | 3 +- docs/swagger.json | 416 ++++++++++++++++++++++++++++++++++++++++++++++ docs/swagger.yaml | 289 -------------------------------- 3 files changed, 418 insertions(+), 290 deletions(-) create mode 100644 docs/swagger.json delete mode 100644 docs/swagger.yaml diff --git a/composer.json b/composer.json index 58e9d40d..13947ddf 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "roave/security-advisories": "dev-master", "filp/whoops": "^2.0", "symfony/var-dumper": "^3.0", - "vlucas/phpdotenv": "^2.2" + "vlucas/phpdotenv": "^2.2", + "phly/changelog-generator": "^2.1" }, "autoload": { "psr-4": { diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 00000000..d1a5ef97 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,416 @@ +{ + "swagger": "2.0", + "info": { + "title": "Shlink", + "description": "Shlink, the self-hosted URL shortener", + "version": "1.2.0" + }, + "schemes": [ + "https" + ], + "basePath": "/rest", + "produces": [ + "application/json" + ], + + "paths": { + "/authenticate": { + "post": { + "description": "Performs an authentication", + "parameters": [ + { + "name": "apiKey", + "in": "formData", + "description": "The API key to authenticate with", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "The authentication worked.", + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "The authentication token that needs to be sent in the Authorization header" + } + } + } + }, + "400": { + "description": "An API key was not provided.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "The API key is incorrect, is disabled or has expired.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/short-codes": { + "get": { + "description": "Returns the list of short codes", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "The page to be displayed. Defaults to 1", + "required": false, + "type": "integer" + }, + { + "$ref": "#/parameters/Authorization" + } + ], + "responses": { + "200": { + "description": "The list of short URLs", + "schema": { + "type": "object", + "properties": { + "shortUrls": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/ShortUrl" + } + }, + "pagination": { + "$ref": "#/definitions/Pagination" + } + } + } + } + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "post": { + "description": "Creates a new short code", + "parameters": [ + { + "name": "longUrl", + "in": "formData", + "description": "The URL to parse", + "required": true, + "type": "string" + }, + { + "name": "tags", + "in": "formData", + "description": "The URL to parse", + "required": false, + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/parameters/Authorization" + } + ], + "responses": { + "200": { + "description": "The result of parsing the long URL", + "schema": { + "type": "object", + "properties": { + "longUrl": { + "type": "string", + "description": "The original long URL that has been parsed" + }, + "shortUrl": { + "type": "string", + "description": "The generated short URL" + }, + "shortCode": { + "type": "string", + "description": "the short code that is being used in the short URL" + } + } + } + }, + "400": { + "description": "The long URL was not provided or is invalid.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/short-codes/{shortCode}": { + "get": { + "description": "Get the long URL behind a short code.", + "parameters": [ + { + "name": "shortCode", + "in": "path", + "type": "string", + "description": "The short code to resolve.", + "required": true + }, + { + "$ref": "#/parameters/Authorization" + } + ], + "responses": { + "200": { + "description": "The long URL behind a short code.", + "schema": { + "type": "object", + "properties": { + "longUrl": { + "type": "string", + "description": "The original long URL behind the short code." + } + } + } + }, + "400": { + "description": "Provided shortCode does not match the character set currently used by the app to generate short codes.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "No URL was found for provided short code.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/short-codes/{shortCode}/visits": { + "get": { + "description": "Get the list of visits on provided short code.", + "parameters": [ + { + "name": "shortCode", + "in": "path", + "type": "string", + "description": "The shortCode from which we want to get the visits.", + "required": true + }, + { + "$ref": "#/parameters/Authorization" + } + ], + "responses": { + "200": { + "description": "List of visits.", + "schema": { + "type": "object", + "properties": { + "visits": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/Visit" + } + } + } + } + } + } + }, + "404": { + "description": "The short code does not belong to any short URL.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/short-codes/{shortCode}/tags": { + "put": { + "description": "Edit the tags on provided short code.", + "parameters": [ + { + "name": "shortCode", + "in": "path", + "type": "string", + "description": "The shortCode in which we want to edit tags.", + "required": true + }, + { + "name": "tags", + "in": "formData", + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of tags to set to the short URL.", + "required": true + }, + { + "$ref": "#/parameters/Authorization" + } + ], + "responses": { + "200": { + "description": "List of tags.", + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "400": { + "description": "The request body does not contain a \"tags\" param with array type.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "No short URL was found for provided short code.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + } + }, + + "definitions": { + "ShortUrl": { + "type": "object", + "properties": { + "shortCode": { + "type": "string", + "description": "The short code for this short URL." + }, + "originalUrl": { + "type": "string", + "description": "The original long URL." + }, + "dateCreated": { + "type": "string", + "format": "date-time", + "description": "The date in which the short URL was created in ISO format." + }, + "visitsCount": { + "type": "integer", + "description": "The number of visits that this short URL has recieved." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of tags applied to this short URL" + } + } + }, + "Visit": { + "type": "object", + "properties": { + "referer": { + "type": "string" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "remoteAddr": { + "type": "string" + }, + "userAgent": { + "type": "string" + } + } + }, + "Error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "A machine unique code" + }, + "message": { + "type": "string", + "description": "A human-friendly error message" + } + } + }, + "Pagination": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer", + "description": "The number of current page being displayed." + }, + "pagesCount": { + "type": "integer", + "description": "The total number of pages that can be displayed." + } + } + } + }, + + "parameters": { + "Authorization": { + "name": "Authorization", + "in": "header", + "description": "The authorization token with Bearer type", + "required": true, + "type": "string" + } + } +} diff --git a/docs/swagger.yaml b/docs/swagger.yaml deleted file mode 100644 index 27037fe5..00000000 --- a/docs/swagger.yaml +++ /dev/null @@ -1,289 +0,0 @@ -swagger: '2.0' -info: - title: Shlink - description: Shlink, the self-hosted URL shortener - version: "1.2.0" - -schemes: - - https - -basePath: /rest -produces: - - application/json - -paths: - /authenticate: - post: - description: Performs an authentication - parameters: - - name: apiKey - in: formData - description: The API key to authenticate with - required: true - type: string - responses: - 200: - description: The authentication worked. - schema: - type: object - properties: - token: - type: string - description: The authentication token that needs to be sent in the Authorization header - 400: - description: An API key was not provided. - schema: - $ref: '#/definitions/Error' - 401: - description: The API key is incorrect, is disabled or has expired. - schema: - $ref: '#/definitions/Error' - 500: - description: Unexpected error. - schema: - $ref: '#/definitions/Error' - /short-codes: - get: - description: Returns the list of short codes - parameters: - - name: page - in: query - description: The page to be displayed. Defaults to 1 - required: false - type: integer - - name: Authorization - in: header - description: The authorization token with Bearer type - required: true - type: string - responses: - 200: - description: The list of short URLs - schema: - type: object - properties: - shortUrls: - type: object - properties: - data: - type: array - items: - $ref: '#/definitions/ShortUrl' - pagination: - $ref: '#/definitions/Pagination' - 500: - description: Unexpected error. - schema: - $ref: '#/definitions/Error' - post: - description: Creates a new short code - parameters: - - name: longUrl - in: formData - description: The URL to parse - required: true - type: string - - name: tags - in: formData - description: The URL to parse - required: false - type: array - items: - type: string - - name: Authorization - in: header - description: The authorization token with Bearer type - required: true - type: string - responses: - 200: - description: The result of parsing the long URL - schema: - type: object - properties: - longUrl: - type: string - description: The original long URL that has been parsed - shortUrl: - type: string - description: The generated short URL - shortCode: - type: string - description: the short code that is being used in the short URL - 400: - description: The long URL was not provided or is invalid. - schema: - $ref: '#/definitions/Error' - 500: - description: Unexpected error. - schema: - $ref: '#/definitions/Error' - /short-codes/{shortCode}: - get: - description: Get the long URL behind a short code. - parameters: - - name: shortCode - in: path - type: string - description: The short code to resolve. - required: true - - name: Authorization - in: header - description: The authorization token with Bearer type - required: true - type: string - responses: - 200: - description: The long URL behind a short code. - schema: - type: object - properties: - longUrl: - type: string - description: The original long URL behind the short code. - 404: - description: No URL was found for provided short code. - schema: - $ref: '#/definitions/Error' - 400: - description: Provided shortCode does not match the character set currently used by the app to generate short codes. - schema: - $ref: '#/definitions/Error' - 500: - description: Unexpected error. - schema: - $ref: '#/definitions/Error' - /short-codes/{shortCode}/visits: - get: - description: Get the list of visits on provided short code. - parameters: - - name: shortCode - in: path - type: string - description: The shortCode from which we want to get the visits. - required: true - - name: Authorization - in: header - description: The authorization token with Bearer type - required: true - type: string - responses: - 200: - description: List of visits. - schema: - type: object - properties: - visits: - type: object - properties: - data: - type: array - items: - $ref: '#/definitions/Visit' - 404: - description: The short code does not belong to any short URL. - schema: - $ref: '#/definitions/Error' - 500: - description: Unexpected error. - schema: - $ref: '#/definitions/Error' - /short-codes/{shortCode}/tags: - put: - description: Edit the tags on provided short code. - parameters: - - name: shortCode - in: path - type: string - description: The shortCode in which we want to edit tags. - required: true - - name: tags - in: formData - type: array - items: - type: string - description: The list of tags to set to the short URL. - required: true - - name: Authorization - in: header - description: The authorization token with Bearer type - required: true - type: string - responses: - 200: - description: List of tags. - schema: - type: object - properties: - tags: - type: array - items: - type: string - 400: - description: The request body does not contain a "tags" param with array type. - schema: - $ref: '#/definitions/Error' - 404: - description: No short URL was found for provided short code. - schema: - $ref: '#/definitions/Error' - 500: - description: Unexpected error. - schema: - $ref: '#/definitions/Error' - -definitions: - ShortUrl: - type: object - properties: - shortCode: - type: string - description: The short code for this short URL. - originalUrl: - type: string - description: The original long URL. - dateCreated: - type: string - format: date-time - description: The date in which the short URL was created in ISO format. - visitsCount: - type: integer - description: The number of visits that this short URL has recieved. - tags: - type: array - items: - type: string - description: A list of tags applied to this short URL - - Visit: - type: object - properties: - referer: - type: string - date: - type: string - format: date-time - remoteAddr: - type: string - userAgent: - type: string - - Error: - type: object - properties: - code: - type: string - description: A machine unique code - message: - type: string - description: A human-friendly error message - - Pagination: - type: object - properties: - currentPage: - type: integer - description: The number of current page being displayed. - pagesCount: - type: integer - description: The total number of pages that can be displayed. From 543c0e62d07ce2c0d2687c610cbee44eb4eeb223 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 12:40:51 +0200 Subject: [PATCH 02/21] Added search term filtering to short codes list --- .../Adapter/PaginableRepositoryAdapter.php | 8 ++-- .../src/Repository/ShortUrlRepository.php | 43 +++++++++++++------ module/Core/src/Service/ShortUrlService.php | 7 +-- .../src/Service/ShortUrlServiceInterface.php | 3 +- .../Rest/src/Action/ListShortcodesAction.php | 16 ++++++- .../test/Action/ListShortcodesActionTest.php | 8 ++-- 6 files changed, 59 insertions(+), 26 deletions(-) diff --git a/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php b/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php index ade0c0c9..017f5e89 100644 --- a/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php +++ b/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php @@ -13,18 +13,18 @@ class PaginableRepositoryAdapter implements AdapterInterface */ private $paginableRepository; /** - * @var null + * @var null|string */ private $searchTerm; /** - * @var null + * @var null|array|string */ private $orderBy; - public function __construct(PaginableRepositoryInterface $paginableRepository, $searchTerm = null, $orderBy = null) + public function __construct(PaginableRepositoryInterface $paginableRepository, $searchQuery = null, $orderBy = null) { $this->paginableRepository = $paginableRepository; - $this->searchTerm = $searchTerm; + $this->searchTerm = trim(strip_tags($searchQuery)); $this->orderBy = $orderBy; } diff --git a/module/Core/src/Repository/ShortUrlRepository.php b/module/Core/src/Repository/ShortUrlRepository.php index e2d81cba..9becb216 100644 --- a/module/Core/src/Repository/ShortUrlRepository.php +++ b/module/Core/src/Repository/ShortUrlRepository.php @@ -2,6 +2,7 @@ namespace Shlinkio\Shlink\Core\Repository; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; use Shlinkio\Shlink\Core\Entity\ShortUrl; class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface @@ -15,7 +16,8 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI */ public function findList($limit = null, $offset = null, $searchTerm = null, $orderBy = null) { - $qb = $this->createQueryBuilder('s'); + $qb = $this->createListQueryBuilder($searchTerm); + $qb->select('s'); if (isset($limit)) { $qb->setMaxResults($limit); @@ -23,9 +25,6 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI if (isset($offset)) { $qb->setFirstResult($offset); } - if (isset($searchTerm)) { - // TODO - } if (isset($orderBy)) { if (is_string($orderBy)) { $qb->orderBy($orderBy); @@ -43,19 +42,39 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI /** * Counts the number of elements in a list using provided filtering data * - * @param null $searchTerm + * @param null|string $searchTerm * @return int */ public function countList($searchTerm = null) { - $qb = $this->getEntityManager()->createQueryBuilder(); - $qb->select('COUNT(s)') - ->from(ShortUrl::class, 's'); - - if (isset($searchTerm)) { - // TODO - } + $qb = $this->createListQueryBuilder($searchTerm); + $qb->select('COUNT(s)'); return (int) $qb->getQuery()->getSingleScalarResult(); } + + /** + * @param null|string $searchTerm + * @return QueryBuilder + */ + protected function createListQueryBuilder($searchTerm = null) + { + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->from(ShortUrl::class, 's'); + + // Apply search term to every searchable field if not empty + if (! empty($searchTerm)) { + $conditions = [ + $qb->expr()->like('s.originalUrl', ':searchPattern'), + $qb->expr()->like('s.shortCode', ':searchPattern'), + ]; + + // Unpack and apply search conditions + $qb->where($qb->expr()->orX(...$conditions)); + $searchTerm = '%' . $searchTerm . '%'; + $qb->setParameter('searchPattern', $searchTerm); + } + + return $qb; + } } diff --git a/module/Core/src/Service/ShortUrlService.php b/module/Core/src/Service/ShortUrlService.php index 60845b88..e3c2e451 100644 --- a/module/Core/src/Service/ShortUrlService.php +++ b/module/Core/src/Service/ShortUrlService.php @@ -32,13 +32,14 @@ class ShortUrlService implements ShortUrlServiceInterface /** * @param int $page - * @return Paginator|ShortUrl[] + * @param string $searchQuery + * @return ShortUrl[]|Paginator */ - public function listShortUrls($page = 1) + public function listShortUrls($page = 1, $searchQuery = null) { /** @var ShortUrlRepository $repo */ $repo = $this->em->getRepository(ShortUrl::class); - $paginator = new Paginator(new PaginableRepositoryAdapter($repo)); + $paginator = new Paginator(new PaginableRepositoryAdapter($repo, $searchQuery)); $paginator->setItemCountPerPage(PaginableRepositoryAdapter::ITEMS_PER_PAGE) ->setCurrentPageNumber($page); diff --git a/module/Core/src/Service/ShortUrlServiceInterface.php b/module/Core/src/Service/ShortUrlServiceInterface.php index 5ad304ee..7fbc1b4e 100644 --- a/module/Core/src/Service/ShortUrlServiceInterface.php +++ b/module/Core/src/Service/ShortUrlServiceInterface.php @@ -9,9 +9,10 @@ interface ShortUrlServiceInterface { /** * @param int $page + * @param string $searchQuery * @return ShortUrl[]|Paginator */ - public function listShortUrls($page = 1); + public function listShortUrls($page = 1, $searchQuery = null); /** * @param string $shortCode diff --git a/module/Rest/src/Action/ListShortcodesAction.php b/module/Rest/src/Action/ListShortcodesAction.php index 1cd376bc..d6f6782d 100644 --- a/module/Rest/src/Action/ListShortcodesAction.php +++ b/module/Rest/src/Action/ListShortcodesAction.php @@ -53,8 +53,8 @@ class ListShortcodesAction extends AbstractRestAction public function dispatch(Request $request, Response $response, callable $out = null) { try { - $query = $request->getQueryParams(); - $shortUrls = $this->shortUrlService->listShortUrls(isset($query['page']) ? $query['page'] : 1); + $params = $this->queryToListParams($request->getQueryParams()); + $shortUrls = $this->shortUrlService->listShortUrls(...$params); return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls)]); } catch (\Exception $e) { $this->logger->error('Unexpected error while listing short URLs.' . PHP_EOL . $e); @@ -64,4 +64,16 @@ class ListShortcodesAction extends AbstractRestAction ], 500); } } + + /** + * @param array $query + * @return string + */ + public function queryToListParams(array $query) + { + return [ + isset($query['page']) ? $query['page'] : 1, + isset($query['searchTerm']) ? $query['searchTerm'] : null, + ]; + } } diff --git a/module/Rest/test/Action/ListShortcodesActionTest.php b/module/Rest/test/Action/ListShortcodesActionTest.php index b5ec0c9d..9ff2e54a 100644 --- a/module/Rest/test/Action/ListShortcodesActionTest.php +++ b/module/Rest/test/Action/ListShortcodesActionTest.php @@ -34,8 +34,8 @@ class ListShortcodesActionTest extends TestCase public function properListReturnsSuccessResponse() { $page = 3; - $this->service->listShortUrls($page)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->service->listShortUrls($page, null)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $response = $this->action->__invoke( ServerRequestFactory::fromGlobals()->withQueryParams([ @@ -52,8 +52,8 @@ class ListShortcodesActionTest extends TestCase public function anExceptionsReturnsErrorResponse() { $page = 3; - $this->service->listShortUrls($page)->willThrow(\Exception::class) - ->shouldBeCalledTimes(1); + $this->service->listShortUrls($page, null)->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); $response = $this->action->__invoke( ServerRequestFactory::fromGlobals()->withQueryParams([ From 0a6030b35d644d5d9645757569dd26bcfd29cd53 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 12:43:22 +0200 Subject: [PATCH 03/21] Documented searchTerm query param for GET /short-codes endpoint --- docs/swagger.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/swagger.json b/docs/swagger.json index d1a5ef97..0d721cd0 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -71,6 +71,13 @@ "required": false, "type": "integer" }, + { + "name": "searchTerm", + "in": "query", + "description": "A query used to filter results by searching for it on the longUrl and shortCode fields", + "required": false, + "type": "string" + }, { "$ref": "#/parameters/Authorization" } From 8610a158d4c7c1b45434f4b128b19153f2062238 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 12:48:24 +0200 Subject: [PATCH 04/21] Added searchTerm param to shortcode:list command --- .../Shortcode/ListShortcodesCommand.php | 11 +++++++++- .../Shortcode/ListShortcodesCommandTest.php | 22 +++++++++++-------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php index a3f77903..b8a1310c 100644 --- a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php +++ b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php @@ -56,6 +56,14 @@ class ListShortcodesCommand extends Command ), 1 ) + ->addOption( + 'searchTerm', + 's', + InputOption::VALUE_OPTIONAL, + $this->translator->translate( + 'A query used to filter results by searching for it on the longUrl and shortCode fields' + ) + ) ->addOption( 'tags', 't', @@ -67,13 +75,14 @@ class ListShortcodesCommand extends Command public function execute(InputInterface $input, OutputInterface $output) { $page = intval($input->getOption('page')); + $searchTerm = $input->getOption('searchTerm'); $showTags = $input->getOption('tags'); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); do { - $result = $this->shortUrlService->listShortUrls($page); + $result = $this->shortUrlService->listShortUrls($page, $searchTerm); $page++; $table = new Table($output); diff --git a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php index a426eb06..8ab0d232 100644 --- a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php +++ b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php @@ -46,8 +46,8 @@ class ListShortcodesCommandTest extends TestCase public function noInputCallsListJustOnce() { $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls(1, null)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute(['command' => 'shortcode:list']); } @@ -66,7 +66,11 @@ class ListShortcodesCommandTest extends TestCase $questionHelper = $this->questionHelper; $that = $this; - $this->shortUrlService->listShortUrls(Argument::any())->will(function () use (&$data, $questionHelper, $that) { + $this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use ( + &$data, + $questionHelper, + $that + ) { $questionHelper->setInputStream($that->getInputStream('y')); return new Paginator(new ArrayAdapter(array_shift($data))); })->shouldBeCalledTimes(3); @@ -86,8 +90,8 @@ class ListShortcodesCommandTest extends TestCase } $this->questionHelper->setInputStream($this->getInputStream('n')); - $this->shortUrlService->listShortUrls(Argument::any())->willReturn(new Paginator(new ArrayAdapter($data))) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data))) + ->shouldBeCalledTimes(1); $this->commandTester->execute(['command' => 'shortcode:list']); } @@ -99,8 +103,8 @@ class ListShortcodesCommandTest extends TestCase { $page = 5; $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls($page)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls($page, null)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:list', @@ -114,8 +118,8 @@ class ListShortcodesCommandTest extends TestCase public function ifTagsFlagIsProvidedTagsColumnIsIncluded() { $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls(1, null)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:list', From 4580d11d3277b38b0e77b798a7f970b31a3192af Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 12:50:35 +0200 Subject: [PATCH 05/21] Noticed in swagger docs that the searchTerm param is only available from v 1.3.0 of shlink --- docs/swagger.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/swagger.json b/docs/swagger.json index 0d721cd0..d731f858 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -74,7 +74,7 @@ { "name": "searchTerm", "in": "query", - "description": "A query used to filter results by searching for it on the longUrl and shortCode fields", + "description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (From Shlink 1.3.0)", "required": false, "type": "string" }, From 8b9caf02d24640874294b61d22e69b7d6dfbed88 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 12:57:24 +0200 Subject: [PATCH 06/21] Added tags param to paginable repository adapter --- .../Adapter/PaginableRepositoryAdapter.php | 25 +++++++++++++++---- .../PaginableRepositoryInterface.php | 6 +++-- .../PaginableRepositoryAdapterTest.php | 6 ++--- .../src/Repository/ShortUrlRepository.php | 11 +++++--- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php b/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php index 017f5e89..995c7263 100644 --- a/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php +++ b/module/Common/src/Paginator/Adapter/PaginableRepositoryAdapter.php @@ -20,12 +20,21 @@ class PaginableRepositoryAdapter implements AdapterInterface * @var null|array|string */ private $orderBy; + /** + * @var array + */ + private $tags; - public function __construct(PaginableRepositoryInterface $paginableRepository, $searchQuery = null, $orderBy = null) - { + public function __construct( + PaginableRepositoryInterface $paginableRepository, + $searchTerm = null, + array $tags = [], + $orderBy = null + ) { $this->paginableRepository = $paginableRepository; - $this->searchTerm = trim(strip_tags($searchQuery)); + $this->searchTerm = trim(strip_tags($searchTerm)); $this->orderBy = $orderBy; + $this->tags = $tags; } /** @@ -37,7 +46,13 @@ class PaginableRepositoryAdapter implements AdapterInterface */ public function getItems($offset, $itemCountPerPage) { - return $this->paginableRepository->findList($itemCountPerPage, $offset, $this->searchTerm, $this->orderBy); + return $this->paginableRepository->findList( + $itemCountPerPage, + $offset, + $this->searchTerm, + $this->tags, + $this->orderBy + ); } /** @@ -51,6 +66,6 @@ class PaginableRepositoryAdapter implements AdapterInterface */ public function count() { - return $this->paginableRepository->countList($this->searchTerm); + return $this->paginableRepository->countList($this->searchTerm, $this->tags); } } diff --git a/module/Common/src/Repository/PaginableRepositoryInterface.php b/module/Common/src/Repository/PaginableRepositoryInterface.php index b9a75aea..7481d186 100644 --- a/module/Common/src/Repository/PaginableRepositoryInterface.php +++ b/module/Common/src/Repository/PaginableRepositoryInterface.php @@ -9,16 +9,18 @@ interface PaginableRepositoryInterface * @param int|null $limit * @param int|null $offset * @param string|null $searchTerm + * @param array $tags * @param string|array|null $orderBy * @return array */ - public function findList($limit = null, $offset = null, $searchTerm = null, $orderBy = null); + public function findList($limit = null, $offset = null, $searchTerm = null, array $tags = [], $orderBy = null); /** * Counts the number of elements in a list using provided filtering data * * @param null $searchTerm + * @param array $tags * @return int */ - public function countList($searchTerm = null); + public function countList($searchTerm = null, array $tags = []); } diff --git a/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php b/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php index 1135682f..196cf0c9 100644 --- a/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php +++ b/module/Common/test/Paginator/PaginableRepositoryAdapterTest.php @@ -20,7 +20,7 @@ class PaginableRepositoryAdapterTest extends TestCase public function setUp() { $this->repo = $this->prophesize(PaginableRepositoryInterface::class); - $this->adapter = new PaginableRepositoryAdapter($this->repo->reveal(), 'search', 'order'); + $this->adapter = new PaginableRepositoryAdapter($this->repo->reveal(), 'search', ['foo', 'bar'], 'order'); } /** @@ -28,7 +28,7 @@ class PaginableRepositoryAdapterTest extends TestCase */ public function getItemsFallbacksToFindList() { - $this->repo->findList(10, 5, 'search', 'order')->shouldBeCalledTimes(1); + $this->repo->findList(10, 5, 'search', ['foo', 'bar'], 'order')->shouldBeCalledTimes(1); $this->adapter->getItems(5, 10); } @@ -37,7 +37,7 @@ class PaginableRepositoryAdapterTest extends TestCase */ public function countFallbacksToCountList() { - $this->repo->countList('search')->shouldBeCalledTimes(1); + $this->repo->countList('search', ['foo', 'bar'])->shouldBeCalledTimes(1); $this->adapter->count(); } } diff --git a/module/Core/src/Repository/ShortUrlRepository.php b/module/Core/src/Repository/ShortUrlRepository.php index 9becb216..78962ccd 100644 --- a/module/Core/src/Repository/ShortUrlRepository.php +++ b/module/Core/src/Repository/ShortUrlRepository.php @@ -11,10 +11,11 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI * @param int|null $limit * @param int|null $offset * @param string|null $searchTerm + * @param array $tags * @param string|array|null $orderBy - * @return ShortUrl[] + * @return \Shlinkio\Shlink\Core\Entity\ShortUrl[] */ - public function findList($limit = null, $offset = null, $searchTerm = null, $orderBy = null) + public function findList($limit = null, $offset = null, $searchTerm = null, array $tags = [], $orderBy = null) { $qb = $this->createListQueryBuilder($searchTerm); $qb->select('s'); @@ -43,9 +44,10 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI * Counts the number of elements in a list using provided filtering data * * @param null|string $searchTerm + * @param array $tags * @return int */ - public function countList($searchTerm = null) + public function countList($searchTerm = null, array $tags = []) { $qb = $this->createListQueryBuilder($searchTerm); $qb->select('COUNT(s)'); @@ -55,9 +57,10 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI /** * @param null|string $searchTerm + * @param array $tags * @return QueryBuilder */ - protected function createListQueryBuilder($searchTerm = null) + protected function createListQueryBuilder($searchTerm = null, array $tags = []) { $qb = $this->getEntityManager()->createQueryBuilder(); $qb->from(ShortUrl::class, 's'); From 52bb14bd6654935bc40dad6f94d9c011a728fd31 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 13:04:17 +0200 Subject: [PATCH 07/21] Implemented filtering by tags in ListShortcodesAction --- module/Core/src/Repository/ShortUrlRepository.php | 13 ++++++++++--- module/Core/src/Service/ShortUrlService.php | 5 +++-- .../Core/src/Service/ShortUrlServiceInterface.php | 3 ++- module/Rest/src/Action/ListShortcodesAction.php | 1 + 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/module/Core/src/Repository/ShortUrlRepository.php b/module/Core/src/Repository/ShortUrlRepository.php index 78962ccd..a287fe0b 100644 --- a/module/Core/src/Repository/ShortUrlRepository.php +++ b/module/Core/src/Repository/ShortUrlRepository.php @@ -17,7 +17,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI */ public function findList($limit = null, $offset = null, $searchTerm = null, array $tags = [], $orderBy = null) { - $qb = $this->createListQueryBuilder($searchTerm); + $qb = $this->createListQueryBuilder($searchTerm, $tags); $qb->select('s'); if (isset($limit)) { @@ -49,7 +49,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI */ public function countList($searchTerm = null, array $tags = []) { - $qb = $this->createListQueryBuilder($searchTerm); + $qb = $this->createListQueryBuilder($searchTerm, $tags); $qb->select('COUNT(s)'); return (int) $qb->getQuery()->getSingleScalarResult(); @@ -64,6 +64,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI { $qb = $this->getEntityManager()->createQueryBuilder(); $qb->from(ShortUrl::class, 's'); + $qb->where('1=1'); // Apply search term to every searchable field if not empty if (! empty($searchTerm)) { @@ -73,11 +74,17 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI ]; // Unpack and apply search conditions - $qb->where($qb->expr()->orX(...$conditions)); + $qb->andWhere($qb->expr()->orX(...$conditions)); $searchTerm = '%' . $searchTerm . '%'; $qb->setParameter('searchPattern', $searchTerm); } + // Filter by tags if provided + if (! empty($tags)) { + $qb->join('s.tags', 't') + ->andWhere($qb->expr()->in('t.name', $tags)); + } + return $qb; } } diff --git a/module/Core/src/Service/ShortUrlService.php b/module/Core/src/Service/ShortUrlService.php index e3c2e451..faf778b1 100644 --- a/module/Core/src/Service/ShortUrlService.php +++ b/module/Core/src/Service/ShortUrlService.php @@ -33,13 +33,14 @@ class ShortUrlService implements ShortUrlServiceInterface /** * @param int $page * @param string $searchQuery + * @param array $tags * @return ShortUrl[]|Paginator */ - public function listShortUrls($page = 1, $searchQuery = null) + public function listShortUrls($page = 1, $searchQuery = null, array $tags = []) { /** @var ShortUrlRepository $repo */ $repo = $this->em->getRepository(ShortUrl::class); - $paginator = new Paginator(new PaginableRepositoryAdapter($repo, $searchQuery)); + $paginator = new Paginator(new PaginableRepositoryAdapter($repo, $searchQuery, $tags)); $paginator->setItemCountPerPage(PaginableRepositoryAdapter::ITEMS_PER_PAGE) ->setCurrentPageNumber($page); diff --git a/module/Core/src/Service/ShortUrlServiceInterface.php b/module/Core/src/Service/ShortUrlServiceInterface.php index 7fbc1b4e..cdf9a45f 100644 --- a/module/Core/src/Service/ShortUrlServiceInterface.php +++ b/module/Core/src/Service/ShortUrlServiceInterface.php @@ -10,9 +10,10 @@ interface ShortUrlServiceInterface /** * @param int $page * @param string $searchQuery + * @param array $tags * @return ShortUrl[]|Paginator */ - public function listShortUrls($page = 1, $searchQuery = null); + public function listShortUrls($page = 1, $searchQuery = null, array $tags = []); /** * @param string $shortCode diff --git a/module/Rest/src/Action/ListShortcodesAction.php b/module/Rest/src/Action/ListShortcodesAction.php index d6f6782d..dd08c071 100644 --- a/module/Rest/src/Action/ListShortcodesAction.php +++ b/module/Rest/src/Action/ListShortcodesAction.php @@ -74,6 +74,7 @@ class ListShortcodesAction extends AbstractRestAction return [ isset($query['page']) ? $query['page'] : 1, isset($query['searchTerm']) ? $query['searchTerm'] : null, + isset($query['tags']) ? $query['tags'] : [], ]; } } From 47a2c18c7ee19b4f7aff4e93371b5ff84f77f694 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 13:11:24 +0200 Subject: [PATCH 08/21] Added the ability to filter by tag in shotcodes:list command --- .../Command/Shortcode/ListShortcodesCommand.php | 12 ++++++++++-- .../Shortcode/ListShortcodesCommandTest.php | 14 +++++++------- .../Rest/test/Action/ListShortcodesActionTest.php | 8 ++++---- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php index b8a1310c..bfe2ae6f 100644 --- a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php +++ b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php @@ -67,6 +67,12 @@ class ListShortcodesCommand extends Command ->addOption( 'tags', 't', + InputOption::VALUE_OPTIONAL, + $this->translator->translate('A comma-separated list of tags to filter results') + ) + ->addOption( + 'showTags', + null, InputOption::VALUE_NONE, $this->translator->translate('Whether to display the tags or not') ); @@ -76,13 +82,15 @@ class ListShortcodesCommand extends Command { $page = intval($input->getOption('page')); $searchTerm = $input->getOption('searchTerm'); - $showTags = $input->getOption('tags'); + $tags = $input->getOption('tags'); + $tags = ! empty($tags) ? explode(',', $tags) : []; + $showTags = $input->getOption('showTags'); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); do { - $result = $this->shortUrlService->listShortUrls($page, $searchTerm); + $result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags); $page++; $table = new Table($output); diff --git a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php index 8ab0d232..306cb87b 100644 --- a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php +++ b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php @@ -46,8 +46,8 @@ class ListShortcodesCommandTest extends TestCase public function noInputCallsListJustOnce() { $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls(1, null)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls(1, null, [])->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute(['command' => 'shortcode:list']); } @@ -103,8 +103,8 @@ class ListShortcodesCommandTest extends TestCase { $page = 5; $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls($page, null)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls($page, null, [])->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:list', @@ -118,12 +118,12 @@ class ListShortcodesCommandTest extends TestCase public function ifTagsFlagIsProvidedTagsColumnIsIncluded() { $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls(1, null)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls(1, null, [])->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:list', - '--tags' => true, + '--showTags' => true, ]); $output = $this->commandTester->getDisplay(); $this->assertTrue(strpos($output, 'Tags') > 0); diff --git a/module/Rest/test/Action/ListShortcodesActionTest.php b/module/Rest/test/Action/ListShortcodesActionTest.php index 9ff2e54a..df15bb0c 100644 --- a/module/Rest/test/Action/ListShortcodesActionTest.php +++ b/module/Rest/test/Action/ListShortcodesActionTest.php @@ -34,8 +34,8 @@ class ListShortcodesActionTest extends TestCase public function properListReturnsSuccessResponse() { $page = 3; - $this->service->listShortUrls($page, null)->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->service->listShortUrls($page, null, [])->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $response = $this->action->__invoke( ServerRequestFactory::fromGlobals()->withQueryParams([ @@ -52,8 +52,8 @@ class ListShortcodesActionTest extends TestCase public function anExceptionsReturnsErrorResponse() { $page = 3; - $this->service->listShortUrls($page, null)->willThrow(\Exception::class) - ->shouldBeCalledTimes(1); + $this->service->listShortUrls($page, null, [])->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); $response = $this->action->__invoke( ServerRequestFactory::fromGlobals()->withQueryParams([ From 230f2d155b09d50d46d4aa13360297d7c3d10e81 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 13:13:50 +0200 Subject: [PATCH 09/21] Documented tags param in GET /short-codes endpoint --- docs/swagger.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/swagger.json b/docs/swagger.json index d731f858..fea8a020 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -74,10 +74,22 @@ { "name": "searchTerm", "in": "query", - "description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (From Shlink 1.3.0)", + "description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)", "required": false, "type": "string" }, + { + "name": "tags", + "in": "query", + "description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)", + "required": false, + "type": "array", + "schema": { + "items": { + "type": "string" + } + } + }, { "$ref": "#/parameters/Authorization" } From c22bbecdc5f8eb1d19fc1cd27997f2ecd566040f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 13:15:35 +0200 Subject: [PATCH 10/21] Updated languages --- module/CLI/lang/es.mo | Bin 6214 -> 6541 bytes module/CLI/lang/es.po | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 876787157c7561472ec5d4d9ad930cbf7d2d0184..06f0a3db516154e7b2427015304e07c32a01489a 100644 GIT binary patch delta 1721 zcmZ|PZ)jC@9LMpme~zwnE~nNk=YGvqT1~fmotkZS=W<#|Db5H<@p$jqcJb_2 zk+)|>6oi+0(4zt$B=#g0Nc5~g@DCUfte!|96~lzWCqfpzKW9e>q96O+*YBLW^ZWh2 z-{1G#3q9`@>(^Qreqgj6^fmOQ^UV(6odtZ*_IPIZ<8$}`9>pGf4WGfY_!7?IFqRgX zJ&SLl=DU(Ri+X<1VzWV9i$i90JIY`gH_l)yPUAwnfJ^Z*uEZaZHS8v~;4ORz7c|W+ z)Pc*n?nlk{0=|uhaV`FW&!czG+y?gHYSy>Y4DRKN&rm_1#})V$K8#n9Kl_6Zhkv3r zwuXna&<6Bz8*appuo*9+Ha>$}@i*LrT}#Z^yN%#7*0)y}D3!-?J)T4U?0Y_V#(qL& z_SH~jKg>sAICGe3a_G4{&Rl+FVuYN*&RbGVqFuK7(9q&T+aEQ!Zxno zC(p{jS>&wjGM2FAKC?r(5BK6Z+=aKX19xx^Pv8-3$9M2W{1jDNw^5nd+e-emkk7&E zY)_(U;7vaCU?ac(d4B&qDnplW1KvVK+Rk~gXDgzrc@Q7P863gCQ5!wLs#@=3WUuz+ zO7gFTFL6WF`YkGjH&Ll-WA^R14wbSas53u~A)Z8~{#VonHgX<3YW-Nk{ivfojl2N% z9TL3#i2LzIok1^y0S-zNN2rNTp^ENZR4PBjo%l7XnmxjJ8M|-`zJYvYUtspy<@aY% z3%b>FnJu6)vkM*8Lk9C0bkf(+6)p8;kI*;K`P!bQtFjfDvY=wr?}ZxkXH7w+SxBu(9Z@SccecsT)nxAdjcRU@WRkoA* z39Bli3ZqF@B}(7vMix)($1Vu-nEXoA=xo_}Nb{7Vn!@ynhDZPZ0e5UFDSz`xtrCU4 a3u=UtxUxSn2}+NmBJR5}O_y7)H~j-XPwp82 delta 1392 zcmYk+T}abW7{~Evxv4pAzGN#)oo_iy-R5de)0$4DQjoB~2m>Jq%3i3bi-Hvr6cR`) z@G2rgg1ixxf*^vRt8Tgx0xPJCq`C?!h!K5%b4G{%`G3yNwsW5U^PKHg_*F3RzSw!! zXdUz|^pwMF2$!sEw}SC{AKCe#Zi=VpRnmz*g+R0A4|2vKiF( z=TRAaiGKFCEVmh#HF9IlqNt3-u^RVc3!X%k;vV+l0&c?;mSR0oD&?K1`9rAnt|FIB zaMOBISccCqQMATvk-Fs+3`*=GKP#-arxg*Bd7?Z=6Lxug9?gXHZYJh)VG{)M@>VTXB-r4&oD3p!w8M z8y-etuyd&O$5ChICMq-2sLZ@6A^+_RmY5(3<|S;^s20aCj7s%=Q~;lm%YJZ+VII+` zmi@?qwh1IQyNw6%A%^fLYF$6|(>krlv9aC+1Eq2hyYVvW^ghR7e1{Q?P*~OW6gNG| zb=3R=)P`?Ssr`z|%pde&9qDkO#wu-;u8nKxP4onxvo?AoT?;5JY8@-251fX9%xe;r zZmM5%W?Yw1=qJX*{%iG&lwk$cLRXe``1JR}F-q7D26__JTTN+NZM6)P9i0g^9lq73 zEOB\n" "Language-Team: \n" "Language: es_ES\n" @@ -162,6 +162,16 @@ msgstr "Listar todas las URLs cortas" msgid "The first page to list (%s items per page)" msgstr "La primera página a listar (%s elementos por página)" +msgid "" +"A query used to filter results by searching for it on the longUrl and " +"shortCode fields" +msgstr "" +"Una consulta usada para filtrar el resultado buscándola en los campos " +"longUrl y shortCode" + +msgid "A comma-separated list of tags to filter results" +msgstr "Una lista de etiquetas separadas por coma para filtrar el resultado" + msgid "Whether to display the tags or not" msgstr "Si se desea mostrar las etiquetas o no" From 42f86a4a24ad9c683a5c4c388921821c5cb64e9e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 18:46:53 +0200 Subject: [PATCH 11/21] Added versioning to API endpoints, allowing not to pass the version which will default to v1 --- docs/swagger.json | 10 ++-- module/Rest/config/dependencies.config.php | 1 + .../config/middleware-pipeline.config.php | 7 +++ module/Rest/config/routes.config.php | 12 ++--- .../src/Middleware/PathVersionMiddleware.php | 54 +++++++++++++++++++ 5 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 module/Rest/src/Middleware/PathVersionMiddleware.php diff --git a/docs/swagger.json b/docs/swagger.json index fea8a020..262b0cfc 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -14,7 +14,7 @@ ], "paths": { - "/authenticate": { + "/v1/authenticate": { "post": { "description": "Performs an authentication", "parameters": [ @@ -60,7 +60,7 @@ } } }, - "/short-codes": { + "/v1/short-codes": { "get": { "description": "Returns the list of short codes", "parameters": [ @@ -185,7 +185,7 @@ } } }, - "/short-codes/{shortCode}": { + "/v1/short-codes/{shortCode}": { "get": { "description": "Get the long URL behind a short code.", "parameters": [ @@ -234,7 +234,7 @@ } } }, - "/short-codes/{shortCode}/visits": { + "/v1/short-codes/{shortCode}/visits": { "get": { "description": "Get the list of visits on provided short code.", "parameters": [ @@ -284,7 +284,7 @@ } } }, - "/short-codes/{shortCode}/tags": { + "/v1/short-codes/{shortCode}/tags": { "put": { "description": "Edit the tags on provided short code.", "parameters": [ diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index a6b51cb7..3fae9af7 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -22,6 +22,7 @@ return [ Middleware\BodyParserMiddleware::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, + Middleware\PathVersionMiddleware::class => InvokableFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, ], ], diff --git a/module/Rest/config/middleware-pipeline.config.php b/module/Rest/config/middleware-pipeline.config.php index ce22dff1..8adea240 100644 --- a/module/Rest/config/middleware-pipeline.config.php +++ b/module/Rest/config/middleware-pipeline.config.php @@ -4,6 +4,13 @@ use Shlinkio\Shlink\Rest\Middleware; return [ 'middleware_pipeline' => [ + 'pre-routing' => [ + 'middleware' => [ + Middleware\PathVersionMiddleware::class, + ], + 'priority' => 11, + ], + 'rest' => [ 'path' => '/rest', 'middleware' => [ diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index d27b9c6d..8d107d8b 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -6,37 +6,37 @@ return [ 'routes' => [ [ 'name' => 'rest-authenticate', - 'path' => '/rest/authenticate', + 'path' => '/rest/v{version:1}/authenticate', 'middleware' => Action\AuthenticateAction::class, 'allowed_methods' => ['POST', 'OPTIONS'], ], [ 'name' => 'rest-create-shortcode', - 'path' => '/rest/short-codes', + 'path' => '/rest/v{version:1}/short-codes', 'middleware' => Action\CreateShortcodeAction::class, 'allowed_methods' => ['POST', 'OPTIONS'], ], [ 'name' => 'rest-resolve-url', - 'path' => '/rest/short-codes/{shortCode}', + 'path' => '/rest/v{version:1}/short-codes/{shortCode}', 'middleware' => Action\ResolveUrlAction::class, 'allowed_methods' => ['GET', 'OPTIONS'], ], [ 'name' => 'rest-list-shortened-url', - 'path' => '/rest/short-codes', + 'path' => '/rest/v{version:1}/short-codes', 'middleware' => Action\ListShortcodesAction::class, 'allowed_methods' => ['GET'], ], [ 'name' => 'rest-get-visits', - 'path' => '/rest/short-codes/{shortCode}/visits', + 'path' => '/rest/v{version:1}/short-codes/{shortCode}/visits', 'middleware' => Action\GetVisitsAction::class, 'allowed_methods' => ['GET', 'OPTIONS'], ], [ 'name' => 'rest-edit-tags', - 'path' => '/rest/short-codes/{shortCode}/tags', + 'path' => '/rest/v{version:1}/short-codes/{shortCode}/tags', 'middleware' => Action\EditTagsAction::class, 'allowed_methods' => ['PUT', 'OPTIONS'], ], diff --git a/module/Rest/src/Middleware/PathVersionMiddleware.php b/module/Rest/src/Middleware/PathVersionMiddleware.php new file mode 100644 index 00000000..b3eae299 --- /dev/null +++ b/module/Rest/src/Middleware/PathVersionMiddleware.php @@ -0,0 +1,54 @@ +getUri(); + $path = $uri->getPath(); + + // If the path does not begin with the version number, prepend v1 by default for retrocompatibility purposes + if (strpos($path, '/rest/v') !== 0) { + $parts = explode('/', $path); + // Remove the first empty part and the "/rest" prefix + array_shift($parts); + array_shift($parts); + // Prepend the prefix with version + array_unshift($parts, '/rest/v1'); + + $request = $request->withUri($uri->withPath(implode('/', $parts))); + } + + return $out($request, $response); + } +} From 31594d47b36fd361d3aed9c2dd7c9f1d9de1fbb2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 18:52:40 +0200 Subject: [PATCH 12/21] Created PathVersionMiddlewareTest --- .../Middleware/PathVersionMiddlewareTest.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 module/Rest/test/Middleware/PathVersionMiddlewareTest.php diff --git a/module/Rest/test/Middleware/PathVersionMiddlewareTest.php b/module/Rest/test/Middleware/PathVersionMiddlewareTest.php new file mode 100644 index 00000000..098c4b01 --- /dev/null +++ b/module/Rest/test/Middleware/PathVersionMiddlewareTest.php @@ -0,0 +1,47 @@ +middleware = new PathVersionMiddleware(); + } + + /** + * @test + */ + public function whenVersionIsProvidedRequestRemainsUnchanged() + { + $request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/rest/v2/foo')); + $test = $this; + $this->middleware->__invoke($request, new Response(), function ($req) use ($request, $test) { + $test->assertSame($request, $req); + }); + } + + /** + * @test + */ + public function versionOneIsPrependedWhenNoVersionIsDefined() + { + $request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/rest/bar/baz')); + $test = $this; + $this->middleware->__invoke($request, new Response(), function (Request $req) use ($request, $test) { + $test->assertNotSame($request, $req); + $this->assertEquals('/rest/v1/bar/baz', $req->getUri()->getPath()); + }); + } +} From e3cbac38cee6313a1a4aa84e8888e63d49ba03d6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 22:11:36 +0200 Subject: [PATCH 13/21] Improved output on api-key:list command --- .../CLI/src/Command/Api/ListKeysCommand.php | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/module/CLI/src/Command/Api/ListKeysCommand.php b/module/CLI/src/Command/Api/ListKeysCommand.php index e6f70ec0..5e93adeb 100644 --- a/module/CLI/src/Command/Api/ListKeysCommand.php +++ b/module/CLI/src/Command/Api/ListKeysCommand.php @@ -73,12 +73,15 @@ class ListKeysCommand extends Command $key = $row->getKey(); $expiration = $row->getExpirationDate(); $rowData = []; + $formatMethod = ! $row->isEnabled() + ? 'getErrorString' + : ($row->isExpired() ? 'getWarningString' : 'getSuccessString'); if ($enabledOnly) { - $rowData[] = $key; + $rowData[] = $this->{$formatMethod}($key); } else { - $rowData[] = $row->isEnabled() ? $this->getSuccessString($key) : $this->getErrorString($key); - $rowData[] = $row->isEnabled() ? $this->getSuccessString('+++') : $this->getErrorString('---'); + $rowData[] = $this->{$formatMethod}($key); + $rowData[] = $this->{$formatMethod}($this->getEnabledSymbol($row)); } $rowData[] = isset($expiration) ? $expiration->format(\DateTime::ISO8601) : '-'; @@ -105,4 +108,22 @@ class ListKeysCommand extends Command { return sprintf('%s', $string); } + + /** + * @param $string + * @return string + */ + protected function getWarningString($string) + { + return sprintf('%s', $string); + } + + /** + * @param ApiKey $apiKey + * @return string + */ + protected function getEnabledSymbol(ApiKey $apiKey) + { + return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++'; + } } From 18ae541c93f9bd253ec900dd867e76a5d7397bf9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 22:17:04 +0200 Subject: [PATCH 14/21] Improved body parsing on BodyParserMiddleware --- .../src/Middleware/BodyParserMiddleware.php | 59 +++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/module/Rest/src/Middleware/BodyParserMiddleware.php b/module/Rest/src/Middleware/BodyParserMiddleware.php index 598f53ca..7344ccc4 100644 --- a/module/Rest/src/Middleware/BodyParserMiddleware.php +++ b/module/Rest/src/Middleware/BodyParserMiddleware.php @@ -3,6 +3,7 @@ namespace Shlinkio\Shlink\Rest\Middleware; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use Shlinkio\Shlink\Common\Exception\RuntimeException; use Zend\Stratigility\MiddlewareInterface; class BodyParserMiddleware implements MiddlewareInterface @@ -35,18 +36,66 @@ class BodyParserMiddleware implements MiddlewareInterface public function __invoke(Request $request, Response $response, callable $out = null) { $method = $request->getMethod(); - if (! in_array($method, ['PUT', 'PATCH'])) { + $currentParams = $request->getParsedBody(); + + // In requests that do not allow body or if the body has already been parsed, continue to next middleware + if (in_array($method, ['GET', 'HEAD', 'OPTIONS']) || ! empty($currentParams)) { return $out($request, $response); } - $contentType = $request->getHeaderLine('Content-type'); - $rawBody = (string) $request->getBody(); + // If the accepted content is JSON, try to parse the body from JSON + $contentType = $this->getRequestContentType($request); if (in_array($contentType, ['application/json', 'text/json', 'application/x-json'])) { - return $out($request->withParsedBody(json_decode($rawBody, true)), $response); + return $out($this->parseFromJson($request), $response); + } + + return $out($this->parseFromUrlEncoded($request), $response); + } + + /** + * @param Request $request + * @return string + */ + protected function getRequestContentType(Request $request) + { + $contentType = $request->getHeaderLine('Content-type'); + $contentTypes = explode(';', $contentType); + return trim(array_shift($contentTypes)); + } + + /** + * @param Request $request + * @return Request + */ + protected function parseFromJson(Request $request) + { + $rawBody = (string) $request->getBody(); + if (empty($rawBody)) { + return $request; + } + + $parsedJson = json_decode($rawBody, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException(sprintf('Error when parsing JSON request body: %s', json_last_error_msg())); + } + + return $request->withParsedBody($parsedJson); + } + + /** + * @param Request $request + * @return Request + */ + protected function parseFromUrlEncoded(Request $request) + { + $rawBody = (string) $request->getBody(); + if (empty($rawBody)) { + return $request; } $parsedBody = []; parse_str($rawBody, $parsedBody); - return $out($request->withParsedBody($parsedBody), $response); + + return $request->withParsedBody($parsedBody); } } From 85146e56763d216946ffc8475fc19de37592f63b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 23:02:12 +0200 Subject: [PATCH 15/21] Added support to order short URL lists --- .../src/Repository/ShortUrlRepository.php | 37 +++++++++++++++---- module/Core/src/Service/ShortUrlService.php | 5 ++- .../src/Service/ShortUrlServiceInterface.php | 3 +- .../Rest/src/Action/ListShortcodesAction.php | 1 + 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/module/Core/src/Repository/ShortUrlRepository.php b/module/Core/src/Repository/ShortUrlRepository.php index a287fe0b..0d7677ea 100644 --- a/module/Core/src/Repository/ShortUrlRepository.php +++ b/module/Core/src/Repository/ShortUrlRepository.php @@ -20,21 +20,42 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI $qb = $this->createListQueryBuilder($searchTerm, $tags); $qb->select('s'); + // Set limit and offset if (isset($limit)) { $qb->setMaxResults($limit); } if (isset($offset)) { $qb->setFirstResult($offset); } + + // In case the ordering has been specified, the query could be more complex. Process it if (isset($orderBy)) { - if (is_string($orderBy)) { - $qb->orderBy($orderBy); - } elseif (is_array($orderBy)) { - $key = key($orderBy); - $qb->orderBy($key, $orderBy[$key]); - } - } else { - $qb->orderBy('s.dateCreated'); + return $this->processOrderByForList($qb, $orderBy); + } + + // With no order by, order by date and just return the list of ShortUrls + $qb->orderBy('s.dateCreated'); + return $qb->getQuery()->getResult(); + } + + protected function processOrderByForList(QueryBuilder $qb, $orderBy) + { + $fieldName = is_array($orderBy) ? key($orderBy) : $orderBy; + $order = is_array($orderBy) ? $orderBy[$fieldName] : 'ASC'; + + if ($fieldName === 'visits') { + $qb->addSelect('COUNT(v) AS totalVisits') + ->leftJoin('s.visits', 'v') + ->groupBy('s') + ->orderBy('totalVisits', $order); + + return array_column($qb->getQuery()->getResult(), 0); + } elseif (in_array($fieldName, [ + 'originalUrl', + 'shortCode', + 'dateCreated', + ])) { + $qb->orderBy('s.' . $fieldName, $order); } return $qb->getQuery()->getResult(); diff --git a/module/Core/src/Service/ShortUrlService.php b/module/Core/src/Service/ShortUrlService.php index faf778b1..59c76565 100644 --- a/module/Core/src/Service/ShortUrlService.php +++ b/module/Core/src/Service/ShortUrlService.php @@ -34,13 +34,14 @@ class ShortUrlService implements ShortUrlServiceInterface * @param int $page * @param string $searchQuery * @param array $tags + * @param null $orderBy * @return ShortUrl[]|Paginator */ - public function listShortUrls($page = 1, $searchQuery = null, array $tags = []) + public function listShortUrls($page = 1, $searchQuery = null, array $tags = [], $orderBy = null) { /** @var ShortUrlRepository $repo */ $repo = $this->em->getRepository(ShortUrl::class); - $paginator = new Paginator(new PaginableRepositoryAdapter($repo, $searchQuery, $tags)); + $paginator = new Paginator(new PaginableRepositoryAdapter($repo, $searchQuery, $tags, $orderBy)); $paginator->setItemCountPerPage(PaginableRepositoryAdapter::ITEMS_PER_PAGE) ->setCurrentPageNumber($page); diff --git a/module/Core/src/Service/ShortUrlServiceInterface.php b/module/Core/src/Service/ShortUrlServiceInterface.php index cdf9a45f..bc9b8daf 100644 --- a/module/Core/src/Service/ShortUrlServiceInterface.php +++ b/module/Core/src/Service/ShortUrlServiceInterface.php @@ -11,9 +11,10 @@ interface ShortUrlServiceInterface * @param int $page * @param string $searchQuery * @param array $tags + * @param null $orderBy * @return ShortUrl[]|Paginator */ - public function listShortUrls($page = 1, $searchQuery = null, array $tags = []); + public function listShortUrls($page = 1, $searchQuery = null, array $tags = [], $orderBy = null); /** * @param string $shortCode diff --git a/module/Rest/src/Action/ListShortcodesAction.php b/module/Rest/src/Action/ListShortcodesAction.php index dd08c071..aa987c2f 100644 --- a/module/Rest/src/Action/ListShortcodesAction.php +++ b/module/Rest/src/Action/ListShortcodesAction.php @@ -75,6 +75,7 @@ class ListShortcodesAction extends AbstractRestAction isset($query['page']) ? $query['page'] : 1, isset($query['searchTerm']) ? $query['searchTerm'] : null, isset($query['tags']) ? $query['tags'] : [], + isset($query['orderBy']) ? $query['orderBy'] : null, ]; } } From 9ac48bfbc5dfb883d4bfa8842c38e0ac1f9bbbdf Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 23:10:30 +0200 Subject: [PATCH 16/21] Added support for ordering in shortcode:list command --- .../Shortcode/ListShortcodesCommand.php | 22 ++++++++++++++++++- .../Shortcode/ListShortcodesCommandTest.php | 12 +++++----- .../src/Repository/ShortUrlRepository.php | 6 ++++- .../test/Action/ListShortcodesActionTest.php | 8 +++---- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php index bfe2ae6f..a8594db7 100644 --- a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php +++ b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php @@ -70,6 +70,14 @@ class ListShortcodesCommand extends Command InputOption::VALUE_OPTIONAL, $this->translator->translate('A comma-separated list of tags to filter results') ) + ->addOption( + 'orderBy', + 'o', + InputOption::VALUE_OPTIONAL, + $this->translator->translate( + 'The field from which we want to order by. Pass ASC or DESC separated by a comma' + ) + ) ->addOption( 'showTags', null, @@ -85,12 +93,13 @@ class ListShortcodesCommand extends Command $tags = $input->getOption('tags'); $tags = ! empty($tags) ? explode(',', $tags) : []; $showTags = $input->getOption('showTags'); + $orderBy = $input->getOption('orderBy'); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); do { - $result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags); + $result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input)); $page++; $table = new Table($output); @@ -136,4 +145,15 @@ class ListShortcodesCommand extends Command } } while ($continue); } + + protected function processOrderBy(InputInterface $input) + { + $orderBy = $input->getOption('orderBy'); + if (empty($orderBy)) { + return null; + } + + $orderBy = explode(',', $orderBy); + return count($orderBy) === 1 ? $orderBy[0] : [$orderBy[0] => $orderBy[1]]; + } } diff --git a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php index 306cb87b..03ac0e0e 100644 --- a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php +++ b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php @@ -46,8 +46,8 @@ class ListShortcodesCommandTest extends TestCase public function noInputCallsListJustOnce() { $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls(1, null, [])->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute(['command' => 'shortcode:list']); } @@ -103,8 +103,8 @@ class ListShortcodesCommandTest extends TestCase { $page = 5; $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls($page, null, [])->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:list', @@ -118,8 +118,8 @@ class ListShortcodesCommandTest extends TestCase public function ifTagsFlagIsProvidedTagsColumnIsIncluded() { $this->questionHelper->setInputStream($this->getInputStream('\n')); - $this->shortUrlService->listShortUrls(1, null, [])->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:list', diff --git a/module/Core/src/Repository/ShortUrlRepository.php b/module/Core/src/Repository/ShortUrlRepository.php index 0d7677ea..256cb985 100644 --- a/module/Core/src/Repository/ShortUrlRepository.php +++ b/module/Core/src/Repository/ShortUrlRepository.php @@ -43,7 +43,11 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI $fieldName = is_array($orderBy) ? key($orderBy) : $orderBy; $order = is_array($orderBy) ? $orderBy[$fieldName] : 'ASC'; - if ($fieldName === 'visits') { + if (in_array($fieldName, [ + 'visits', + 'visitsCount', + 'visitCount', + ])) { $qb->addSelect('COUNT(v) AS totalVisits') ->leftJoin('s.visits', 'v') ->groupBy('s') diff --git a/module/Rest/test/Action/ListShortcodesActionTest.php b/module/Rest/test/Action/ListShortcodesActionTest.php index df15bb0c..d17740be 100644 --- a/module/Rest/test/Action/ListShortcodesActionTest.php +++ b/module/Rest/test/Action/ListShortcodesActionTest.php @@ -34,8 +34,8 @@ class ListShortcodesActionTest extends TestCase public function properListReturnsSuccessResponse() { $page = 3; - $this->service->listShortUrls($page, null, [])->willReturn(new Paginator(new ArrayAdapter())) - ->shouldBeCalledTimes(1); + $this->service->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter())) + ->shouldBeCalledTimes(1); $response = $this->action->__invoke( ServerRequestFactory::fromGlobals()->withQueryParams([ @@ -52,8 +52,8 @@ class ListShortcodesActionTest extends TestCase public function anExceptionsReturnsErrorResponse() { $page = 3; - $this->service->listShortUrls($page, null, [])->willThrow(\Exception::class) - ->shouldBeCalledTimes(1); + $this->service->listShortUrls($page, null, [], null)->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); $response = $this->action->__invoke( ServerRequestFactory::fromGlobals()->withQueryParams([ From 0b9753582d5fc7f660922089f393dfaa5f52eb7a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 23:13:54 +0200 Subject: [PATCH 17/21] Documented how to order results --- docs/swagger.json | 13 +++++++++++++ module/CLI/lang/es.mo | Bin 6541 -> 6739 bytes module/CLI/lang/es.po | 10 ++++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/swagger.json b/docs/swagger.json index 262b0cfc..b3b7a5c5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -90,6 +90,19 @@ } } }, + { + "name": "orderBy", + "in": "query", + "description": "The field from which you want to order the result. (Since v1.3.0)", + "enum": [ + "originalUrl", + "shortCode", + "dateCreated", + "visits" + ], + "required": false, + "type": "string" + }, { "$ref": "#/parameters/Authorization" } diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index 06f0a3db516154e7b2427015304e07c32a01489a..3365694abb1e2e8bd51fda869dbb18fc3c7e86c0 100644 GIT binary patch delta 1600 zcmYk+OKeP09LMp0tEKv4s%7eV+Il}qr=Ioh%Oe^ZB1nX*bETs5Fl`kfvmv^X2q-EC?GR5|OZwU?uUkknsK8TjFHS{hXf8z5jFn|8wSC{jwMPjM$kMw+!^CpP1C)bqX$8yOYeUyJLRZ^b6F zto1V}W#S2z;VV?^k64UfupF(xj96BIV{j5q!g;7b8!&=#)boyFKc2y?D>F-%c}7Dlg(}e1?4N7Z>ie0-`A+ z*oY0NU4067;bm;YZ@3hjD#MzeK)uif)ckc+;CHA@W`8nJii#+_!x-kEnpUhYD>G`# zLV^qo4}|D5v&@-u7?HWw3^>k}MydtilyK|7tq-`cYAfh1e=o>eX}{ z)pUOUHth$cR%dw$eGz>=U2S@ZEkH^iw0f>8;S9QdxXi6OlpJcy-#coP-xYTUGpU5@ z>paxa>H2)v=OuewPs*h-aX;e@9ItiTyzXwdetQ#BuDJ!f{j`_ydi*&5cAo1un8a*+iTSvUrT76^!`877H*hy*ZAmXwg#o_DQ1hL^TR4oB_!Up1Z)33&_XbbKx2LjEcw_R^tzB#u9d+)Fd#0!*~Gaa67J`LjJ|O z{|7Z+9lK*_5p*L@KVLfi3PFg{}*t11Y z)!dEya21E}FKVNMtg7`MB73!GrNmzgFL6WF`UVxkbyTRzn7tiqP$3&Z<$N3`a1s^z zZ>SA4kPjZU7{+k`mD*|K1+cfs;q4<1;F`lA%%ForY2vG>iKb9RcLx>9`*<8*psLx& zG0tNx9>MF#Pxchu*Ur12LM<2yrX$;oicBwt(3xP6!JwL6L)U4kFWXCRr1P^KqpPxY zGDShfsQ(K!=69P8p++FwraEZy#``^mURiC_YmS@87~hoF7P_LK6X;dc&ULydT~Sa| z-qaKwo#ZfGDN|Fi{%;`$ncf$IY)9!Ta!STJ=xS{$OBEXb4emRso2rLH8qkyq)d7X? z=1-wlYo^!IBXnxZTIp^}Uh!SdO;#5KGJN4kd$?sL>aTG}t;tdUY+kY_Sd{S(%RzNh diff --git a/module/CLI/lang/es.po b/module/CLI/lang/es.po index f2c806c7..dab5bdb7 100644 --- a/module/CLI/lang/es.po +++ b/module/CLI/lang/es.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shlink 1.0\n" -"POT-Creation-Date: 2016-10-22 13:14+0200\n" -"PO-Revision-Date: 2016-10-22 13:15+0200\n" +"POT-Creation-Date: 2016-10-22 23:12+0200\n" +"PO-Revision-Date: 2016-10-22 23:13+0200\n" "Last-Translator: Alejandro Celaya \n" "Language-Team: \n" "Language: es_ES\n" @@ -172,6 +172,12 @@ msgstr "" msgid "A comma-separated list of tags to filter results" msgstr "Una lista de etiquetas separadas por coma para filtrar el resultado" +msgid "" +"The field from which we want to order by. Pass ASC or DESC separated by a " +"comma" +msgstr "" +"El campo por el cual queremos ordernar. Pasa ASC o DESC separado por una coma" + msgid "Whether to display the tags or not" msgstr "Si se desea mostrar las etiquetas o no" From 8a12ed6b8c61af22e3b371453185a8d32dc287f4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 22 Oct 2016 23:44:14 +0200 Subject: [PATCH 18/21] Separated swagger specification into multiple files --- .gitignore | 1 + docs/swagger.json | 448 ------------------ docs/swagger/definitions/Error.json | 13 + docs/swagger/definitions/Pagination.json | 13 + docs/swagger/definitions/ShortUrl.json | 29 ++ docs/swagger/definitions/Visit.json | 18 + docs/swagger/endpoints/v1_authenticate.json | 46 ++ docs/swagger/endpoints/v1_short-codes.json | 138 ++++++ .../endpoints/v1_short-codes_{shortCode}.json | 49 ++ .../v1_short-codes_{shortCode}_tags.json | 61 +++ .../v1_short-codes_{shortCode}_visits.json | 50 ++ docs/swagger/parameters/Authorization.json | 7 + docs/swagger/swagger.json | 33 ++ 13 files changed, 458 insertions(+), 448 deletions(-) delete mode 100644 docs/swagger.json create mode 100644 docs/swagger/definitions/Error.json create mode 100644 docs/swagger/definitions/Pagination.json create mode 100644 docs/swagger/definitions/ShortUrl.json create mode 100644 docs/swagger/definitions/Visit.json create mode 100644 docs/swagger/endpoints/v1_authenticate.json create mode 100644 docs/swagger/endpoints/v1_short-codes.json create mode 100644 docs/swagger/endpoints/v1_short-codes_{shortCode}.json create mode 100644 docs/swagger/endpoints/v1_short-codes_{shortCode}_tags.json create mode 100644 docs/swagger/endpoints/v1_short-codes_{shortCode}_visits.json create mode 100644 docs/swagger/parameters/Authorization.json create mode 100644 docs/swagger/swagger.json diff --git a/.gitignore b/.gitignore index aebab397..2a7fb730 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ composer.lock vendor/ .env data/database.sqlite +docs/swagger-ui diff --git a/docs/swagger.json b/docs/swagger.json deleted file mode 100644 index b3b7a5c5..00000000 --- a/docs/swagger.json +++ /dev/null @@ -1,448 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "title": "Shlink", - "description": "Shlink, the self-hosted URL shortener", - "version": "1.2.0" - }, - "schemes": [ - "https" - ], - "basePath": "/rest", - "produces": [ - "application/json" - ], - - "paths": { - "/v1/authenticate": { - "post": { - "description": "Performs an authentication", - "parameters": [ - { - "name": "apiKey", - "in": "formData", - "description": "The API key to authenticate with", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "The authentication worked.", - "schema": { - "type": "object", - "properties": { - "token": { - "type": "string", - "description": "The authentication token that needs to be sent in the Authorization header" - } - } - } - }, - "400": { - "description": "An API key was not provided.", - "schema": { - "$ref": "#/definitions/Error" - } - }, - "401": { - "description": "The API key is incorrect, is disabled or has expired.", - "schema": { - "$ref": "#/definitions/Error" - } - }, - "500": { - "description": "Unexpected error.", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - } - }, - "/v1/short-codes": { - "get": { - "description": "Returns the list of short codes", - "parameters": [ - { - "name": "page", - "in": "query", - "description": "The page to be displayed. Defaults to 1", - "required": false, - "type": "integer" - }, - { - "name": "searchTerm", - "in": "query", - "description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)", - "required": false, - "type": "string" - }, - { - "name": "tags", - "in": "query", - "description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)", - "required": false, - "type": "array", - "schema": { - "items": { - "type": "string" - } - } - }, - { - "name": "orderBy", - "in": "query", - "description": "The field from which you want to order the result. (Since v1.3.0)", - "enum": [ - "originalUrl", - "shortCode", - "dateCreated", - "visits" - ], - "required": false, - "type": "string" - }, - { - "$ref": "#/parameters/Authorization" - } - ], - "responses": { - "200": { - "description": "The list of short URLs", - "schema": { - "type": "object", - "properties": { - "shortUrls": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/ShortUrl" - } - }, - "pagination": { - "$ref": "#/definitions/Pagination" - } - } - } - } - } - }, - "500": { - "description": "Unexpected error.", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - }, - "post": { - "description": "Creates a new short code", - "parameters": [ - { - "name": "longUrl", - "in": "formData", - "description": "The URL to parse", - "required": true, - "type": "string" - }, - { - "name": "tags", - "in": "formData", - "description": "The URL to parse", - "required": false, - "type": "array", - "items": { - "type": "string" - } - }, - { - "$ref": "#/parameters/Authorization" - } - ], - "responses": { - "200": { - "description": "The result of parsing the long URL", - "schema": { - "type": "object", - "properties": { - "longUrl": { - "type": "string", - "description": "The original long URL that has been parsed" - }, - "shortUrl": { - "type": "string", - "description": "The generated short URL" - }, - "shortCode": { - "type": "string", - "description": "the short code that is being used in the short URL" - } - } - } - }, - "400": { - "description": "The long URL was not provided or is invalid.", - "schema": { - "$ref": "#/definitions/Error" - } - }, - "500": { - "description": "Unexpected error.", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - } - }, - "/v1/short-codes/{shortCode}": { - "get": { - "description": "Get the long URL behind a short code.", - "parameters": [ - { - "name": "shortCode", - "in": "path", - "type": "string", - "description": "The short code to resolve.", - "required": true - }, - { - "$ref": "#/parameters/Authorization" - } - ], - "responses": { - "200": { - "description": "The long URL behind a short code.", - "schema": { - "type": "object", - "properties": { - "longUrl": { - "type": "string", - "description": "The original long URL behind the short code." - } - } - } - }, - "400": { - "description": "Provided shortCode does not match the character set currently used by the app to generate short codes.", - "schema": { - "$ref": "#/definitions/Error" - } - }, - "404": { - "description": "No URL was found for provided short code.", - "schema": { - "$ref": "#/definitions/Error" - } - }, - "500": { - "description": "Unexpected error.", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - } - }, - "/v1/short-codes/{shortCode}/visits": { - "get": { - "description": "Get the list of visits on provided short code.", - "parameters": [ - { - "name": "shortCode", - "in": "path", - "type": "string", - "description": "The shortCode from which we want to get the visits.", - "required": true - }, - { - "$ref": "#/parameters/Authorization" - } - ], - "responses": { - "200": { - "description": "List of visits.", - "schema": { - "type": "object", - "properties": { - "visits": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/Visit" - } - } - } - } - } - } - }, - "404": { - "description": "The short code does not belong to any short URL.", - "schema": { - "$ref": "#/definitions/Error" - } - }, - "500": { - "description": "Unexpected error.", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - } - }, - "/v1/short-codes/{shortCode}/tags": { - "put": { - "description": "Edit the tags on provided short code.", - "parameters": [ - { - "name": "shortCode", - "in": "path", - "type": "string", - "description": "The shortCode in which we want to edit tags.", - "required": true - }, - { - "name": "tags", - "in": "formData", - "type": "array", - "items": { - "type": "string" - }, - "description": "The list of tags to set to the short URL.", - "required": true - }, - { - "$ref": "#/parameters/Authorization" - } - ], - "responses": { - "200": { - "description": "List of tags.", - "schema": { - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "400": { - "description": "The request body does not contain a \"tags\" param with array type.", - "schema": { - "$ref": "#/definitions/Error" - } - }, - "404": { - "description": "No short URL was found for provided short code.", - "schema": { - "$ref": "#/definitions/Error" - } - }, - "500": { - "description": "Unexpected error.", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - } - } - }, - - "definitions": { - "ShortUrl": { - "type": "object", - "properties": { - "shortCode": { - "type": "string", - "description": "The short code for this short URL." - }, - "originalUrl": { - "type": "string", - "description": "The original long URL." - }, - "dateCreated": { - "type": "string", - "format": "date-time", - "description": "The date in which the short URL was created in ISO format." - }, - "visitsCount": { - "type": "integer", - "description": "The number of visits that this short URL has recieved." - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "A list of tags applied to this short URL" - } - } - }, - "Visit": { - "type": "object", - "properties": { - "referer": { - "type": "string" - }, - "date": { - "type": "string", - "format": "date-time" - }, - "remoteAddr": { - "type": "string" - }, - "userAgent": { - "type": "string" - } - } - }, - "Error": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "A machine unique code" - }, - "message": { - "type": "string", - "description": "A human-friendly error message" - } - } - }, - "Pagination": { - "type": "object", - "properties": { - "currentPage": { - "type": "integer", - "description": "The number of current page being displayed." - }, - "pagesCount": { - "type": "integer", - "description": "The total number of pages that can be displayed." - } - } - } - }, - - "parameters": { - "Authorization": { - "name": "Authorization", - "in": "header", - "description": "The authorization token with Bearer type", - "required": true, - "type": "string" - } - } -} diff --git a/docs/swagger/definitions/Error.json b/docs/swagger/definitions/Error.json new file mode 100644 index 00000000..3585227d --- /dev/null +++ b/docs/swagger/definitions/Error.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "A machine unique code" + }, + "message": { + "type": "string", + "description": "A human-friendly error message" + } + } +} diff --git a/docs/swagger/definitions/Pagination.json b/docs/swagger/definitions/Pagination.json new file mode 100644 index 00000000..e9b731ef --- /dev/null +++ b/docs/swagger/definitions/Pagination.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "currentPage": { + "type": "integer", + "description": "The number of current page being displayed." + }, + "pagesCount": { + "type": "integer", + "description": "The total number of pages that can be displayed." + } + } +} diff --git a/docs/swagger/definitions/ShortUrl.json b/docs/swagger/definitions/ShortUrl.json new file mode 100644 index 00000000..6b1750b8 --- /dev/null +++ b/docs/swagger/definitions/ShortUrl.json @@ -0,0 +1,29 @@ +{ + "type": "object", + "properties": { + "shortCode": { + "type": "string", + "description": "The short code for this short URL." + }, + "originalUrl": { + "type": "string", + "description": "The original long URL." + }, + "dateCreated": { + "type": "string", + "format": "date-time", + "description": "The date in which the short URL was created in ISO format." + }, + "visitsCount": { + "type": "integer", + "description": "The number of visits that this short URL has recieved." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of tags applied to this short URL" + } + } +} diff --git a/docs/swagger/definitions/Visit.json b/docs/swagger/definitions/Visit.json new file mode 100644 index 00000000..8e0bc944 --- /dev/null +++ b/docs/swagger/definitions/Visit.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "properties": { + "referer": { + "type": "string" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "remoteAddr": { + "type": "string" + }, + "userAgent": { + "type": "string" + } + } +} diff --git a/docs/swagger/endpoints/v1_authenticate.json b/docs/swagger/endpoints/v1_authenticate.json new file mode 100644 index 00000000..d7828f52 --- /dev/null +++ b/docs/swagger/endpoints/v1_authenticate.json @@ -0,0 +1,46 @@ +{ + "post": { + "description": "Performs an authentication", + "parameters": [ + { + "name": "apiKey", + "in": "formData", + "description": "The API key to authenticate with", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "The authentication worked.", + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "The authentication token that needs to be sent in the Authorization header" + } + } + } + }, + "400": { + "description": "An API key was not provided.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "401": { + "description": "The API key is incorrect, is disabled or has expired.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + } +} diff --git a/docs/swagger/endpoints/v1_short-codes.json b/docs/swagger/endpoints/v1_short-codes.json new file mode 100644 index 00000000..27448d4d --- /dev/null +++ b/docs/swagger/endpoints/v1_short-codes.json @@ -0,0 +1,138 @@ +{ + "get": { + "description": "Returns the list of short codes", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "The page to be displayed. Defaults to 1", + "required": false, + "type": "integer" + }, + { + "name": "searchTerm", + "in": "query", + "description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)", + "required": false, + "type": "string" + }, + { + "name": "tags", + "in": "query", + "description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)", + "required": false, + "type": "array", + "schema": { + "items": { + "type": "string" + } + } + }, + { + "name": "orderBy", + "in": "query", + "description": "The field from which you want to order the result. (Since v1.3.0)", + "enum": [ + "originalUrl", + "shortCode", + "dateCreated", + "visits" + ], + "required": false, + "type": "string" + }, + { + "$ref": "../parameters/Authorization.json" + } + ], + "responses": { + "200": { + "description": "The list of short URLs", + "schema": { + "type": "object", + "properties": { + "shortUrls": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "../definitions/ShortUrl.json" + } + }, + "pagination": { + "$ref": "../definitions/Pagination.json" + } + } + } + } + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + }, + "post": { + "description": "Creates a new short code", + "parameters": [ + { + "name": "longUrl", + "in": "formData", + "description": "The URL to parse", + "required": true, + "type": "string" + }, + { + "name": "tags", + "in": "formData", + "description": "The URL to parse", + "required": false, + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "../parameters/Authorization.json" + } + ], + "responses": { + "200": { + "description": "The result of parsing the long URL", + "schema": { + "type": "object", + "properties": { + "longUrl": { + "type": "string", + "description": "The original long URL that has been parsed" + }, + "shortUrl": { + "type": "string", + "description": "The generated short URL" + }, + "shortCode": { + "type": "string", + "description": "the short code that is being used in the short URL" + } + } + } + }, + "400": { + "description": "The long URL was not provided or is invalid.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + } +} diff --git a/docs/swagger/endpoints/v1_short-codes_{shortCode}.json b/docs/swagger/endpoints/v1_short-codes_{shortCode}.json new file mode 100644 index 00000000..1a24dcf5 --- /dev/null +++ b/docs/swagger/endpoints/v1_short-codes_{shortCode}.json @@ -0,0 +1,49 @@ +{ + "get": { + "description": "Get the long URL behind a short code.", + "parameters": [ + { + "name": "shortCode", + "in": "path", + "type": "string", + "description": "The short code to resolve.", + "required": true + }, + { + "$ref": "../parameters/Authorization.json" + } + ], + "responses": { + "200": { + "description": "The long URL behind a short code.", + "schema": { + "type": "object", + "properties": { + "longUrl": { + "type": "string", + "description": "The original long URL behind the short code." + } + } + } + }, + "400": { + "description": "Provided shortCode does not match the character set currently used by the app to generate short codes.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "404": { + "description": "No URL was found for provided short code.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + } +} diff --git a/docs/swagger/endpoints/v1_short-codes_{shortCode}_tags.json b/docs/swagger/endpoints/v1_short-codes_{shortCode}_tags.json new file mode 100644 index 00000000..8ff67199 --- /dev/null +++ b/docs/swagger/endpoints/v1_short-codes_{shortCode}_tags.json @@ -0,0 +1,61 @@ +{ + "put": { + "description": "Edit the tags on provided short code.", + "parameters": [ + { + "name": "shortCode", + "in": "path", + "type": "string", + "description": "The shortCode in which we want to edit tags.", + "required": true + }, + { + "name": "tags", + "in": "formData", + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of tags to set to the short URL.", + "required": true + }, + { + "$ref": "../parameters/Authorization.json" + } + ], + "responses": { + "200": { + "description": "List of tags.", + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "400": { + "description": "The request body does not contain a \"tags\" param with array type.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "404": { + "description": "No short URL was found for provided short code.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + } +} diff --git a/docs/swagger/endpoints/v1_short-codes_{shortCode}_visits.json b/docs/swagger/endpoints/v1_short-codes_{shortCode}_visits.json new file mode 100644 index 00000000..3b416eb0 --- /dev/null +++ b/docs/swagger/endpoints/v1_short-codes_{shortCode}_visits.json @@ -0,0 +1,50 @@ +{ + "get": { + "description": "Get the list of visits on provided short code.", + "parameters": [ + { + "name": "shortCode", + "in": "path", + "type": "string", + "description": "The shortCode from which we want to get the visits.", + "required": true + }, + { + "$ref": "../parameters/Authorization.json" + } + ], + "responses": { + "200": { + "description": "List of visits.", + "schema": { + "type": "object", + "properties": { + "visits": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "../definitions/Visit.json" + } + } + } + } + } + } + }, + "404": { + "description": "The short code does not belong to any short URL.", + "schema": { + "$ref": "../definitions/Error.json" + } + }, + "500": { + "description": "Unexpected error.", + "schema": { + "$ref": "../definitions/Error.json" + } + } + } + } +} diff --git a/docs/swagger/parameters/Authorization.json b/docs/swagger/parameters/Authorization.json new file mode 100644 index 00000000..54386de6 --- /dev/null +++ b/docs/swagger/parameters/Authorization.json @@ -0,0 +1,7 @@ +{ + "name": "Authorization", + "in": "header", + "description": "The authorization token with Bearer type", + "required": true, + "type": "string" +} diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json new file mode 100644 index 00000000..bdc3c023 --- /dev/null +++ b/docs/swagger/swagger.json @@ -0,0 +1,33 @@ +{ + "swagger": "2.0", + "info": { + "title": "Shlink", + "description": "Shlink, the self-hosted URL shortener", + "version": "1.2.0" + }, + "schemes": [ + "https" + ], + "basePath": "/rest", + "produces": [ + "application/json" + ], + + "paths": { + "/v1/authenticate": { + "$ref": "endpoints/v1_authenticate.json" + }, + "/v1/short-codes": { + "$ref": "endpoints/v1_short-codes.json" + }, + "/v1/short-codes/{shortCode}": { + "$ref": "endpoints/v1_short-codes_{shortCode}.json" + }, + "/v1/short-codes/{shortCode}/visits": { + "$ref": "endpoints/v1_short-codes_{shortCode}_visits.json" + }, + "/v1/short-codes/{shortCode}/tags": { + "$ref": "endpoints/v1_short-codes_{shortCode}_tags.json" + } + } +} From b225c03ef11636a78a901fd4a707bc8a3e56fa31 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 23 Oct 2016 00:02:13 +0200 Subject: [PATCH 19/21] Improved swagger definition --- .../{endpoints => paths}/v1_authenticate.json | 0 .../{endpoints => paths}/v1_short-codes.json | 0 .../v1_short-codes_{shortCode}.json | 0 .../v1_short-codes_{shortCode}_tags.json | 0 .../v1_short-codes_{shortCode}_visits.json | 0 docs/swagger/swagger.json | 15 ++++++++++----- 6 files changed, 10 insertions(+), 5 deletions(-) rename docs/swagger/{endpoints => paths}/v1_authenticate.json (100%) rename docs/swagger/{endpoints => paths}/v1_short-codes.json (100%) rename docs/swagger/{endpoints => paths}/v1_short-codes_{shortCode}.json (100%) rename docs/swagger/{endpoints => paths}/v1_short-codes_{shortCode}_tags.json (100%) rename docs/swagger/{endpoints => paths}/v1_short-codes_{shortCode}_visits.json (100%) diff --git a/docs/swagger/endpoints/v1_authenticate.json b/docs/swagger/paths/v1_authenticate.json similarity index 100% rename from docs/swagger/endpoints/v1_authenticate.json rename to docs/swagger/paths/v1_authenticate.json diff --git a/docs/swagger/endpoints/v1_short-codes.json b/docs/swagger/paths/v1_short-codes.json similarity index 100% rename from docs/swagger/endpoints/v1_short-codes.json rename to docs/swagger/paths/v1_short-codes.json diff --git a/docs/swagger/endpoints/v1_short-codes_{shortCode}.json b/docs/swagger/paths/v1_short-codes_{shortCode}.json similarity index 100% rename from docs/swagger/endpoints/v1_short-codes_{shortCode}.json rename to docs/swagger/paths/v1_short-codes_{shortCode}.json diff --git a/docs/swagger/endpoints/v1_short-codes_{shortCode}_tags.json b/docs/swagger/paths/v1_short-codes_{shortCode}_tags.json similarity index 100% rename from docs/swagger/endpoints/v1_short-codes_{shortCode}_tags.json rename to docs/swagger/paths/v1_short-codes_{shortCode}_tags.json diff --git a/docs/swagger/endpoints/v1_short-codes_{shortCode}_visits.json b/docs/swagger/paths/v1_short-codes_{shortCode}_visits.json similarity index 100% rename from docs/swagger/endpoints/v1_short-codes_{shortCode}_visits.json rename to docs/swagger/paths/v1_short-codes_{shortCode}_visits.json diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index bdc3c023..6e9244f7 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -6,28 +6,33 @@ "version": "1.2.0" }, "schemes": [ + "http", "https" ], "basePath": "/rest", "produces": [ "application/json" ], + "consumes": [ + "application/x-www-form-urlencoded", + "application/json" + ], "paths": { "/v1/authenticate": { - "$ref": "endpoints/v1_authenticate.json" + "$ref": "paths/v1_authenticate.json" }, "/v1/short-codes": { - "$ref": "endpoints/v1_short-codes.json" + "$ref": "paths/v1_short-codes.json" }, "/v1/short-codes/{shortCode}": { - "$ref": "endpoints/v1_short-codes_{shortCode}.json" + "$ref": "paths/v1_short-codes_{shortCode}.json" }, "/v1/short-codes/{shortCode}/visits": { - "$ref": "endpoints/v1_short-codes_{shortCode}_visits.json" + "$ref": "paths/v1_short-codes_{shortCode}_visits.json" }, "/v1/short-codes/{shortCode}/tags": { - "$ref": "endpoints/v1_short-codes_{shortCode}_tags.json" + "$ref": "paths/v1_short-codes_{shortCode}_tags.json" } } } From 8af9b0ee02de3dbd30ffd6ed4c027c8885aba75b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 23 Oct 2016 00:07:31 +0200 Subject: [PATCH 20/21] Tagged and summarized all endpoints in swagger docs --- docs/swagger/paths/v1_authenticate.json | 4 ++++ docs/swagger/paths/v1_short-codes.json | 8 ++++++++ docs/swagger/paths/v1_short-codes_{shortCode}.json | 4 ++++ docs/swagger/paths/v1_short-codes_{shortCode}_tags.json | 5 +++++ docs/swagger/paths/v1_short-codes_{shortCode}_visits.json | 5 +++++ 5 files changed, 26 insertions(+) diff --git a/docs/swagger/paths/v1_authenticate.json b/docs/swagger/paths/v1_authenticate.json index d7828f52..843081bb 100644 --- a/docs/swagger/paths/v1_authenticate.json +++ b/docs/swagger/paths/v1_authenticate.json @@ -1,5 +1,9 @@ { "post": { + "tags": [ + "Authentication" + ], + "summary": "Perform authentication", "description": "Performs an authentication", "parameters": [ { diff --git a/docs/swagger/paths/v1_short-codes.json b/docs/swagger/paths/v1_short-codes.json index 27448d4d..42b22f82 100644 --- a/docs/swagger/paths/v1_short-codes.json +++ b/docs/swagger/paths/v1_short-codes.json @@ -1,5 +1,9 @@ { "get": { + "tags": [ + "ShortCodes" + ], + "summary": "List short URLs", "description": "Returns the list of short codes", "parameters": [ { @@ -77,6 +81,10 @@ } }, "post": { + "tags": [ + "ShortCodes" + ], + "summary": "Create short URL", "description": "Creates a new short code", "parameters": [ { diff --git a/docs/swagger/paths/v1_short-codes_{shortCode}.json b/docs/swagger/paths/v1_short-codes_{shortCode}.json index 1a24dcf5..a706f675 100644 --- a/docs/swagger/paths/v1_short-codes_{shortCode}.json +++ b/docs/swagger/paths/v1_short-codes_{shortCode}.json @@ -1,5 +1,9 @@ { "get": { + "tags": [ + "ShortCodes" + ], + "summary": "Parse short code", "description": "Get the long URL behind a short code.", "parameters": [ { diff --git a/docs/swagger/paths/v1_short-codes_{shortCode}_tags.json b/docs/swagger/paths/v1_short-codes_{shortCode}_tags.json index 8ff67199..3cbfc952 100644 --- a/docs/swagger/paths/v1_short-codes_{shortCode}_tags.json +++ b/docs/swagger/paths/v1_short-codes_{shortCode}_tags.json @@ -1,5 +1,10 @@ { "put": { + "tags": [ + "ShortCodes", + "Tags" + ], + "summary": "Edit tags on short URL", "description": "Edit the tags on provided short code.", "parameters": [ { diff --git a/docs/swagger/paths/v1_short-codes_{shortCode}_visits.json b/docs/swagger/paths/v1_short-codes_{shortCode}_visits.json index 3b416eb0..f90daf7a 100644 --- a/docs/swagger/paths/v1_short-codes_{shortCode}_visits.json +++ b/docs/swagger/paths/v1_short-codes_{shortCode}_visits.json @@ -1,5 +1,10 @@ { "get": { + "tags": [ + "ShortCodes", + "Visits" + ], + "summary": "List visits for short URL", "description": "Get the list of visits on provided short code.", "parameters": [ { From bf7c760ca9f3327b4a90da278d81394f13a16bd5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 23 Oct 2016 00:13:00 +0200 Subject: [PATCH 21/21] Updated changelog with last versions --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 643f085f..91b5a74e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ ## CHANGELOG +### 1.3.0 + +**Enhancements:** + +* [67: Allow to order the short codes list](https://github.com/acelaya/url-shortener/issues/67) +* [60: Accept JSON requests in REST and use a body parser middleware to set the parsedBody](https://github.com/acelaya/url-shortener/issues/60) +* [72: When listing API keys from CLI, display in yellow color enabled keys that have expired](https://github.com/acelaya/url-shortener/issues/72) +* [58: Allow to filter short URLs by tag](https://github.com/acelaya/url-shortener/issues/58) +* [69: Allow to filter short codes by text query](https://github.com/acelaya/url-shortener/issues/69) + +**Tasks** + +* [73: Tag endpoints in swagger file](https://github.com/acelaya/url-shortener/issues/73) +* [71: Separate swagger docs into multiple files](https://github.com/acelaya/url-shortener/issues/71) +* [63: Add path versioning to REST API routes](https://github.com/acelaya/url-shortener/issues/63) + +### 1.2.2 + +**Bugs** + +* Fixed minor bugs on CORS requests + ### 1.2.1 **Bugs**