diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f4acd1..d28ef51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ 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). +## 2.0.1 - 2020-01-10 + +#### Added + +* *Nothing* + +#### Changed + +* *Nothing* + +#### Deprecated + +* *Nothing* + +#### Removed + +* *Nothing* + +#### Fixed + +* [#607](https://github.com/shlinkio/shlink/issues/607) Added missing info on UPGRADE.md doc. +* [#610](https://github.com/shlinkio/shlink/issues/610) Fixed use of hardcoded quotes on a database migration which makes it fail on postgres. +* [#605](https://github.com/shlinkio/shlink/issues/605) Fixed crashes occurring when migrating from old Shlink versions with nullable DB columns that are assigned to non-nullable entity typed props. + + ## 2.0.0 - 2020-01-08 #### Added diff --git a/UPGRADE.md b/UPGRADE.md index 42b67fe9..ebe6ad17 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -2,6 +2,14 @@ ## From v1.x to v2.x +### PHP 7.4 required + +This new version takes advantage of several new features introduced in PHP 7.4. + +Thanks to that, the code is more reliable and robust, and easier to maintain and improve. + +However, that means that any previous PHP version is no longer supported. + ### Preview generation The ability to generate website previews has been completely removed and has no replacement. @@ -43,6 +51,16 @@ Endpoints need to provide a version in the path now. Previously, not providing a The only exception is the `/rest/health` endpoint, which will continue working without the version. +### API errors + +Shlink v1.21.0 introduced support for API errors using the Problem Details format, as well as the v2 of the API. + +For backwards compatibility reasons, requests performed to v1 continued to return the old `error` and `message` properties. + +Starting with Shlink v2.0.0, both versions of the API will no longer return those two properties. + +As a replacement, use `type` instead of `error`, and `detail` instead of `message`. + ### Changes in models The next REST API models have changed: diff --git a/data/migrations/Version20200105165647.php b/data/migrations/Version20200105165647.php index 24b9f984..6367f440 100644 --- a/data/migrations/Version20200105165647.php +++ b/data/migrations/Version20200105165647.php @@ -9,18 +9,35 @@ use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Types\Types; use Doctrine\Migrations\AbstractMigration; +use function Functional\some; + final class Version20200105165647 extends AbstractMigration { private const COLUMNS = ['lat' => 'latitude', 'lon' => 'longitude']; + /** + * @throws DBALException + */ public function preUp(Schema $schema): void { + $visitLocations = $schema->getTable('visit_locations'); + $this->skipIf(some( + self::COLUMNS, + fn (string $v, string $newColName) => $visitLocations->hasColumn($newColName), + ), 'New columns already exist'); + foreach (self::COLUMNS as $columnName) { $qb = $this->connection->createQueryBuilder(); $qb->update('visit_locations') - ->set($columnName, '"0"') - ->where($columnName . '=""') - ->orWhere($columnName . ' IS NULL') + ->set($columnName, ':zeroValue') + ->where($qb->expr()->orX( + $qb->expr()->eq($columnName, ':emptyString'), + $qb->expr()->isNull($columnName), + )) + ->setParameters([ + 'zeroValue' => '0', + 'emptyString' => '', + ]) ->execute(); } } @@ -33,16 +50,24 @@ final class Version20200105165647 extends AbstractMigration $visitLocations = $schema->getTable('visit_locations'); foreach (self::COLUMNS as $newName => $oldName) { - $visitLocations->addColumn($newName, Types::FLOAT); + $visitLocations->addColumn($newName, Types::FLOAT, [ + 'default' => '0.0', + ]); } } + /** + * @throws DBALException + */ public function postUp(Schema $schema): void { + $platformName = $this->connection->getDatabasePlatform()->getName(); + $castType = $platformName === 'postgres' ? 'DOUBLE PRECISION' : 'DECIMAL(9,2)'; + foreach (self::COLUMNS as $newName => $oldName) { $qb = $this->connection->createQueryBuilder(); $qb->update('visit_locations') - ->set($newName, $oldName) + ->set($newName, 'CAST(' . $oldName . ' AS ' . $castType . ')') ->execute(); } } diff --git a/data/migrations/Version20200106215144.php b/data/migrations/Version20200106215144.php index 8969441b..e7a2c31a 100644 --- a/data/migrations/Version20200106215144.php +++ b/data/migrations/Version20200106215144.php @@ -9,6 +9,8 @@ use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Types\Types; use Doctrine\Migrations\AbstractMigration; +use function Functional\none; + final class Version20200106215144 extends AbstractMigration { private const COLUMNS = ['latitude', 'longitude']; @@ -19,6 +21,10 @@ final class Version20200106215144 extends AbstractMigration public function up(Schema $schema): void { $visitLocations = $schema->getTable('visit_locations'); + $this->skipIf(none( + self::COLUMNS, + fn (string $oldColName) => $visitLocations->hasColumn($oldColName), + ), 'Old columns do not exist'); foreach (self::COLUMNS as $colName) { $visitLocations->dropColumn($colName); diff --git a/data/migrations/Version20200110182849.php b/data/migrations/Version20200110182849.php new file mode 100644 index 00000000..16b858f9 --- /dev/null +++ b/data/migrations/Version20200110182849.php @@ -0,0 +1,53 @@ + [ + 'referer', + 'user_agent', + ], + 'visit_locations' => [ + 'timezone', + 'country_code', + 'country_name', + 'region_name', + 'city_name', + ], + ]; + + public function up(Schema $schema): void + { + each( + self::COLUMN_DEFAULTS_MAP, + fn (array $columns, string $tableName) => + each($columns, partial_left([$this, 'setDefaultValueForColumnInTable'], $tableName)), + ); + } + + public function setDefaultValueForColumnInTable(string $tableName, string $columnName): void + { + $qb = $this->connection->createQueryBuilder(); + $qb->update($tableName) + ->set($columnName, ':emptyValue') + ->setParameter('emptyValue', self::DEFAULT_EMPTY_VALUE) + ->where($qb->expr()->isNull($columnName)) + ->execute(); + } + + public function down(Schema $schema): void + { + // No need (and no way) to undo this migration + } +} diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index a48a93fa..2e14bb3f 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -29,9 +29,9 @@ class ShortUrl extends AbstractEntity private Collection $visits; /** @var Collection|Tag[] */ private Collection $tags; - private ?Chronos $validSince; - private ?Chronos $validUntil; - private ?int $maxVisits; + private ?Chronos $validSince = null; + private ?Chronos $validUntil = null; + private ?int $maxVisits = null; private ?Domain $domain; private bool $customSlugWasProvided; diff --git a/module/Core/src/Entity/Visit.php b/module/Core/src/Entity/Visit.php index 8ada8176..d278ed6a 100644 --- a/module/Core/src/Entity/Visit.php +++ b/module/Core/src/Entity/Visit.php @@ -15,10 +15,10 @@ use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface; class Visit extends AbstractEntity implements JsonSerializable { - private string $referer = ''; + private string $referer; private Chronos $date; private ?string $remoteAddr = null; - private string $userAgent = ''; + private string $userAgent; private ShortUrl $shortUrl; private ?VisitLocation $visitLocation = null;