Skip to content

Commit 539ae39

Browse files
authored
Merge pull request #185 from utopia-php/add-webhook-migration
Add webhook migration support
2 parents 0d10f71 + 278bf59 commit 539ae39

7 files changed

Lines changed: 258 additions & 0 deletions

File tree

src/Migration/Destinations/Appwrite.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
use Utopia\Migration\Resources\Messaging\Subscriber;
5858
use Utopia\Migration\Resources\Messaging\Topic;
5959
use Utopia\Migration\Resources\Settings\ProjectVariable;
60+
use Utopia\Migration\Resources\Settings\Webhook;
6061
use Utopia\Migration\Resources\Sites\Deployment as SiteDeployment;
6162
use Utopia\Migration\Resources\Sites\EnvVar as SiteEnvVar;
6263
use Utopia\Migration\Resources\Sites\Site;
@@ -278,6 +279,7 @@ public static function getSupportedResources(): array
278279
// Integrations
279280
Resource::TYPE_PLATFORM,
280281
Resource::TYPE_API_KEY,
282+
Resource::TYPE_WEBHOOK,
281283

282284
// Settings
283285
Resource::TYPE_PROJECT_VARIABLE,
@@ -3085,6 +3087,10 @@ public function importIntegrationsResource(Resource $resource): Resource
30853087
/** @var ApiKey $resource */
30863088
$this->createApiKey($resource);
30873089
break;
3090+
case Resource::TYPE_WEBHOOK:
3091+
/** @var Webhook $resource */
3092+
$this->createWebhook($resource);
3093+
break;
30883094
}
30893095

30903096
if ($resource->getStatus() !== Resource::STATUS_SKIPPED) {
@@ -3149,6 +3155,50 @@ protected function createProjectVariable(ProjectVariable $resource): bool
31493155
return true;
31503156
}
31513157

3158+
protected function createWebhook(Webhook $resource): bool
3159+
{
3160+
$existing = $this->dbForPlatform->findOne('webhooks', [
3161+
Query::equal('projectInternalId', [$this->projectInternalId]),
3162+
Query::equal('name', [$resource->getWebhookName()]),
3163+
]);
3164+
3165+
if ($existing !== false && !$existing->isEmpty()) {
3166+
$resource->setStatus(Resource::STATUS_SKIPPED, 'Webhook already exists');
3167+
return false;
3168+
}
3169+
3170+
$createdAt = $this->normalizeDateTime($resource->getCreatedAt());
3171+
$updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);
3172+
3173+
try {
3174+
$this->dbForPlatform->createDocument('webhooks', new UtopiaDocument([
3175+
'$id' => ID::unique(),
3176+
'$permissions' => $resource->getPermissions(),
3177+
'projectInternalId' => $this->projectInternalId,
3178+
'projectId' => $this->project,
3179+
'name' => $resource->getWebhookName(),
3180+
'events' => $resource->getEvents(),
3181+
'url' => $resource->getUrl(),
3182+
'security' => $resource->getSecurity(),
3183+
'httpUser' => $resource->getHttpUser(),
3184+
'httpPass' => $resource->getHttpPass(),
3185+
// SDK only returns the signing secret on creation, never on list — regenerate
3186+
// a fresh one on the destination to match upstream createWebhook behavior.
3187+
'signatureKey' => \bin2hex(\random_bytes(64)),
3188+
'enabled' => $resource->isEnabled(),
3189+
'$createdAt' => $createdAt,
3190+
'$updatedAt' => $updatedAt,
3191+
]));
3192+
} catch (DuplicateException) {
3193+
$resource->setStatus(Resource::STATUS_SKIPPED, 'Webhook already exists');
3194+
return false;
3195+
}
3196+
3197+
$this->dbForPlatform->purgeCachedDocument('projects', $this->project);
3198+
3199+
return true;
3200+
}
3201+
31523202
/**
31533203
* @throws \Throwable
31543204
*/

src/Migration/Resource.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ abstract class Resource implements \JsonSerializable
7777

7878
// Settings
7979
public const TYPE_PROJECT_VARIABLE = 'project-variable';
80+
public const TYPE_WEBHOOK = 'webhook';
8081

8182
// Messaging
8283
public const TYPE_SUBSCRIBER = 'subscriber';
@@ -119,6 +120,7 @@ abstract class Resource implements \JsonSerializable
119120
self::TYPE_PLATFORM,
120121
self::TYPE_API_KEY,
121122
self::TYPE_PROJECT_VARIABLE,
123+
self::TYPE_WEBHOOK,
122124
self::TYPE_PROVIDER,
123125
self::TYPE_TOPIC,
124126
self::TYPE_SUBSCRIBER,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
namespace Utopia\Migration\Resources\Settings;
4+
5+
use Utopia\Migration\Resource;
6+
use Utopia\Migration\Transfer;
7+
8+
class Webhook extends Resource
9+
{
10+
/**
11+
* @param array<string> $events
12+
*/
13+
public function __construct(
14+
string $id,
15+
private readonly string $name,
16+
private readonly string $url,
17+
private readonly array $events = [],
18+
private readonly bool $security = false,
19+
private readonly string $httpUser = '',
20+
private readonly string $httpPass = '',
21+
private readonly bool $enabled = true,
22+
string $createdAt = '',
23+
string $updatedAt = '',
24+
) {
25+
$this->id = $id;
26+
$this->createdAt = $createdAt;
27+
$this->updatedAt = $updatedAt;
28+
}
29+
30+
/**
31+
* @param array<string, mixed> $array
32+
* @return self
33+
*/
34+
public static function fromArray(array $array): self
35+
{
36+
return new self(
37+
$array['id'],
38+
$array['name'],
39+
$array['url'],
40+
$array['events'] ?? [],
41+
(bool) ($array['security'] ?? false),
42+
$array['httpUser'] ?? '',
43+
$array['httpPass'] ?? '',
44+
(bool) ($array['enabled'] ?? true),
45+
createdAt: $array['createdAt'] ?? '',
46+
updatedAt: $array['updatedAt'] ?? '',
47+
);
48+
}
49+
50+
/**
51+
* @return array<string, mixed>
52+
*/
53+
public function jsonSerialize(): array
54+
{
55+
return [
56+
'id' => $this->id,
57+
'name' => $this->name,
58+
'url' => $this->url,
59+
'events' => $this->events,
60+
'security' => $this->security,
61+
'httpUser' => $this->httpUser,
62+
'httpPass' => $this->httpPass,
63+
'enabled' => $this->enabled,
64+
'createdAt' => $this->createdAt,
65+
'updatedAt' => $this->updatedAt,
66+
];
67+
}
68+
69+
public static function getName(): string
70+
{
71+
return Resource::TYPE_WEBHOOK;
72+
}
73+
74+
public function getGroup(): string
75+
{
76+
return Transfer::GROUP_INTEGRATIONS;
77+
}
78+
79+
public function getWebhookName(): string
80+
{
81+
return $this->name;
82+
}
83+
84+
public function getUrl(): string
85+
{
86+
return $this->url;
87+
}
88+
89+
/**
90+
* @return array<string>
91+
*/
92+
public function getEvents(): array
93+
{
94+
return $this->events;
95+
}
96+
97+
public function getSecurity(): bool
98+
{
99+
return $this->security;
100+
}
101+
102+
public function getHttpUser(): string
103+
{
104+
return $this->httpUser;
105+
}
106+
107+
public function getHttpPass(): string
108+
{
109+
return $this->httpPass;
110+
}
111+
112+
public function isEnabled(): bool
113+
{
114+
return $this->enabled;
115+
}
116+
}

src/Migration/Sources/Appwrite.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Appwrite\Services\TablesDB;
1414
use Appwrite\Services\Teams;
1515
use Appwrite\Services\Users;
16+
use Appwrite\Services\Webhooks;
1617
use Utopia\Database\Database as UtopiaDatabase;
1718
use Utopia\Database\DateTime as UtopiaDateTime;
1819
use Utopia\Database\Document as UtopiaDocument;
@@ -62,6 +63,7 @@
6263
use Utopia\Migration\Resources\Messaging\Subscriber;
6364
use Utopia\Migration\Resources\Messaging\Topic;
6465
use Utopia\Migration\Resources\Settings\ProjectVariable;
66+
use Utopia\Migration\Resources\Settings\Webhook;
6567
use Utopia\Migration\Resources\Sites\Deployment as SiteDeployment;
6668
use Utopia\Migration\Resources\Sites\EnvVar as SiteEnvVar;
6769
use Utopia\Migration\Resources\Sites\Site;
@@ -98,6 +100,8 @@ class Appwrite extends Source
98100

99101
private Project $project;
100102

103+
private Webhooks $webhooks;
104+
101105
/**
102106
* @var callable(UtopiaDocument $database|null): UtopiaDatabase
103107
*/
@@ -127,6 +131,7 @@ public function __construct(
127131
$this->messaging = new Messaging($this->client);
128132
$this->sites = new Sites($this->client);
129133
$this->project = new Project($this->client);
134+
$this->webhooks = new Webhooks($this->client);
130135

131136
$this->headers['x-appwrite-project'] = $this->projectId;
132137
$this->headers['x-appwrite-key'] = $this->key;
@@ -210,6 +215,7 @@ public static function getSupportedResources(): array
210215
// Integrations
211216
Resource::TYPE_PLATFORM,
212217
Resource::TYPE_API_KEY,
218+
Resource::TYPE_WEBHOOK,
213219

214220
// Backups
215221
Resource::TYPE_BACKUP_POLICY,
@@ -1536,6 +1542,57 @@ private function exportProjectVariables(int $batchSize): void
15361542
}
15371543
}
15381544

1545+
/**
1546+
* @throws AppwriteException
1547+
*/
1548+
private function exportWebhooks(int $batchSize): void
1549+
{
1550+
$lastId = null;
1551+
1552+
while (true) {
1553+
$queries = [Query::limit($batchSize)];
1554+
1555+
if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_WEBHOOK) {
1556+
$queries[] = Query::equal('$id', $this->rootResourceId);
1557+
$queries[] = Query::limit(1);
1558+
}
1559+
1560+
if ($lastId !== null) {
1561+
$queries[] = Query::cursorAfter($lastId);
1562+
}
1563+
1564+
$response = $this->webhooks->list($queries);
1565+
if ($response->total === 0) {
1566+
break;
1567+
}
1568+
1569+
$webhooks = [];
1570+
1571+
foreach ($response->webhooks as $webhook) {
1572+
$webhooks[] = new Webhook(
1573+
$webhook->id,
1574+
$webhook->name,
1575+
$webhook->url,
1576+
$webhook->events,
1577+
$webhook->tls,
1578+
$webhook->authUsername,
1579+
$webhook->authPassword,
1580+
$webhook->enabled,
1581+
createdAt: $webhook->createdAt,
1582+
updatedAt: $webhook->updatedAt,
1583+
);
1584+
1585+
$lastId = $webhook->id;
1586+
}
1587+
1588+
$this->callback($webhooks);
1589+
1590+
if (\count($response->webhooks) < $batchSize) {
1591+
break;
1592+
}
1593+
}
1594+
}
1595+
15391596
/**
15401597
* @throws AppwriteException
15411598
*/
@@ -2341,6 +2398,19 @@ private function reportIntegrations(array $resources, array &$report, array $res
23412398
$report[Resource::TYPE_API_KEY] = 0;
23422399
}
23432400
}
2401+
2402+
if (\in_array(Resource::TYPE_WEBHOOK, $resources)) {
2403+
$webhookQueries = $this->buildQueries(
2404+
resourceType: Resource::TYPE_WEBHOOK,
2405+
resourceIds: $resourceIds,
2406+
limit: 1
2407+
);
2408+
try {
2409+
$report[Resource::TYPE_WEBHOOK] = $this->webhooks->list($webhookQueries)->total;
2410+
} catch (\Throwable) {
2411+
$report[Resource::TYPE_WEBHOOK] = 0;
2412+
}
2413+
}
23442414
}
23452415

23462416
/**
@@ -2400,6 +2470,20 @@ protected function exportGroupIntegrations(int $batchSize, array $resources): vo
24002470
));
24012471
}
24022472
}
2473+
2474+
if (\in_array(Resource::TYPE_WEBHOOK, $resources)) {
2475+
try {
2476+
$this->exportWebhooks($batchSize);
2477+
} catch (\Throwable $e) {
2478+
$this->addError(new Exception(
2479+
Resource::TYPE_WEBHOOK,
2480+
Transfer::GROUP_INTEGRATIONS,
2481+
message: $e->getMessage(),
2482+
code: $e->getCode(),
2483+
previous: $e
2484+
));
2485+
}
2486+
}
24032487
}
24042488

24052489
/**

src/Migration/Transfer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class Transfer
6565
public const GROUP_INTEGRATIONS_RESOURCES = [
6666
Resource::TYPE_PLATFORM,
6767
Resource::TYPE_API_KEY,
68+
Resource::TYPE_WEBHOOK,
6869
];
6970
public const GROUP_DOCUMENTSDB_RESOURCES = [
7071
Resource::TYPE_DATABASE_DOCUMENTSDB,
@@ -135,6 +136,7 @@ class Transfer
135136
// Integrations
136137
Resource::TYPE_PLATFORM,
137138
Resource::TYPE_API_KEY,
139+
Resource::TYPE_WEBHOOK,
138140

139141
// Settings
140142
Resource::TYPE_PROJECT_VARIABLE,

tests/Migration/Unit/Adapters/MockDestination.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public static function getSupportedResources(): array
5353
Resource::TYPE_MEMBERSHIP,
5454
Resource::TYPE_PLATFORM,
5555
Resource::TYPE_API_KEY,
56+
Resource::TYPE_PROJECT_VARIABLE,
57+
Resource::TYPE_WEBHOOK,
5658
Resource::TYPE_PROVIDER,
5759
Resource::TYPE_TOPIC,
5860
Resource::TYPE_SUBSCRIBER,

tests/Migration/Unit/Adapters/MockSource.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ public static function getSupportedResources(): array
8282
Resource::TYPE_MEMBERSHIP,
8383
Resource::TYPE_PLATFORM,
8484
Resource::TYPE_API_KEY,
85+
Resource::TYPE_PROJECT_VARIABLE,
86+
Resource::TYPE_WEBHOOK,
8587
Resource::TYPE_PROVIDER,
8688
Resource::TYPE_TOPIC,
8789
Resource::TYPE_SUBSCRIBER,

0 commit comments

Comments
 (0)