Rework the webhooks table, allow to update exists webhooks

This commit is contained in:
ErickSkrauch 2020-06-14 01:20:31 +03:00
parent 17f1794a4e
commit fb452901b8
10 changed files with 131 additions and 104 deletions

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace common\models; namespace common\models;
use yii\behaviors\TimestampBehavior; use yii\behaviors\TimestampBehavior;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveRecord; use yii\db\ActiveRecord;
/** /**
@ -12,18 +11,16 @@ use yii\db\ActiveRecord;
* @property int $id * @property int $id
* @property string $url * @property string $url
* @property string|null $secret * @property string|null $secret
* @property string[] $events
* @property int $created_at * @property int $created_at
* *
* Relations:
* @property WebHookEvent[] $events
*
* Behaviors: * Behaviors:
* @mixin TimestampBehavior * @mixin TimestampBehavior
*/ */
class WebHook extends ActiveRecord { class WebHook extends ActiveRecord {
public static function tableName(): string { public static function tableName(): string {
return '{{%webhooks}}'; return 'webhooks';
} }
public function behaviors(): array { public function behaviors(): array {
@ -35,8 +32,4 @@ class WebHook extends ActiveRecord {
]; ];
} }
public function getEvents(): ActiveQueryInterface {
return $this->hasMany(WebHookEvent::class, ['webhook_id' => 'id']);
}
} }

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace common\models;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveRecord;
/**
* Fields:
* @property int $webhook_id
* @property string $event_type
*
* Relations:
* @property WebHook $webhook
*/
class WebHookEvent extends ActiveRecord {
public static function tableName(): string {
return '{{%webhooks_events}}';
}
public function getWebhook(): ActiveQueryInterface {
return $this->hasOne(WebHook::class, ['id' => 'webhook_id']);
}
}

View File

