Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/acp 3507/acp 3746 persist webhooks and allow to run them later #7

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0481050
ACP-3746 Added persistence for WebhookRequests.
stereomon Aug 21, 2024
69dac60
ACP-3764 Added strict flag to transfer definition.
stereomon Aug 21, 2024
048da60
ACP-3746 Only delete webhook inbox entity when webhook is successful.
stereomon Aug 21, 2024
369ad69
Merge branch 'feature/acp-3507/acp-3746-persist-webhooks-and-allow-to…
stereomon Aug 21, 2024
1e62e66
ACP-3746 Removed log file.
stereomon Aug 22, 2024
bdfc991
ACP-3746 Added log file to ignore.
stereomon Aug 22, 2024
68679c1
ACP-3746 Cache file.
stereomon Aug 22, 2024
153dcf3
ACP-3746 Added cache to ignore.
stereomon Aug 22, 2024
35163b3
ACP-3507 SprykerCV fixes.
stereomon Sep 11, 2024
3021698
ACP-3507 CI Fixes.
stereomon Sep 11, 2024
265c54c
ACP-3507 Applied CR fixes.
stereomon Sep 11, 2024
abd8277
ACP-3636 Added docs.
stereomon Sep 11, 2024
c457da4
ACP-3507 Added logger.
stereomon Sep 16, 2024
f7a6dc1
ACP-3839 Added Facade method to be able to delete persisted webhooks.
stereomon Sep 17, 2024
f017410
ACP-3507 Fixes after CR.
stereomon Sep 18, 2024
bf228d4
ACP-3507 Removed commented line.
stereomon Sep 25, 2024
bd21b6a
ACP-3507 Updated min PHP Version.
stereomon Sep 25, 2024
57cfacd
ACP-3507 Updated batch in README.md
stereomon Sep 26, 2024
56fe060
Update README.md
matweew Sep 30, 2024
358ed7c
ACP-3507 Updated README.md
stereomon Sep 30, 2024
8cf6ed2
Merge branch 'feature/acp-3507/acp-3746-persist-webhooks-and-allow-to…
stereomon Sep 30, 2024
65afb78
Merge branch 'feature/acp-3507/acp-3636-add-update-documentations' of…
stereomon Sep 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ jobs:
- name: Composer install
run: composer install --optimize-autoloader

- name: Setup Database
run: |
mkdir -p tests/_data
touch tests/_data/app_webhook_db

- name: Setup AppWebhook
run: composer setup

Expand Down
1 change: 1 addition & 0 deletions codeception.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace: SprykerTest

include:
- tests/SprykerTest/Glue/AppWebhookBackendApi/
- tests/SprykerTest/Zed/AppWebhook/

