diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Domain.Entity.Domain.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Domain.Entity.Domain.php index e02d1f7e..ad77476a 100644 --- a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Domain.Entity.Domain.php +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Domain.Entity.Domain.php @@ -28,15 +28,18 @@ return static function (ClassMetadata $metadata, array $emConfig): void { fieldWithUtf8Charset($builder->createField('baseUrlRedirect', Types::TEXT), $emConfig) ->columnName('base_url_redirect') ->nullable() + ->length(2048) ->build(); fieldWithUtf8Charset($builder->createField('regular404Redirect', Types::TEXT), $emConfig) ->columnName('regular_not_found_redirect') ->nullable() + ->length(2048) ->build(); fieldWithUtf8Charset($builder->createField('invalidShortUrlRedirect', Types::TEXT), $emConfig) ->columnName('invalid_short_url_redirect') ->nullable() + ->length(2048) ->build(); }; diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.RedirectRule.Entity.RedirectCondition.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.RedirectRule.Entity.RedirectCondition.php new file mode 100644 index 00000000..2c1e1bdc --- /dev/null +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.RedirectRule.Entity.RedirectCondition.php @@ -0,0 +1,50 @@ +setTable(determineTableName('redirect_conditions', $emConfig)); + + $builder->createField('id', Types::BIGINT) + ->columnName('id') + ->makePrimaryKey() + ->generatedValue('IDENTITY') + ->option('unsigned', true) + ->build(); + + fieldWithUtf8Charset($builder->createField('name', Types::STRING), $emConfig) + ->columnName('name') + ->length(512) + ->build(); + + $builder->addUniqueConstraint(['name'], 'UQ_name'); + + (new FieldBuilder($builder, [ + 'fieldName' => 'type', + 'type' => Types::STRING, + 'enumType' => RedirectConditionType::class, + ]))->columnName('type') + ->length(255) + ->build(); + + fieldWithUtf8Charset($builder->createField('matchKey', Types::STRING), $emConfig) + ->columnName('match_key') + ->length(512) + ->nullable() + ->build(); + + fieldWithUtf8Charset($builder->createField('matchValue', Types::STRING), $emConfig) + ->columnName('match_value') + ->length(512) + ->build(); +}; diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.RedirectRule.Entity.ShortUrlRedirectRule.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.RedirectRule.Entity.ShortUrlRedirectRule.php new file mode 100644 index 00000000..cab72e89 --- /dev/null +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.RedirectRule.Entity.ShortUrlRedirectRule.php @@ -0,0 +1,42 @@ +setTable(determineTableName('short_url_redirect_rules', $emConfig)); + + $builder->createField('id', Types::BIGINT) + ->columnName('id') + ->makePrimaryKey() + ->generatedValue('IDENTITY') + ->option('unsigned', true) + ->build(); + + $builder->createField('priority', Types::INTEGER) + ->columnName('priority') + ->build(); + + fieldWithUtf8Charset($builder->createField('longUrl', Types::TEXT), $emConfig) + ->columnName('long_url') + ->length(2048) + ->build(); + + $builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class) + ->addJoinColumn('short_url_id', 'id', nullable: false, onDelete: 'CASCADE') + ->build(); + + $builder->createManyToMany('conditions', RedirectRule\Entity\RedirectCondition::class) + ->setJoinTable(determineTableName('redirect_conditions_in_short_url_redirect_rules', $emConfig)) + ->addInverseJoinColumn('redirect_condition_id', 'id', onDelete: 'CASCADE') + ->addJoinColumn('short_url_redirect_rule_id', 'id', onDelete: 'CASCADE') + ->fetchEager() // Always fetch the corresponding conditions when loading a rule + ->build(); +}; diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.DeviceLongUrl.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.DeviceLongUrl.php index 3620ea54..1e84a292 100644 --- a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.DeviceLongUrl.php +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.DeviceLongUrl.php @@ -32,9 +32,10 @@ return static function (ClassMetadata $metadata, array $emConfig): void { fieldWithUtf8Charset($builder->createField('longUrl', Types::TEXT), $emConfig) ->columnName('long_url') + ->length(2048) ->build(); $builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class) - ->addJoinColumn('short_url_id', 'id', false, false, 'CASCADE') + ->addJoinColumn('short_url_id', 'id', nullable: false, onDelete: 'CASCADE') ->build(); }; diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.ShortUrl.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.ShortUrl.php index f33530ec..ff933b8c 100644 --- a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.ShortUrl.php +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.ShortUrl.Entity.ShortUrl.php @@ -25,6 +25,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void { fieldWithUtf8Charset($builder->createField('longUrl', Types::TEXT), $emConfig) ->columnName('original_url') // Rename to long_url some day? ¯\_(ツ)_/¯ + ->length(2048) ->build(); fieldWithUtf8Charset($builder->createField('shortCode', Types::STRING), $emConfig, 'bin') @@ -75,18 +76,18 @@ return static function (ClassMetadata $metadata, array $emConfig): void { $builder->createManyToMany('tags', Tag\Entity\Tag::class) ->setJoinTable(determineTableName('short_urls_in_tags', $emConfig)) - ->addInverseJoinColumn('tag_id', 'id', true, false, 'CASCADE') - ->addJoinColumn('short_url_id', 'id', true, false, 'CASCADE') + ->addInverseJoinColumn('tag_id', 'id', onDelete: 'CASCADE') + ->addJoinColumn('short_url_id', 'id', onDelete: 'CASCADE') ->setOrderBy(['name' => 'ASC']) ->build(); $builder->createManyToOne('domain', Domain\Entity\Domain::class) - ->addJoinColumn('domain_id', 'id', true, false, 'RESTRICT') + ->addJoinColumn('domain_id', 'id', onDelete: 'RESTRICT') ->cascadePersist() ->build(); $builder->createManyToOne('authorApiKey', ApiKey::class) - ->addJoinColumn('author_api_key_id', 'id', true, false, 'SET NULL') + ->addJoinColumn('author_api_key_id', 'id', onDelete: 'SET NULL') ->build(); $builder->addUniqueConstraint(['short_code', 'domain_id'], 'unique_short_code_plus_domain'); diff --git a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Visit.Entity.Visit.php b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Visit.Entity.Visit.php index 28adea80..7d402384 100644 --- a/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Visit.Entity.Visit.php +++ b/module/Core/config/entities-mappings/Shlinkio.Shlink.Core.Visit.Entity.Visit.php @@ -49,11 +49,11 @@ return static function (ClassMetadata $metadata, array $emConfig): void { ->build(); $builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class) - ->addJoinColumn('short_url_id', 'id', true, false, 'CASCADE') + ->addJoinColumn('short_url_id', 'id', onDelete: 'CASCADE') ->build(); $builder->createManyToOne('visitLocation', Visit\Entity\VisitLocation::class) - ->addJoinColumn('visit_location_id', 'id', true, false, 'Set NULL') + ->addJoinColumn('visit_location_id', 'id', onDelete: 'Set NULL') ->cascadePersist() ->build(); diff --git a/module/Core/migrations/Version20240224115725.php b/module/Core/migrations/Version20240224115725.php new file mode 100644 index 00000000..2b68174c --- /dev/null +++ b/module/Core/migrations/Version20240224115725.php @@ -0,0 +1,94 @@ +skipIf($schema->hasTable('short_url_redirect_rules'), 'New columns already exist'); + + $redirectRules = $this->createTableWithId($schema, 'short_url_redirect_rules'); + $redirectRules->addColumn('priority', Types::INTEGER, ['unsigned' => true, 'default' => 1]); + // The length here is just so that Doctrine knows it should not use too small text types + $redirectRules->addColumn('long_url', Types::TEXT, ['length' => 2048]); + + $redirectRules->addColumn('short_url_id', Types::BIGINT, [ + 'unsigned' => true, + 'notnull' => true, + ]); + $redirectRules->addForeignKeyConstraint('short_urls', ['short_url_id'], ['id'], [ + 'onDelete' => 'CASCADE', + 'onUpdate' => 'RESTRICT', + ]); + + $redirectConditions = $this->createTableWithId($schema, 'redirect_conditions'); + $redirectConditions->addColumn('name', Types::STRING, ['length' => 512]); + $redirectConditions->addUniqueIndex(['name'], 'UQ_name'); + + $redirectConditions->addColumn('type', Types::STRING, ['length' => 255]); + $redirectConditions->addColumn('match_key', Types::STRING, [ + 'length' => 512, + 'notnull' => false, + 'default' => null, + ]); + $redirectConditions->addColumn('match_value', Types::STRING, ['length' => 512]); + + $joinTable = $schema->createTable('redirect_conditions_in_short_url_redirect_rules'); + + $joinTable->addColumn('redirect_condition_id', Types::BIGINT, [ + 'unsigned' => true, + 'notnull' => true, + ]); + $joinTable->addForeignKeyConstraint('redirect_conditions', ['redirect_condition_id'], ['id'], [ + 'onDelete' => 'CASCADE', + 'onUpdate' => 'RESTRICT', + ]); + + $joinTable->addColumn('short_url_redirect_rule_id', Types::BIGINT, [ + 'unsigned' => true, + 'notnull' => true, + ]); + $joinTable->addForeignKeyConstraint('short_url_redirect_rules', ['short_url_redirect_rule_id'], ['id'], [ + 'onDelete' => 'CASCADE', + 'onUpdate' => 'RESTRICT', + ]); + + $joinTable->setPrimaryKey(['redirect_condition_id', 'short_url_redirect_rule_id']); + } + + private function createTableWithId(Schema $schema, string $tableName): Table + { + $table = $schema->createTable($tableName); + $table->addColumn('id', Types::BIGINT, [ + 'unsigned' => true, + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->setPrimaryKey(['id']); + + return $table; + } + + public function down(Schema $schema): void + { + $this->skipIf(! $schema->hasTable('short_url_redirect_rules'), 'Columns do not exist'); + + $schema->dropTable('redirect_conditions_in_short_url_redirect_rules'); + $schema->dropTable('short_url_redirect_rules'); + $schema->dropTable('redirect_conditions'); + } + + public function isTransactional(): bool + { + return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform); + } +} diff --git a/module/Core/src/RedirectRule/Entity/RedirectCondition.php b/module/Core/src/RedirectRule/Entity/RedirectCondition.php new file mode 100644 index 00000000..367d96d9 --- /dev/null +++ b/module/Core/src/RedirectRule/Entity/RedirectCondition.php @@ -0,0 +1,17 @@ + $conditions + */ + public function __construct( + private readonly ShortUrl $shortUrl, // No need to read this field. It's used by doctrine + public readonly int $priority, + public readonly string $longUrl, + public readonly Collection $conditions = new ArrayCollection(), + ) { + } +} diff --git a/module/Core/src/RedirectRule/Model/RedirectConditionType.php b/module/Core/src/RedirectRule/Model/RedirectConditionType.php new file mode 100644 index 00000000..6e6b8113 --- /dev/null +++ b/module/Core/src/RedirectRule/Model/RedirectConditionType.php @@ -0,0 +1,10 @@ +