@ -6,6 +6,7 @@ namespace common\tasks;
use common\models\Account; use common\models\Account;
use common\models\WebHook; use common\models\WebHook;
use Yii; use Yii;
use yii\db\Expression;
use yii\queue\RetryableJobInterface; use yii\queue\RetryableJobInterface;
final class CreateWebHooksDeliveries implements RetryableJobInterface { final class CreateWebHooksDeliveries implements RetryableJobInterface {
@ -67,8 +68,9 @@ final class CreateWebHooksDeliveries implements RetryableJobInterface {
public function execute($queue): void { public function execute($queue): void {
/** @var WebHook[] $targets */ /** @var WebHook[] $targets */
$targets = WebHook::find() $targets = WebHook::find()
->joinWith('events e', false) // It's very important to use exactly single quote to begin the string
->andWhere(['e.event_type' => $this->type]) // and double quote to specify the string value
->andWhere(new Expression("JSON_CONTAINS(`events`, '\"{$this->type}\"')"))
->all(); ->all();
foreach ($targets as $target) { foreach ($targets as $target) {
$job = new DeliveryWebHook(); $job = new DeliveryWebHook();

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace common\tests\fixtures;
use common\models\WebHookEvent;
use yii\test\ActiveFixture;
class WebHooksEventsFixture extends ActiveFixture {
public $modelClass = WebHookEvent::class;
public $dataFile = '@root/common/tests/fixtures/data/webhooks-events.php';
public $depends = [
WebHooksFixture::class,
];
}

View File

@ -1,11 +0,0 @@
<?php
return [
[
'webhook_id' => 1,
'event_type' => 'account.edit',
],
[
'webhook_id' => 2,
'event_type' => 'account.edit',
],
];

View File

@ -4,18 +4,21 @@ return [
'id' => 1, 'id' => 1,
'url' => 'http://localhost:80/webhooks/ely', 'url' => 'http://localhost:80/webhooks/ely',
'secret' => 'my-secret', 'secret' => 'my-secret',
'events' => ['account.edit'],
'created_at' => 1531054333, 'created_at' => 1531054333,
], ],
'webhook-without-secret' => [ 'webhook-without-secret' => [
'id' => 2, 'id' => 2,
'url' => 'http://localhost:81/webhooks/ely', 'url' => 'http://localhost:81/webhooks/ely',
'secret' => null, 'secret' => null,
'events' => ['account.edit'],
'created_at' => 1531054837, 'created_at' => 1531054837,
], ],
'webhook-without-events' => [ 'webhook-without-events' => [
'id' => 3, 'id' => 3,
'url' => 'http://localhost:82/webhooks/ely', 'url' => 'http://localhost:82/webhooks/ely',
'secret' => null, 'secret' => null,
'events' => [],
'created_at' => 1531054990, 'created_at' => 1531054990,
], ],
]; ];

View File

@ -18,7 +18,6 @@ class CreateWebHooksDeliveriesTest extends TestCase {
public function _fixtures(): array { public function _fixtures(): array {
return [ return [
'webhooks' => fixtures\WebHooksFixture::class, 'webhooks' => fixtures\WebHooksFixture::class,
'webhooksEvents' => fixtures\WebHooksEventsFixture::class,
]; ];
} }

View File

@ -7,15 +7,46 @@ use common\models\WebHook;
use console\models\WebHookForm; use console\models\WebHookForm;
use yii\console\Controller; use yii\console\Controller;
use yii\console\ExitCode; use yii\console\ExitCode;
use yii\helpers\Console; use yii\console\widgets\Table;
use yii\helpers\Console as C;
class WebhooksController extends Controller { class WebhooksController extends Controller {
public function actionCreate(): int { public $defaultAction = 'list';
$form = new WebHookForm(new WebHook());
$url = Console::prompt('Enter webhook url:', [ public function actionList(): void {
$rows = [];
/** @var WebHook $webHook */
foreach (WebHook::find()->with('events')->all() as $webHook) {
$rows[] = [$webHook->id, $webHook->url, $webHook->secret, implode(', ', $webHook->events)];
}
echo (new Table([
'headers' => ['id', 'url', 'secret', 'events'],
'rows' => $rows,
]))->run();
}
public function actionCreate(): int {
return $this->runForm(new WebHookForm(new WebHook()));
}
public function actionUpdate(int $id): int {
/** @var WebHook|null $webHook */
$webHook = WebHook::findOne(['id' => $id]);
if ($webHook === null) {
C::error("Entity with id {$id} isn't found.");
return ExitCode::DATAERR;
}
return $this->runForm(new WebHookForm($webHook));
}
private function runForm(WebHookForm $form): int {
C::prompt(C::ansiFormat('Enter webhook url:', [C::FG_GREY]), [
'required' => true, 'required' => true,
'default' => $form->url,
'validator' => function(string $input, ?string &$error) use ($form): bool { 'validator' => function(string $input, ?string &$error) use ($form): bool {
$form->url = $input; $form->url = $input;
if (!$form->validate('url')) { if (!$form->validate('url')) {
@ -26,34 +57,48 @@ class WebhooksController extends Controller {
return true; return true;
}, },
]); ]);
$secret = Console::prompt('Enter webhook secret (empty to no secret):');
$options = $form::getEvents(); $secret = C::prompt(C::ansiFormat('Enter webhook secret (empty to no secret):', [C::FG_GREY]), [
$options[''] = 'Finish input'; // It's needed to allow finish input cycle 'default' => $form->secret,
$events = []; ]);
do {
$availableOptions = array_diff($options, $events);
$eventIndex = Console::select('Choose wanted events (submit no input to finish):', $availableOptions);
if ($eventIndex !== '') {
$events[] = $options[$eventIndex];
}
} while ($eventIndex !== '' || empty($events));
$form->url = $url;
$form->events = $events;
if ($secret !== '') { if ($secret !== '') {
$form->secret = $secret; $form->secret = $secret;
} }
$allEvents = WebHookForm::getEvents();
do {
$options = [];
foreach ($allEvents as $id => $option) {
if (in_array($option, $form->events, true)) {
$options["-{$id}"] = $option; // Cast to string to create "-0" index
} else {
$options[$id] = $option;
}
}
$options[''] = 'Finish input'; // This needed to allow finish input cycle
$eventIndex = C::select(
C::ansiFormat('Choose wanted events (submit no input to finish):', [C::FG_GREY]),
$options,
);
if ($eventIndex === '') {
continue;
}
if ($eventIndex[0] === '-') {
unset($form->events[array_search($options[$eventIndex], $form->events, true)]);
} else {
$form->events[] = $options[$eventIndex];
}
} while ($eventIndex !== '' || empty($form->events));
if (!$form->save()) { if (!$form->save()) {
Console::error('Unable to create new webhook. Check errors list below' . PHP_EOL . Console::errorSummary($form)); C::error('Unable to create new webhook. Check errors list below' . PHP_EOL . C::errorSummary($form));
return ExitCode::UNSPECIFIED_ERROR; return ExitCode::UNSPECIFIED_ERROR;
} }
return ExitCode::OK; return ExitCode::OK;
} }
// TODO: add action to modify the webhook events
} }

View File

@ -0,0 +1,51 @@
<?php
use console\db\Migration;
class m200613_204832_remove_webhooks_events_table extends Migration {
public function safeUp() {
$this->addColumn('webhooks', 'events', $this->json()->toString('events') . ' AFTER `secret`');
$webhooksIds = $this->db->createCommand('SELECT id FROM webhooks')->queryColumn();
foreach ($webhooksIds as $webhookId) {
$events = $this->db->createCommand("SELECT event_type FROM webhooks_events WHERE webhook_id = {$webhookId}")->queryColumn();
if (empty($events)) {
continue;
}
$this->execute('UPDATE webhooks SET events = JSON_ARRAY("' . implode('","', $events) . '")');
}
$this->dropTable('webhooks_events');
}
public function safeDown() {
$this->createTable('webhooks_events', [
'webhook_id' => $this->db->getTableSchema('webhooks')->getColumn('id')->dbType . ' NOT NULL',
'event_type' => $this->string()->notNull(),
$this->primary('webhook_id', 'event_type'),
]);
$this->addForeignKey('FK_webhook_event_to_webhook', 'webhooks_events', 'webhook_id', 'webhooks', 'id', 'CASCADE', 'CASCADE');
$webhooks = $this->db->createCommand('SELECT id, `events` FROM webhooks')->queryAll();
foreach ($webhooks as $webhook) {
if (empty($webhook['events'])) {
continue;
}
$events = json_decode($webhook['events'], true);
if (empty($events)) {
continue;
}
$this->batchInsert(
'webhooks_events',
['webhook_id', 'event_type'],
array_map(fn($event) => [$webhook['id'], $event], $events),
);
}
$this->dropColumn('webhooks', 'events');
}
}

View File

@ -4,9 +4,7 @@ declare(strict_types=1);
namespace console\models; namespace console\models;
use common\models\WebHook; use common\models\WebHook;
use common\models\WebHookEvent;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Yii;
use yii\base\Model; use yii\base\Model;
class WebHookForm extends Model { class WebHookForm extends Model {
@ -22,6 +20,9 @@ class WebHookForm extends Model {
public function __construct(WebHook $webHook, array $config = []) { public function __construct(WebHook $webHook, array $config = []) {
parent::__construct($config); parent::__construct($config);
$this->webHook = $webHook; $this->webHook = $webHook;
$this->url = $webHook->url;
$this->secret = $webHook->secret;
$this->events = (array)$webHook->events;
} }
public function rules(): array { public function rules(): array {
@ -38,22 +39,12 @@ class WebHookForm extends Model {
return false; return false;
} }
$transaction = Yii::$app->db->beginTransaction();
$webHook = $this->webHook; $webHook = $this->webHook;
$webHook->url = $this->url; $webHook->url = $this->url;
$webHook->secret = $this->secret; $webHook->secret = $this->secret;
$webHook->events = array_values($this->events); // Drop the keys order
Assert::true($webHook->save(), 'Cannot save webhook.'); Assert::true($webHook->save(), 'Cannot save webhook.');
foreach ($this->events as $event) {
$eventModel = new WebHookEvent();
$eventModel->webhook_id = $webHook->id;
$eventModel->event_type = $event;
Assert::true($eventModel->save(), 'Cannot save webhook event.');
}
$transaction->commit();
return true; return true;
} }