mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-24 13:49:03 +03:00
commit
014eb2a924
11 changed files with 217 additions and 9 deletions
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
* [#411](https://github.com/shlinkio/shlink/issues/411) Added new `meta` property on the `ShortUrl` REST API model.
|
||||||
|
|
||||||
|
These endpoints are affected and include the new property when suitable:
|
||||||
|
|
||||||
|
* `GET /short-urls` - List short URLs.
|
||||||
|
* `GET /short-urls/shorten` - Create a short URL (for integrations).
|
||||||
|
* `GET /short-urls/{shortCode}` - Get one short URL.
|
||||||
|
* `POST /short-urls` - Create short URL.
|
||||||
|
|
||||||
|
The property includes the values `validSince`, `validUntil` and `maxVisits` in a single object. All of them are nullable.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"validSince": "2016-01-01T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
|
||||||
## 1.17.0 - 2019-05-13
|
## 1.17.0 - 2019-05-13
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
},
|
},
|
||||||
"description": "A list of tags applied to this short URL"
|
"description": "A list of tags applied to this short URL"
|
||||||
},
|
},
|
||||||
|
"meta": {
|
||||||
|
"$ref": "./ShortUrlMeta.json"
|
||||||
|
},
|
||||||
"originalUrl": {
|
"originalUrl": {
|
||||||
"deprecated": true,
|
"deprecated": true,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
21
docs/swagger/definitions/ShortUrlMeta.json
Normal file
21
docs/swagger/definitions/ShortUrlMeta.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["validSince", "validUntil", "maxVisits"],
|
||||||
|
"properties": {
|
||||||
|
"validSince": {
|
||||||
|
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"validUntil": {
|
||||||
|
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"maxVisits": {
|
||||||
|
"description": "The maximum number of allowed visits for this short code",
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -100,7 +100,12 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"games",
|
"games",
|
||||||
"tech"
|
"tech"
|
||||||
]
|
],
|
||||||
|
"meta": {
|
||||||
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": 100
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"shortCode": "12Kb3",
|
"shortCode": "12Kb3",
|
||||||
|
@ -110,7 +115,12 @@
|
||||||
"visitsCount": 1029,
|
"visitsCount": 1029,
|
||||||
"tags": [
|
"tags": [
|
||||||
"shlink"
|
"shlink"
|
||||||
]
|
],
|
||||||
|
"meta": {
|
||||||
|
"validSince": null,
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"shortCode": "123bA",
|
"shortCode": "123bA",
|
||||||
|
@ -118,7 +128,12 @@
|
||||||
"longUrl": "https://www.google.com",
|
"longUrl": "https://www.google.com",
|
||||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||||
"visitsCount": 25,
|
"visitsCount": 25,
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"meta": {
|
||||||
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pagination": {
|
"pagination": {
|
||||||
|
@ -227,7 +242,12 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"games",
|
"games",
|
||||||
"tech"
|
"tech"
|
||||||
]
|
],
|
||||||
|
"meta": {
|
||||||
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": 500
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -64,7 +64,12 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"games",
|
"games",
|
||||||
"tech"
|
"tech"
|
||||||
]
|
],
|
||||||
|
"meta": {
|
||||||
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": 100
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"text/plain": "https://doma.in/abc123"
|
"text/plain": "https://doma.in/abc123"
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,12 @@
|
||||||
"visitsCount": 1029,
|
"visitsCount": 1029,
|
||||||
"tags": [
|
"tags": [
|
||||||
"shlink"
|
"shlink"
|
||||||
]
|
],
|
||||||
|
"meta": {
|
||||||
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
|
"validUntil": null,
|
||||||
|
"maxVisits": 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,6 +17,8 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\Paginator\Paginator;
|
use Zend\Paginator\Paginator;
|
||||||
|
|
||||||
|
use function array_flip;
|
||||||
|
use function array_intersect_key;
|
||||||
use function array_values;
|
use function array_values;
|
||||||
use function count;
|
use function count;
|
||||||
use function explode;
|
use function explode;
|
||||||
|
@ -29,6 +31,14 @@ class ListShortUrlsCommand extends Command
|
||||||
|
|
||||||
public const NAME = 'short-url:list';
|
public const NAME = 'short-url:list';
|
||||||
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
||||||
|
private const COLUMNS_WHITELIST = [
|
||||||
|
'shortCode',
|
||||||
|
'shortUrl',
|
||||||
|
'longUrl',
|
||||||
|
'dateCreated',
|
||||||
|
'visitsCount',
|
||||||
|
'tags',
|
||||||
|
];
|
||||||
|
|
||||||
/** @var ShortUrlServiceInterface */
|
/** @var ShortUrlServiceInterface */
|
||||||
private $shortUrlService;
|
private $shortUrlService;
|
||||||
|
@ -125,8 +135,7 @@ class ListShortUrlsCommand extends Command
|
||||||
unset($shortUrl['tags']);
|
unset($shortUrl['tags']);
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($shortUrl['originalUrl']);
|
$rows[] = array_values(array_intersect_key($shortUrl, array_flip(self::COLUMNS_WHITELIST)));
|
||||||
$rows[] = array_values($shortUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
|
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
|
||||||
|
|
|
@ -8,6 +8,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait;
|
use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait;
|
||||||
|
|
||||||
use function Functional\invoke;
|
use function Functional\invoke;
|
||||||
|
use function Functional\invoke_if;
|
||||||
|
|
||||||
class ShortUrlDataTransformer implements DataTransformerInterface
|
class ShortUrlDataTransformer implements DataTransformerInterface
|
||||||
{
|
{
|
||||||
|
@ -36,9 +37,23 @@ class ShortUrlDataTransformer implements DataTransformerInterface
|
||||||
'dateCreated' => $value->getDateCreated()->toAtomString(),
|
'dateCreated' => $value->getDateCreated()->toAtomString(),
|
||||||
'visitsCount' => $value->getVisitsCount(),
|
'visitsCount' => $value->getVisitsCount(),
|
||||||
'tags' => invoke($value->getTags(), '__toString'),
|
'tags' => invoke($value->getTags(), '__toString'),
|
||||||
|
'meta' => $this->buildMeta($value),
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
'originalUrl' => $longUrl,
|
'originalUrl' => $longUrl,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildMeta(ShortUrl $shortUrl): array
|
||||||
|
{
|
||||||
|
$validSince = $shortUrl->getValidSince();
|
||||||
|
$validUntil = $shortUrl->getValidUntil();
|
||||||
|
$maxVisits = $shortUrl->getMaxVisits();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'validSince' => invoke_if($validSince, 'toAtomString'),
|
||||||
|
'validUntil' => invoke_if($validUntil, 'toAtomString'),
|
||||||
|
'maxVisits' => $maxVisits,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
75
module/Core/test/Transformer/ShortUrlDataTransformerTest.php
Normal file
75
module/Core/test/Transformer/ShortUrlDataTransformerTest.php
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\Core\Transformer;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||||
|
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||||
|
|
||||||
|
use function random_int;
|
||||||
|
|
||||||
|
class ShortUrlDataTransformerTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var ShortUrlDataTransformer */
|
||||||
|
private $transformer;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->transformer = new ShortUrlDataTransformer([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideShortUrls
|
||||||
|
*/
|
||||||
|
public function properMetadataIsReturned(ShortUrl $shortUrl, array $expectedMeta): void
|
||||||
|
{
|
||||||
|
['meta' => $meta] = $this->transformer->transform($shortUrl);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedMeta, $meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideShortUrls(): iterable
|
||||||
|
{
|
||||||
|
$maxVisits = random_int(1, 1000);
|
||||||
|
$now = Chronos::now();
|
||||||
|
|
||||||
|
yield 'no metadata' => [new ShortUrl('', ShortUrlMeta::createEmpty()), [
|
||||||
|
'validSince' => null,
|
||||||
|
'validUntil' => null,
|
||||||
|
'maxVisits' => null,
|
||||||
|
]];
|
||||||
|
yield 'max visits only' => [new ShortUrl('', ShortUrlMeta::createFromParams(null, null, null, $maxVisits)), [
|
||||||
|
'validSince' => null,
|
||||||
|
'validUntil' => null,
|
||||||
|
'maxVisits' => $maxVisits,
|
||||||
|
]];
|
||||||
|
yield 'max visits and valid since' => [
|
||||||
|
new ShortUrl('', ShortUrlMeta::createFromParams($now, null, null, $maxVisits)),
|
||||||
|
[
|
||||||
|
'validSince' => $now->toAtomString(),
|
||||||
|
'validUntil' => null,
|
||||||
|
'maxVisits' => $maxVisits,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
yield 'both dates' => [
|
||||||
|
new ShortUrl('', ShortUrlMeta::createFromParams($now, $now->subDays(10))),
|
||||||
|
[
|
||||||
|
'validSince' => $now->toAtomString(),
|
||||||
|
'validUntil' => $now->subDays(10)->toAtomString(),
|
||||||
|
'maxVisits' => null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
yield 'everything' => [
|
||||||
|
new ShortUrl('', ShortUrlMeta::createFromParams($now, $now->subDays(5), null, $maxVisits)),
|
||||||
|
[
|
||||||
|
'validSince' => $now->toAtomString(),
|
||||||
|
'validUntil' => $now->subDays(5)->toAtomString(),
|
||||||
|
'maxVisits' => $maxVisits,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,11 @@ class ListShortUrlsTest extends ApiTestCase
|
||||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||||
'visitsCount' => 3,
|
'visitsCount' => 3,
|
||||||
'tags' => ['foo'],
|
'tags' => ['foo'],
|
||||||
|
'meta' => [
|
||||||
|
'validSince' => null,
|
||||||
|
'validUntil' => null,
|
||||||
|
'maxVisits' => null,
|
||||||
|
],
|
||||||
'originalUrl' => 'https://shlink.io',
|
'originalUrl' => 'https://shlink.io',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
@ -35,6 +40,11 @@ class ListShortUrlsTest extends ApiTestCase
|
||||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||||
'visitsCount' => 2,
|
'visitsCount' => 2,
|
||||||
'tags' => ['foo', 'bar'],
|
'tags' => ['foo', 'bar'],
|
||||||
|
'meta' => [
|
||||||
|
'validSince' => '2020-05-01T00:00:00+00:00',
|
||||||
|
'validUntil' => null,
|
||||||
|
'maxVisits' => null,
|
||||||
|
],
|
||||||
'originalUrl' =>
|
'originalUrl' =>
|
||||||
'https://blog.alejandrocelaya.com/2017/12/09'
|
'https://blog.alejandrocelaya.com/2017/12/09'
|
||||||
. '/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
. '/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
||||||
|
@ -46,6 +56,11 @@ class ListShortUrlsTest extends ApiTestCase
|
||||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||||
'visitsCount' => 0,
|
'visitsCount' => 0,
|
||||||
'tags' => [],
|
'tags' => [],
|
||||||
|
'meta' => [
|
||||||
|
'validSince' => null,
|
||||||
|
'validUntil' => null,
|
||||||
|
'maxVisits' => 2,
|
||||||
|
],
|
||||||
'originalUrl' => 'https://shlink.io',
|
'originalUrl' => 'https://shlink.io',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
|
@ -24,7 +24,7 @@ class ShortUrlsFixture extends AbstractFixture
|
||||||
|
|
||||||
$defShortUrl = $this->setShortUrlDate(new ShortUrl(
|
$defShortUrl = $this->setShortUrlDate(new ShortUrl(
|
||||||
'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
||||||
ShortUrlMeta::createFromParams(Chronos::now()->addDays(3))
|
ShortUrlMeta::createFromParams(Chronos::parse('2020-05-01'))
|
||||||
))->setShortCode('def456');
|
))->setShortCode('def456');
|
||||||
$manager->persist($defShortUrl);
|
$manager->persist($defShortUrl);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue