diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0b6bb9..971895bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this Also, when using swoole, the file is now updated **after** tracking a visit, which means it will not apply until the next one. +* [#1059](https://github.com/shlinkio/shlink/issues/1059) Added ability to optionally display author API key and its name when listing short URLs from the command line. + ### Changed * [#1036](https://github.com/shlinkio/shlink/issues/1036) Updated to `happyr/doctrine-specification` 2.0. * [#1039](https://github.com/shlinkio/shlink/issues/1039) Updated to `endroid/qr-code` 4.0. diff --git a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php index 24689bcb..0d637f5f 100644 --- a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php +++ b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php @@ -10,6 +10,7 @@ use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait; use Shlinkio\Shlink\Common\Rest\DataTransformerInterface; +use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; @@ -19,6 +20,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use function array_keys; use function array_pad; use function explode; use function Functional\map; @@ -30,18 +32,6 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand use PagerfantaUtilsTrait; public const NAME = 'short-url:list'; - private const COLUMNS_TO_SHOW = [ - 'shortCode', - 'title', - 'shortUrl', - 'longUrl', - 'dateCreated', - 'visitsCount', - ]; - private const COLUMNS_TO_SHOW_WITH_TAGS = [ - ...self::COLUMNS_TO_SHOW, - 'tags', - ]; private ShortUrlServiceInterface $shortUrlService; private DataTransformerInterface $transformer; @@ -90,6 +80,18 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand InputOption::VALUE_NONE, 'Whether to display the tags or not.', ) + ->addOption( + 'show-api-key', + 'k', + InputOption::VALUE_NONE, + 'Whether to display the API key from which the URL was generated or not.', + ) + ->addOption( + 'show-api-key-name', + 'm', + InputOption::VALUE_NONE, + 'Whether to display the API key name from which the URL was generated or not.', + ) ->addOption( 'all', 'a', @@ -117,11 +119,11 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand $searchTerm = $this->getOptionWithDeprecatedFallback($input, 'search-term'); $tags = $input->getOption('tags'); $tags = ! empty($tags) ? explode(',', $tags) : []; - $showTags = $this->getOptionWithDeprecatedFallback($input, 'show-tags'); $all = $input->getOption('all'); $startDate = $this->getStartDateOption($input, $output); $endDate = $this->getEndDateOption($input, $output); $orderBy = $this->processOrderBy($input); + $columnsMap = $this->resolveColumnsMap($input); $data = [ ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm, @@ -137,7 +139,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand do { $data[ShortUrlsParamsInputFilter::PAGE] = $page; - $result = $this->renderPage($output, $showTags, ShortUrlsParams::fromRawData($data), $all); + $result = $this->renderPage($output, $columnsMap, ShortUrlsParams::fromRawData($data), $all); $page++; $continue = $result->hasNextPage() && $io->confirm( @@ -152,32 +154,26 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand return ExitCodes::EXIT_SUCCESS; } - private function renderPage(OutputInterface $output, bool $showTags, ShortUrlsParams $params, bool $all): Paginator - { - $result = $this->shortUrlService->listShortUrls($params); + private function renderPage( + OutputInterface $output, + array $columnsMap, + ShortUrlsParams $params, + bool $all + ): Paginator { + $shortUrls = $this->shortUrlService->listShortUrls($params); - $headers = ['Short code', 'Title', 'Short URL', 'Long URL', 'Date created', 'Visits count']; - if ($showTags) { - $headers[] = 'Tags'; - } + $rows = map($shortUrls, function (ShortUrl $shortUrl) use ($columnsMap) { + $rawShortUrl = $this->transformer->transform($shortUrl); + return map($columnsMap, fn (callable $call) => $call($rawShortUrl, $shortUrl)); + }); - $rows = []; - foreach ($result as $row) { - $columnsToShow = $showTags ? self::COLUMNS_TO_SHOW_WITH_TAGS : self::COLUMNS_TO_SHOW; - $shortUrl = $this->transformer->transform($row); - if ($showTags) { - $shortUrl['tags'] = implode(', ', $shortUrl['tags']); - } + ShlinkTable::fromOutput($output)->render( + array_keys($columnsMap), + $rows, + $all ? null : $this->formatCurrentPageMessage($shortUrls, 'Page %s of %s'), + ); - $rows[] = map($columnsToShow, fn (string $prop) => $shortUrl[$prop]); - } - - ShlinkTable::fromOutput($output)->render($headers, $rows, $all ? null : $this->formatCurrentPageMessage( - $result, - 'Page %s of %s', - )); - - return $result; + return $shortUrls; } private function processOrderBy(InputInterface $input): ?string @@ -190,4 +186,33 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand [$field, $dir] = array_pad(explode(',', $orderBy), 2, null); return $dir === null ? $field : sprintf('%s-%s', $field, $dir); } + + private function resolveColumnsMap(InputInterface $input): array + { + $pickProp = static fn (string $prop): callable => static fn (array $shortUrl) => $shortUrl[$prop]; + $columnsMap = [ + 'Short Code' => $pickProp('shortCode'), + 'Title' => $pickProp('title'), + 'Short URL' => $pickProp('shortUrl'), + 'Long URL' => $pickProp('longUrl'), + 'Date created' => $pickProp('dateCreated'), + 'Visits count' => $pickProp('visitsCount'), + ]; + if ($this->getOptionWithDeprecatedFallback($input, 'show-tags')) { + $columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']); + } + if ($input->getOption('show-api-key')) { + $columnsMap['API Key'] = static fn (array $_, ShortUrl $shortUrl): string => + (string) $shortUrl->authorApiKey(); + } + if ($input->getOption('show-api-key-name')) { + $columnsMap['API Key Name'] = static function (array $_, ShortUrl $shortUrl): ?string { + $apiKey = $shortUrl->authorApiKey(); + + return $apiKey !== null ? $apiKey->name() : null; + }; + } + + return $columnsMap; + } } diff --git a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php index 08519b62..6f7b11a6 100644 --- a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php @@ -12,13 +12,17 @@ use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier; use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer; +use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta; +use Shlinkio\Shlink\Rest\Entity\ApiKey; use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait; use Symfony\Component\Console\Tester\CommandTester; +use function count; use function explode; class ListShortUrlsCommandTest extends TestCase @@ -98,17 +102,77 @@ class ListShortUrlsCommandTest extends TestCase $this->commandTester->execute(['--page' => $page]); } - /** @test */ - public function ifTagsFlagIsProvidedTagsColumnIsIncluded(): void - { + /** + * @test + * @dataProvider provideOptionalFlags + */ + public function provideOptionalFlagsMakesNewColumnsToBeIncluded( + array $input, + array $expectedContents, + array $notExpectedContents, + ApiKey $apiKey + ): void { $this->shortUrlService->listShortUrls(ShortUrlsParams::emptyInstance()) - ->willReturn(new Paginator(new ArrayAdapter([]))) + ->willReturn(new Paginator(new ArrayAdapter([ + ShortUrl::fromMeta(ShortUrlMeta::fromRawData([ + 'longUrl' => 'foo.com', + 'tags' => ['foo', 'bar', 'baz'], + 'apiKey' => $apiKey, + ])), + ]))) ->shouldBeCalledOnce(); $this->commandTester->setInputs(['y']); - $this->commandTester->execute(['--show-tags' => true]); + $this->commandTester->execute($input); $output = $this->commandTester->getDisplay(); - self::assertStringContainsString('Tags', $output); + + if (count($expectedContents) === 0 && count($notExpectedContents) === 0) { + self::fail('No expectations were run'); + } + + foreach ($expectedContents as $column) { + self::assertStringContainsString($column, $output); + } + foreach ($notExpectedContents as $column) { + self::assertStringNotContainsString($column, $output); + } + } + + public function provideOptionalFlags(): iterable + { + $apiKey = ApiKey::fromMeta(ApiKeyMeta::withName('my api key')); + $key = $apiKey->toString(); + + yield 'tags only' => [ + ['--show-tags' => true], + ['| Tags ', '| foo, bar, baz'], + ['| API Key ', '| API Key Name |', $key, '| my api key'], + $apiKey, + ]; + yield 'api key only' => [ + ['--show-api-key' => true], + ['| API Key ', $key], + ['| Tags ', '| foo, bar, baz', '| API Key Name |', '| my api key'], + $apiKey, + ]; + yield 'api key name only' => [ + ['--show-api-key-name' => true], + ['| API Key Name |', '| my api key'], + ['| Tags ', '| foo, bar, baz', '| API Key ', $key], + $apiKey, + ]; + yield 'tags and api key' => [ + ['--show-tags' => true, '--show-api-key' => true], + ['| API Key ', '| Tags ', '| foo, bar, baz', $key], + ['| API Key Name |', '| my api key'], + $apiKey, + ]; + yield 'all' => [ + ['--show-tags' => true, '--show-api-key' => true, '--show-api-key-name' => true], + ['| API Key ', '| Tags ', '| API Key Name |', '| foo, bar, baz', $key, '| my api key'], + [], + $apiKey, + ]; } /** diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index 810281fa..84f215de 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -132,6 +132,11 @@ class ShortUrl extends AbstractEntity return $this->tags; } + public function authorApiKey(): ?ApiKey + { + return $this->authorApiKey; + } + public function getValidSince(): ?Chronos { return $this->validSince;