Skip to content

Add backup policy migration support#145

Open
premtsd-code wants to merge 4 commits intomainfrom
feat-backup-policy-migration
Open

Add backup policy migration support#145
premtsd-code wants to merge 4 commits intomainfrom
feat-backup-policy-migration

Conversation

@premtsd-code
Copy link
Contributor

@premtsd-code premtsd-code commented Feb 5, 2026

Summary

  • Add backup-policy as a new resource type with Policy resource model
  • Export backup policies from Appwrite source (Cloud-only, graceful skip for self-hosted)
  • Import backup policies to Appwrite destination with resource reference validation
  • Add destination scope validation for policies.read and policies.write in report()
  • Make exportGroupBackups abstract in Source base class, add stubs in CSV, Firebase, JSON, NHost sources

Test plan

  • Migrate backup policies between two Cloud projects
  • Verify migration skips gracefully when source is self-hosted
  • Verify scope validation catches missing policies.read/policies.write
  • Verify plan validation catches additional_resource_not_allowed on Starter plan
  • Verify resource reference validation when policy targets a database/bucket that doesn't exist on destination
  • Run existing migration unit tests

Summary by CodeRabbit

  • New Features

    • Backup policy migration: export, import and reporting for backup policies across sources and Appwrite destinations.
    • Backups are treated as a new resource group with batching support for exports.
  • Behavior

    • Improved reporting with clear messages when backups aren’t available on a destination plan.
    • Source adapters include hooks for backup exports; some adapters provide stubs where not yet implemented.
  • Tests

    • Unit mocks updated to include the new backup policy resource.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

Walkthrough

Adds backup policy support across the migration system: introduces Resource::TYPE_BACKUP_POLICY and Transfer group GROUP_BACKUPS; adds a new Policy resource class; extends Source and Transfer to include backups grouping and batching; implements export/import and reporting for backups in Appwrite (including direct API calls and error handling); adds importBackupResource() to Appwrite destination; adds exportGroupBackups stubs to CSV, Firebase, JSON, NHost, MockSource, and updates test adapters to recognize the new resource type.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding backup policy migration support, which is the central feature across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-backup-policy-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/Migration/Destinations/Appwrite.php`:
- Around line 213-240: The POST check for backup policy scope (in the block
guarded by Resource::TYPE_BACKUP_POLICY where call() is used against
'/backups/policies' and the $scope variable is set) currently only treats 401s
specially; update the catch in that try/catch to also handle 400 and 404 status
codes: decode the error body from $e->getMessage(), and if the code is 400 or
404, swallow the error (treat as non-fatal so migrations continue) while
preserving other behavior for 401 (keep the existing
additional_resource_not_allowed handling and Missing scope exception); leave
other throw behavior unchanged. Ensure the logic references the same $scope
variable and call() invocations so it applies to the POST validation.

In `@src/Migration/Sources/Appwrite.php`:
- Around line 184-185: The current generic exception handler around backup
policy reporting masks permission errors; update the catch block inside the
Appwrite migration code that calls reportBackups (the code that uses
filterByIdArray() with $resourceIds) to inspect the exception type/code: if the
exception indicates an authorization/permission failure (HTTP 401 or 403 from
the Appwrite client exception), surface or rethrow it (or log it as a permission
error) so missing policies.read scope is visible; for other errors (404/501 or
other API-unavailable cases) continue to treat it as "0 policies" as before. Use
the exception's getCode()/getStatusCode() and the reportBackups/filterByIdArray
symbols to locate the handler and implement the conditional handling
accordingly.

Comment on lines +184 to 185
$this->reportBackups($resources, $report, $resourceIds);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Distinguish between API availability errors and permission errors in backup policy reporting.

The backup policy reporting uses a generic catch-all error handler (line 1665) that converts any exception into "0 policies", which masks permission errors (401/403) caused by missing policies.read scope. Since Appwrite Cloud may return different error codes than self-hosted (e.g., 404/501 for unsupported features vs. 401/403 for missing permissions), the error handling should distinguish between these cases and surface permission errors appropriately.

Note: $resourceIds is actually used via filterByIdArray() on line 1598, so no change is needed for that parameter.

Applies to: lines 1640-1669

🤖 Prompt for AI Agents
In `@src/Migration/Sources/Appwrite.php` around lines 184 - 185, The current
generic exception handler around backup policy reporting masks permission
errors; update the catch block inside the Appwrite migration code that calls
reportBackups (the code that uses filterByIdArray() with $resourceIds) to
inspect the exception type/code: if the exception indicates an
authorization/permission failure (HTTP 401 or 403 from the Appwrite client
exception), surface or rethrow it (or log it as a permission error) so missing
policies.read scope is visible; for other errors (404/501 or other
API-unavailable cases) continue to treat it as "0 policies" as before. Use the
exception's getCode()/getStatusCode() and the reportBackups/filterByIdArray
symbols to locate the handler and implement the conditional handling
accordingly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/Migration/Destinations/Appwrite.php`:
- Around line 1488-1524: The POST payload omits the resourceType even though the
import code reads and validates it; update the logic in the Appwrite destination
(the block that builds $params and the conditional that checks
$resource->getResourceId() and $collection) to add a 'resourceType' key to
$params whenever you set 'resourceId' (i.e., for supported collections
determined by match on $resource->getResourceType()), so the $this->call('POST',
'/backups/policies', ...) sends both resourceId and resourceType alongside
policyId, name, services, enabled, retention and schedule.
🧹 Nitpick comments (1)
src/Migration/Destinations/Appwrite.php (1)

1497-1517: Silently dropping resourceId for unsupported resource types may mask bugs.