bootstrap: _data/Environment.php

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"scripts": {
"cs-check": "phpcs -p src/ tests/",
"cs-fix": "phpcbf -p src/ tests/",
"setup": "tests/bin/console transfer:generate && tests/bin/console transfer:databuilder:generate && tests/bin/console dev:ide-auto-completion:zed:generate && tests/bin/console dev:ide-auto-completion:glue:generate",
"setup": "tests/bin/console app-webhook:setup && tests/bin/console transfer:generate && tests/bin/console transfer:databuilder:generate && tests/bin/console dev:ide-auto-completion:zed:generate && tests/bin/console dev:ide-auto-completion:glue:generate && tests/bin/console propel:install && vendor/bin/codecept build",
"stan": "phpstan analyze src/Spryker/",
"test": "codecept build && codecept run",
"test-strict": "vendor/bin/infection --threads=max --min-msi=100 --min-covered-msi=100",
Expand Down
55 changes: 55 additions & 0 deletions config/Shared/config_default.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,61 @@
use Spryker\Shared\ErrorHandler\ErrorHandlerConstants;
use Spryker\Shared\Kernel\KernelConstants;
use Spryker\Shared\Log\LogConstants;
use Spryker\Shared\Propel\PropelConstants;
use Spryker\Zed\PropelOrm\Business\Builder\ExtensionObjectBuilder;
use Spryker\Zed\PropelOrm\Business\Builder\ExtensionQueryBuilder;
use Spryker\Zed\PropelOrm\Business\Builder\ObjectBuilder;
use Spryker\Zed\PropelOrm\Business\Builder\QueryBuilder;

$connections = [
'mysql' => [
'adapter' => 'sqlite',
'dsn' => 'sqlite:tests/_data/app_webhook_db',
'user' => '',
'password' => '',
'settings' => [],
],
];

$config[PropelConstants::PROPEL] = [
'database' => [
'connections' => [],
],
'runtime' => [
'defaultConnection' => 'default',
'connections' => ['default', 'zed'],
],
'generator' => [
'defaultConnection' => 'default',
'connections' => ['default', 'zed'],
'objectModel' => [
'defaultKeyType' => 'fieldName',
'builders' => [
// If you need full entity logging on Create/Update/Delete, then switch to
// Spryker\Zed\PropelOrm\Business\Builder\ObjectBuilderWithLogger instead.
'object' => ObjectBuilder::class,
'objectstub' => ExtensionObjectBuilder::class,
'query' => QueryBuilder::class,
'querystub' => ExtensionQueryBuilder::class,
],
],
],
'paths' => [
'phpDir' => APPLICATION_ROOT_DIR,
'sqlDir' => APPLICATION_SOURCE_DIR . '/Orm/Propel/Sql/',
'migrationDir' => APPLICATION_SOURCE_DIR . '/Orm/Propel/Migration_SQLite/',
'schemaDir' => APPLICATION_SOURCE_DIR . '/Orm/Propel/Schema/',
],
];

$config[PropelConstants::ZED_DB_ENGINE] = 'mysql';
$config[PropelConstants::ZED_DB_HOST] = 'localhost';
$config[PropelConstants::ZED_DB_PORT] = 1234;
$config[PropelConstants::ZED_DB_USERNAME] = 'catface';
$config[PropelConstants::ZED_DB_PASSWORD] = 'catface';

$config[PropelConstants::PROPEL]['database']['connections']['default'] = $connections['mysql'];
$config[PropelConstants::PROPEL]['database']['connections']['zed'] = $connections['mysql'];

$config[KernelConstants::PROJECT_NAMESPACE] = 'Spryker';
$config[KernelConstants::PROJECT_NAMESPACES] = ['Spryker'];
Expand Down
7 changes: 7 additions & 0 deletions data/DE/logs/ZED/propel.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[2024-08-21T07:57:52.760961+00:00] defaultLogger.ERROR: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: spy_webhook_inbox.identifier, spy_webhook_inbox.sequence_number [] []
[2024-08-21T08:01:33.888727+00:00] defaultLogger.ERROR: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: spy_webhook_inbox.identifier, spy_webhook_inbox.sequence_number [] []
[2024-08-21T08:02:25.094834+00:00] defaultLogger.ERROR: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: spy_webhook_inbox.identifier, spy_webhook_inbox.sequence_number [] []
[2024-08-21T08:03:20.923046+00:00] defaultLogger.ERROR: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: spy_webhook_inbox.identifier, spy_webhook_inbox.sequence_number [] []
[2024-08-21T08:04:39.624414+00:00] defaultLogger.ERROR: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: spy_webhook_inbox.identifier, spy_webhook_inbox.sequence_number [] []
[2024-08-21T08:05:22.702889+00:00] defaultLogger.ERROR: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: spy_webhook_inbox.identifier, spy_webhook_inbox.sequence_number [] []
[2024-08-21T08:10:00.680570+00:00] defaultLogger.ERROR: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: spy_webhook_inbox.identifier, spy_webhook_inbox.sequence_number [] []
stereomon marked this conversation as resolved.
Show resolved Hide resolved
25 changes: 25 additions & 0 deletions data/cache/propel/generated-conf/loadDatabase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
$serviceContainer = \Propel\Runtime\Propel::getServiceContainer();
$serviceContainer->initDatabaseMapFromDumps(array (
stereomon marked this conversation as resolved.
Show resolved Hide resolved
'zed' =>
array (
'tablesByName' =>
array (
'spy_app_config' => '\\Orm\\Zed\\AppKernel\\Persistence\\Map\\SpyAppConfigTableMap',
'spy_locale' => '\\Orm\\Zed\\Locale\\Persistence\\Map\\SpyLocaleTableMap',
'spy_locale_store' => '\\Orm\\Zed\\Locale\\Persistence\\Map\\SpyLocaleStoreTableMap',
'spy_queue_process' => '\\Orm\\Zed\\Queue\\Persistence\\Map\\SpyQueueProcessTableMap',
'spy_store' => '\\Orm\\Zed\\Store\\Persistence\\Map\\SpyStoreTableMap',
'spy_webhook_inbox' => '\\Orm\\Zed\\AppWebhook\\Persistence\\Map\\SpyWebhookInboxTableMap',
),
'tablesByPhpName' =>
array (
'\\SpyAppConfig' => '\\Orm\\Zed\\AppKernel\\Persistence\\Map\\SpyAppConfigTableMap',
'\\SpyLocale' => '\\Orm\\Zed\\Locale\\Persistence\\Map\\SpyLocaleTableMap',
'\\SpyLocaleStore' => '\\Orm\\Zed\\Locale\\Persistence\\Map\\SpyLocaleStoreTableMap',
'\\SpyQueueProcess' => '\\Orm\\Zed\\Queue\\Persistence\\Map\\SpyQueueProcessTableMap',
'\\SpyStore' => '\\Orm\\Zed\\Store\\Persistence\\Map\\SpyStoreTableMap',
'\\SpyWebhookInbox' => '\\Orm\\Zed\\AppWebhook\\Persistence\\Map\\SpyWebhookInboxTableMap',
),
),
));
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@
<transfer name="WebhookRequest" strict="true">
<property name="content" type="string"/>
<property name="mode" type="string"/>
<property name="identifier" type="string" description="This is the default identifier set for internal processing."/>
<property name="isRetry" type="bool" description="When this WebhookRequest was tried to be processed before we set this to not persist it again in the database."/>
</transfer>

<transfer name="WebhookResponse" strict="true">
<property name="isSuccessful" type="bool"/>
<property name="isHandled" type="bool" description="A Webhook can be accepted for later processing and setting this to false will keep it persisted. When true or null it will be removed from persistence."/>
<property name="identifier" type="string" description="The identifier will be set from outside and used to find previously unprocessed webhooks and execute them when possible."/>
<property name="sequenceNumber" type="int" description="This number will be set from outside. When there were already unprocessed webhooks for the same identifier this number will be increased."/>
<property name="content" type="string"/>
<property name="message" type="string"/>
</transfer>

<transfer name="WebhookInboxCriteria" strict="true">
<property name="identifier" type="array" singular="identifier"/>
pushokwhite marked this conversation as resolved.
Show resolved Hide resolved
</transfer>

<transfer name="GlueError">
<property name="message" type="string"/>
</transfer>
Expand Down
22 changes: 22 additions & 0 deletions src/Spryker/Zed/AppWebhook/Business/AppWebhookBusinessFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,40 @@
namespace Spryker\Zed\AppWebhook\Business;

use Spryker\Zed\AppWebhook\AppWebhookDependencyProvider;
use Spryker\Zed\AppWebhook\Business\Identifier\IdentifierBuilder;
use Spryker\Zed\AppWebhook\Business\Identifier\IdentifierBuilderInterface;
use Spryker\Zed\AppWebhook\Business\WebhookHandler\WebhookHandler;
use Spryker\Zed\AppWebhook\Business\WebhookProcessor\WebhookProcessor;
use Spryker\Zed\Kernel\Business\AbstractBusinessFactory;

/**
* @method \Spryker\Zed\AppWebhook\Persistence\AppWebhookEntityManagerInterface getEntityManager()
* @method \Spryker\Zed\AppWebhook\Persistence\AppWebhookRepositoryInterface getRepository()
*/
class AppWebhookBusinessFactory extends AbstractBusinessFactory
{
public function createWebhookHandler(): WebhookHandler
{
return new WebhookHandler(
$this->getAppWebhookHandlerPlugins(),
$this->getEntityManager(),
$this->createIdentifierBuilder(),
);
}

public function createWebhookProcessor(): WebhookProcessor
{
return new WebhookProcessor(
$this->createWebhookHandler(),
$this->getRepository(),
);
}

public function createIdentifierBuilder(): IdentifierBuilderInterface
{
return new IdentifierBuilder();
}

/**
* @return array<\Spryker\Zed\AppWebhook\Dependency\Plugin\WebhookHandlerPluginInterface>
*/
Expand Down
13 changes: 13 additions & 0 deletions src/Spryker/Zed/AppWebhook/Business/AppWebhookFacade.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

namespace Spryker\Zed\AppWebhook\Business;

use Generated\Shared\Transfer\WebhookInboxCriteriaTransfer;
use Generated\Shared\Transfer\WebhookRequestTransfer;
use Generated\Shared\Transfer\WebhookResponseTransfer;
use Spryker\Zed\Kernel\Business\AbstractFacade;

/**
* @method \Spryker\Zed\AppWebhook\Business\AppWebhookBusinessFactory getFactory()
* @method \Spryker\Zed\AppWebhook\Persistence\AppWebhookEntityManagerInterface getEntityManager()
* @method \Spryker\Zed\AppWebhook\Persistence\AppWebhookRepositoryInterface getRepository()
*/
class AppWebhookFacade extends AbstractFacade implements AppWebhookFacadeInterface
{
Expand All @@ -25,4 +28,14 @@ public function handleWebhook(WebhookRequestTransfer $webhookRequestTransfer, We
{
return $this->getFactory()->createWebhookHandler()->handleWebhook($webhookRequestTransfer, $webhookResponseTransfer);
}

/**
* @api
*
* @inheritDoc
*/
public function processUnprocessedWebhooks(WebhookInboxCriteriaTransfer $webhookInboxCriteriaTransfer): void
{
$this->getFactory()->createWebhookProcessor()->processUnprocessedWebhooks($webhookInboxCriteriaTransfer);
}
}
12 changes: 12 additions & 0 deletions src/Spryker/Zed/AppWebhook/Business/AppWebhookFacadeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Spryker\Zed\AppWebhook\Business;

use Generated\Shared\Transfer\WebhookInboxCriteriaTransfer;
use Generated\Shared\Transfer\WebhookRequestTransfer;
use Generated\Shared\Transfer\WebhookResponseTransfer;

Expand All @@ -24,4 +25,15 @@ interface AppWebhookFacadeInterface
* @api
*/
public function handleWebhook(WebhookRequestTransfer $webhookRequestTransfer, WebhookResponseTransfer $webhookResponseTransfer): WebhookResponseTransfer;

/**
* Specification:
* - Process unhandled webhook requests.
* - Loads all unprocessed webhooks from the inbox.
* - Converts entities to transfers.
* - Runs the WebhookHandler for all request transfers.
*
* @api
*/
public function processUnprocessedWebhooks(WebhookInboxCriteriaTransfer $webhookInboxCriteriaTransfer): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

/**
* Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
* Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
*/

namespace Spryker\Zed\AppWebhook\Business\Identifier;

use Ramsey\Uuid\Uuid;

class IdentifierBuilder implements IdentifierBuilderInterface
{
public function getIdentifier(): string
{
return Uuid::uuid4()->toString();
pushokwhite marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

/**
* Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
* Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
*/

namespace Spryker\Zed\AppWebhook\Business\Identifier;

interface IdentifierBuilderInterface
{
public function getIdentifier(): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,62 @@

use Generated\Shared\Transfer\WebhookRequestTransfer;
use Generated\Shared\Transfer\WebhookResponseTransfer;
use Spryker\Zed\AppWebhook\Business\Identifier\IdentifierBuilderInterface;
use Spryker\Zed\AppWebhook\Persistence\AppWebhookEntityManagerInterface;
use Throwable;

class WebhookHandler
{
/**
* @param array<\Spryker\Zed\AppWebhook\Dependency\Plugin\WebhookHandlerPluginInterface> $webhookHandlerPlugins
*/
public function __construct(protected array $webhookHandlerPlugins)
{
public function __construct(
protected array $webhookHandlerPlugins,
protected AppWebhookEntityManagerInterface $appWebhookEntityManager,
protected IdentifierBuilderInterface $identifierBuilder
) {
}

public function handleWebhook(WebhookRequestTransfer $webhookRequestTransfer, WebhookResponseTransfer $webhookResponseTransfer): WebhookResponseTransfer
{
foreach ($this->webhookHandlerPlugins as $webhookHandlerPlugin) {
if (!$webhookHandlerPlugin->canHandle($webhookRequestTransfer)) {
continue;
}
// get the default identifier which will be only used when the webhook is not a retry
$identifier = $this->identifierBuilder->getIdentifier();

$webhookResponseTransfer = $webhookHandlerPlugin->handleWebhook($webhookRequestTransfer, $webhookResponseTransfer);
// Only persist the webhook if it's not a retry.
if ($webhookRequestTransfer->getIsRetry() !== true) {
pushokwhite marked this conversation as resolved.
Show resolved Hide resolved
$webhookRequestTransfer->setIdentifier($identifier);
$this->appWebhookEntityManager->saveWebhookRequest($webhookRequestTransfer);
}
stereomon marked this conversation as resolved.
Show resolved Hide resolved

if ($webhookResponseTransfer->getIsSuccessful() === null) {
try {
foreach ($this->webhookHandlerPlugins as $webhookHandlerPlugin) {
if (!$webhookHandlerPlugin->canHandle($webhookRequestTransfer)) {
continue;
}

$webhookResponseTransfer = $webhookHandlerPlugin->handleWebhook($webhookRequestTransfer, $webhookResponseTransfer);
}

if ($webhookResponseTransfer->getIsSuccessful() === null) {
$webhookResponseTransfer
->setIsSuccessful(false)
->setMessage(sprintf('The webhook was not handled by any of the registered plugins. WebhookRequestTransfer: %s', json_encode($webhookRequestTransfer->toArray())));
stereomon marked this conversation as resolved.
Show resolved Hide resolved
}

if ($webhookResponseTransfer->getIsHandled() === false) {
$this->appWebhookEntityManager->updateWebhookRequest($webhookRequestTransfer, $webhookResponseTransfer);
}

// Using packages may not set or use the isHandled at all (null by default), so we need to check for null explicitly.
if ($webhookResponseTransfer->getIsSuccessful() === true && ($webhookResponseTransfer->getIsHandled() === true || $webhookResponseTransfer->getIsHandled() === null)) {
$this->appWebhookEntityManager->deleteWebhookRequest($webhookRequestTransfer);
}
} catch (Throwable $throwable) {
$webhookResponseTransfer
->setIsSuccessful(false)
->setMessage(sprintf('The webhook was not handled by any of the registered plugins. WebhookRequestTransfer: %s', json_encode($webhookRequestTransfer->toArray())));
->setMessage($throwable->getMessage());

$this->appWebhookEntityManager->updateWebhookRequest($webhookRequestTransfer, $webhookResponseTransfer);
}

return $webhookResponseTransfer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/**
* Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
* Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
*/

namespace Spryker\Zed\AppWebhook\Business\WebhookProcessor;

use Generated\Shared\Transfer\WebhookInboxCriteriaTransfer;
use Generated\Shared\Transfer\WebhookResponseTransfer;
use Spryker\Zed\AppWebhook\Business\WebhookHandler\WebhookHandler;
use Spryker\Zed\AppWebhook\Persistence\AppWebhookRepositoryInterface;

class WebhookProcessor
{
public function __construct(protected WebhookHandler $webhookHandler, protected AppWebhookRepositoryInterface $appWebhookRepository)
{
}

public function processUnprocessedWebhooks(WebhookInboxCriteriaTransfer $webhookInboxCriteriaTransfer): void
{
$webhookRequestTransfers = $this->appWebhookRepository->getUnprocessedWebhookRequests($webhookInboxCriteriaTransfer);

foreach ($webhookRequestTransfers as $webhookRequestTransfer) {
$webhookRequestTransfer->setIsRetry(true);
$this->webhookHandler->handleWebhook($webhookRequestTransfer, new WebhookResponseTransfer());
}
}
}
Loading