Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 13 additions & 1 deletion src/Migration/Destinations/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -2896,6 +2896,18 @@ protected function createProvider(Provider $resource): void
($options['replyToEmail'] ?? '') ?: null,
$enabled,
),
'ses' => $this->messaging->createSesProvider(
$id,
$name,
$credentials['accessKey'] ?? null,
$credentials['secretKey'] ?? null,
$credentials['region'] ?? null,
($options['fromName'] ?? '') ?: null,
($options['fromEmail'] ?? '') ?: null,
($options['replyToName'] ?? '') ?: null,
($options['replyToEmail'] ?? '') ?: null,
$enabled,
),
'smtp' => $this->messaging->createSMTPProvider(
$id,
$name,
Expand Down Expand Up @@ -3158,7 +3170,7 @@ protected function createScheduledMessage(Message $resource, array $resolvedTarg
$targets,
$data['cc'] ?? null,
$data['bcc'] ?? null,
null,
$data['attachments'] ?? null,
false,
$data['html'] ?? null,
$scheduledAt,
Expand Down
61 changes: 60 additions & 1 deletion src/Migration/Source.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ public function supportsDatabaseStatus(): bool
return false;
}

/**
* Resources this source cannot transfer unless their prerequisites travel
* with them, keyed by resource type.
*
* @return array<string, array<string>>
*/
public function getResourceDependencies(): array
{
return [];
}

public function getAuthBatchSize(): int
{
return static::$defaultBatchSize;
Expand Down Expand Up @@ -160,8 +171,9 @@ public function runWithResourceSelector(
*/
public function exportResources(array $resources): void
{
$requested = $resources;
$groups = [];
foreach ($resources as $resource) {
foreach ($requested as $resource) {
$mapping = [
Transfer::GROUP_AUTH => Transfer::GROUP_AUTH_RESOURCES,
Transfer::GROUP_DATABASES => Transfer::GROUP_DATABASES_RESOURCES,
Expand All @@ -187,6 +199,8 @@ public function exportResources(array $resources): void
return;
}

$this->reportMissingDependencies($requested, $groups);

foreach ($groups as $group => $resources) {
switch ($group) {
case Transfer::GROUP_AUTH:
Expand Down Expand Up @@ -223,6 +237,51 @@ public function exportResources(array $resources): void
}
}

/**
* Record an error for every requested resource whose prerequisites are absent.
*
* A resource requested without its prerequisites is not an error on its own:
* its exporter walks a cache the missing prerequisite never filled, or is
* only reached by the prerequisite's own exporter. Neither path raises
* anything, so the transfer finishes reporting success having moved none of
* it. Naming what is absent turns that into a failure someone can act on.
*
* @param array<string> $requested
* @param array<string, array<string>> $groups
*/
private function reportMissingDependencies(array $requested, array $groups): void
{
$groupOf = [];
foreach ($groups as $group => $resources) {
foreach ($resources as $resource) {
$groupOf[$resource] = $group;
}
}

foreach ($this->getResourceDependencies() as $resource => $requires) {
if (!\in_array($resource, $requested, true)) {
continue;
}

$missing = \array_values(\array_diff($requires, $requested));

if (empty($missing)) {
continue;
}

$this->addError(new Exception(
$resource,
$groupOf[$resource] ?? Transfer::GROUP_GENERAL,
message: \sprintf(
'Cannot transfer %s without %s.',
$resource,
\implode(' and ', $missing)
),
code: Exception::CODE_VALIDATION,
));
}
}

/**
* Export Auth Group
*
Expand Down
29 changes: 29 additions & 0 deletions src/Migration/Sources/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,35 @@ public static function getSupportedResources(): array
];
}

/**
* Prerequisites each resource needs in the same request. Every exporter here
* either walks its parent out of the transfer cache, or is emitted inline by
* the parent's own exporter, so a request naming the child alone moves
* nothing at all.
*
* Index, collection, attribute and document are deliberately absent. They
* are shared between the tables, documents and vectors flavours, whose
* parents differ, and naming one flavour's parents would reject the others.
*
Comment on lines +332 to +335

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Database dependencies remain incomplete

The declaration deliberately omits index, collection, attribute, and document, leaving the silent-success defect incomplete. These are requestable resources whose exporters depend on caches populated by their database or entity exporters. For example, requesting index without table or collection makes exportIndexes() iterate an empty entity cache, transfer nothing, and record no dependency error. The dependency model needs to support valid alternative parents instead of exempting these resources from validation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Migration/Sources/Appwrite.php
Line: 332-335

Comment:
**Database dependencies remain incomplete**

The declaration deliberately omits `index`, `collection`, `attribute`, and `document`, leaving the silent-success defect incomplete. These are requestable resources whose exporters depend on caches populated by their database or entity exporters. For example, requesting `index` without `table` or `collection` makes `exportIndexes()` iterate an empty entity cache, transfer nothing, and record no dependency error. The dependency model needs to support valid alternative parents instead of exempting these resources from validation.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

* @return array<string, array<string>>
*/
#[Override]
public function getResourceDependencies(): array
{
return [
Resource::TYPE_MEMBERSHIP => [Resource::TYPE_USER, Resource::TYPE_TEAM],
Resource::TYPE_SUBSCRIBER => [Resource::TYPE_TOPIC, Resource::TYPE_USER],
Resource::TYPE_TABLE => [Resource::TYPE_DATABASE],
Resource::TYPE_COLUMN => [Resource::TYPE_DATABASE, Resource::TYPE_TABLE],
Resource::TYPE_ROW => [Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_COLUMN],
Resource::TYPE_FILE => [Resource::TYPE_BUCKET],
Resource::TYPE_ENVIRONMENT_VARIABLE => [Resource::TYPE_FUNCTION],
Resource::TYPE_DEPLOYMENT => [Resource::TYPE_FUNCTION],
Resource::TYPE_SITE_VARIABLE => [Resource::TYPE_SITE],
Resource::TYPE_SITE_DEPLOYMENT => [Resource::TYPE_SITE],
];
}

/**
* @return int
*/
Expand Down
13 changes: 13 additions & 0 deletions tests/Migration/Unit/Adapters/MockSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,21 @@ class MockSource extends Source
{
private array $mockResources = [];

private array $resourceDependencies = [];

private ?string $resourceChildId = null;

public function setResourceDependencies(array $dependencies): void
{
$this->resourceDependencies = $dependencies;
}

#[Override]
public function getResourceDependencies(): array
{
return $this->resourceDependencies;
}

#[Override]
public function run(
array $resources,
Expand Down
100 changes: 100 additions & 0 deletions tests/Migration/Unit/General/ResourceDependenciesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

namespace Utopia\Tests\Unit\General;

use PHPUnit\Framework\TestCase;
use Utopia\Migration\Resource;
use Utopia\Migration\Resources\Auth\Membership;
use Utopia\Migration\Resources\Auth\Team;
use Utopia\Migration\Resources\Auth\User;
use Utopia\Migration\Transfer;
use Utopia\Tests\Unit\Adapters\MockDestination;
use Utopia\Tests\Unit\Adapters\MockSource;

/**
* A resource requested without its prerequisites cannot be transferred: its
* exporter walks a cache the missing prerequisite never filled, or is only
* reached by the prerequisite's own exporter. Nothing throws, so without this
* check the transfer finishes reporting success having moved nothing.
*/
class ResourceDependenciesTest extends TestCase
{
protected Transfer $transfer;

protected MockSource $source;

protected MockDestination $destination;

public function setup(): void
{
$this->source = new MockSource();
$this->destination = new MockDestination();

$this->transfer = new Transfer(
$this->source,
$this->destination
);

$this->source->setResourceDependencies([
Resource::TYPE_MEMBERSHIP => [Resource::TYPE_USER, Resource::TYPE_TEAM],
]);
Comment on lines +38 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tests mirror dependency configuration

These tests inject the exact membership prerequisites that the production Appwrite source is supposed to declare. A typo, omission, or incorrect Appwrite declaration therefore leaves every test green; the real declaration is never exercised. This violates the repository directive to test observable behavior instead of mirroring source code or configuration in assertions. Replace this setup with coverage that consumes Appwrite's actual dependency declarations. This repository requirement must be satisfied before merging.

Context Used: Call out and harshly judge implementation-coupled ... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/Migration/Unit/General/ResourceDependenciesTest.php
Line: 38-40

Comment:
**Tests mirror dependency configuration**

These tests inject the exact membership prerequisites that the production Appwrite source is supposed to declare. A typo, omission, or incorrect Appwrite declaration therefore leaves every test green; the real declaration is never exercised. This violates the repository directive to test observable behavior instead of mirroring source code or configuration in assertions. Replace this setup with coverage that consumes Appwrite's actual dependency declarations. This repository requirement must be satisfied before merging.

**Context Used:** Call out and harshly judge implementation-coupled ... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex


$team = new Team('team', 'Team');
$user = new User('user', 'user@example.com');

$this->source->pushMockResource($team);
$this->source->pushMockResource($user);
$this->source->pushMockResource(new Membership('membership', $team, $user));
}

public function testMissingPrerequisitesAreReported(): void
{
$this->transfer->run([Resource::TYPE_MEMBERSHIP], function () {
});

$errors = $this->source->getErrors();

$this->assertCount(1, $errors);
$this->assertSame(Resource::TYPE_MEMBERSHIP, $errors[0]->getResourceName());
$this->assertSame(
'Cannot transfer membership without user and team.',
$errors[0]->getMessage()
);
}

public function testOnlyTheAbsentPrerequisitesAreNamed(): void
{
$this->transfer->run(
[Resource::TYPE_USER, Resource::TYPE_MEMBERSHIP],
function () {
}
);

$errors = $this->source->getErrors();

$this->assertCount(1, $errors);
$this->assertSame(
'Cannot transfer membership without team.',
$errors[0]->getMessage()
);
}

public function testASatisfiedRequestIsNotReported(): void
{
$this->transfer->run(
[Resource::TYPE_USER, Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP],
function () {
}
);

$this->assertEmpty($this->source->getErrors());
}

public function testAResourceWithNoPrerequisitesIsNotReported(): void
{
$this->transfer->run([Resource::TYPE_USER], function () {
});

$this->assertEmpty($this->source->getErrors());
}
}
Loading