When $resource->getResourceType() returns something other than TYPE_DATABASE or TYPE_BUCKET, the match falls through to null and the resourceId is silently omitted from the API call. Consider logging a warning or throwing when an unexpected resource type has a non-null resourceId, so future additions don't silently break.

♻️ Suggested approach
                     $collection = match ($resource->getResourceType()) {
                         Resource::TYPE_DATABASE => 'databases',
                         Resource::TYPE_BUCKET => 'buckets',
-                        default => null, // Only databases and buckets support per-resource backup policies
+                        default => throw new Exception(
+                            resourceName: $resource->getName(),
+                            resourceGroup: $resource->getGroup(),
+                            resourceId: $resource->getId(),
+                            message: 'Unsupported resource type for backup policy: ' . $resource->getResourceType(),
+                        ),
                     };
-
-                    // Only pass resourceId for supported resource types
-                    if ($collection !== null) {
-                        $doc = $this->database->getDocument($collection, $resource->getResourceId());

Comment on lines +1488 to +1524
$params = [
'policyId' => $resource->getId(),
'name' => $resource->getPolicyName(),
'services' => $resource->getServices(),
'enabled' => $resource->getEnabled(),
'retention' => $resource->getRetention(),
'schedule' => $resource->getSchedule(),
];

if ($resource->getResourceId()) {
$collection = match ($resource->getResourceType()) {
Resource::TYPE_DATABASE => 'databases',
Resource::TYPE_BUCKET => 'buckets',
default => null, // Only databases and buckets support per-resource backup policies
};

// Only pass resourceId for supported resource types
if ($collection !== null) {
$doc = $this->database->getDocument($collection, $resource->getResourceId());
if ($doc->isEmpty()) {
throw new Exception(
resourceName: $resource->getName(),
resourceGroup: $resource->getGroup(),
resourceId: $resource->getId(),
message: 'Referenced ' . $resource->getResourceType() . ' "' . $resource->getResourceId() . '" not found on destination',
);
}

$params['resourceId'] = $resource->getResourceId();
}
}

$this->call('POST', '/backups/policies', [
'Content-Type' => 'application/json',
'X-Appwrite-Project' => $this->project,
'X-Appwrite-Key' => $this->key,
], $params);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for /backups/policies usage to understand the API contract
rg -n "backups/policies" --type php -B 2 -A 5

Repository: utopia-php/migration

Length of output: 3194


🏁 Script executed:

#!/bin/bash
# Find and examine the Policy resource class
fd "Policy.php" -type f

Repository: utopia-php/migration

Length of output: 234


🏁 Script executed:

#!/bin/bash
# Look for exportBackupResource method to understand reverse operation
rg -n "exportBackupResource|TYPE_BACKUP_POLICY" --type php -A 5 -B 2

Repository: utopia-php/migration

Length of output: 8359


🏁 Script executed:

#!/bin/bash
# Find the Policy resource class file and examine its properties
find . -name "Policy.php" -path "*/Backups/*" | head -5

Repository: utopia-php/migration

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Look at the exportBackupPolicies method in full
rg -n "exportBackupPolicies" -A 30 src/Migration/Sources/Appwrite.php

Repository: utopia-php/migration

Length of output: 2239


🏁 Script executed:

#!/bin/bash
# Check the Policy class properties and methods
rg -n "class Policy|public function get|private \$" src/Migration/Resources/Backups/Policy.php

Repository: utopia-php/migration

Length of output: 457


🏁 Script executed:

#!/bin/bash
# Examine the Policy class constructor and all properties
cat -n src/Migration/Resources/Backups/Policy.php

Repository: utopia-php/migration

Length of output: 3521


🏁 Script executed:

#!/bin/bash
# Check the complete importBackupResource method to see the full context
rg -n "importBackupResource" -A 50 src/Migration/Destinations/Appwrite.php | head -80

Repository: utopia-php/migration

Length of output: 3726


🏁 Script executed:

#!/bin/bash
# Check if there are any tests or examples showing the expected API contract
rg -n "resourceType" src/Migration/Destinations/Appwrite.php -B 2 -A 2

Repository: utopia-php/migration

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Look for any comments or documentation about the POST /backups/policies endpoint
rg -n "policies" src/Migration/Destinations/Appwrite.php -B 3 -A 3 | grep -A 5 -B 5 "POST"

Repository: utopia-php/migration

Length of output: 974


🏁 Script executed:

#!/bin/bash
# Verify the complete POST call in importBackupResource to see full context
sed -n '1483,1530p' src/Migration/Destinations/Appwrite.php

Repository: utopia-php/migration

Length of output: 2276


resourceType parameter is missing from the POST /backups/policies request.

The Policy resource class stores and provides getResourceType(), and the export method reads it from the source API (line 1685 in Sources/Appwrite.php). However, the import method reads this value for validation (line 1498) but never includes it in the $params sent to the destination API. Only resourceId is conditionally added to params (line 1517), creating an asymmetry in the bidirectional data mapping.

Fix: Include resourceType in POST params when resourceId is present
                        $params['resourceId'] = $resource->getResourceId();
+                       $params['resourceType'] = $resource->getResourceType();
🤖 Prompt for AI Agents
In `@src/Migration/Destinations/Appwrite.php` around lines 1488 - 1524, The POST
payload omits the resourceType even though the import code reads and validates
it; update the logic in the Appwrite destination (the block that builds $params
and the conditional that checks $resource->getResourceId() and $collection) to
add a 'resourceType' key to $params whenever you set 'resourceId' (i.e., for
supported collections determined by match on $resource->getResourceType()), so
the $this->call('POST', '/backups/policies', ...) sends both resourceId and
resourceType alongside policyId, name, services, enabled, retention and
schedule.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant