Skip to content

Feat csv export#10813

Merged
abnegate merged 8 commits into1.8.xfrom
feat-csv-export
Nov 14, 2025
Merged

Feat csv export#10813
abnegate merged 8 commits into1.8.xfrom
feat-csv-export

Conversation

@abnegate
Copy link
Copy Markdown
Member

What does this PR do?

(Provide a description of what this PR does and why it's needed.)

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.)

Related PRs and Issues

  • (Related PR or issue)

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Nov 13, 2025

📝 Walkthrough

Walkthrough

This pull request switches CSV export storage to an internal platform "default" bucket and removes the external bucketId parameter from the CSV export API. Controller action signatures were updated to inject dbForPlatform and no longer accept bucketId; bucket resolution now reads the 'default' bucket from the platform DB and migration options hardcode bucketId to 'default'. The migration worker and email flow now use a Document $user (not a userInternalId), create export file documents in the platform DB with explicit read permission for the exporting user, and generate JWT-protected download URLs. The storage push endpoint gained dbForPlatform and selects platform vs. project DB based on an internal JWT flag. Tests, a maintenance task, Deletes worker handling, and a new DELETE_TYPE_CSV_EXPORTS constant were added to support internal CSV export lifecycle and cleanup.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Verify sendCSVEmail signature change (Document $user) is consistently updated across callers and queued tasks.
  • Audit creation of export file documents in platform DB and applied permissions (Permission::read(Role::user(...))) for ACL correctness and no unintended exposure.
  • Review controller injection changes (added dbForPlatform, removed bucketId) and platform DB lookups (default bucket) for authorization and database boundary correctness.
  • Inspect storage push endpoint routing via the JWT internal flag to ensure correct db selection for internal vs project requests.
  • Validate new maintenance/Delete worker logic and DELETE_TYPE_CSV_EXPORTS handling for cutoff computation, query correctness, and safe physical-file deletion.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feat csv export' is vague and generic; it lacks specificity about what the CSV export feature entails and doesn't clearly convey the main objectives of the changes. Use a more descriptive title that specifies the primary change, such as 'Add CSV export for migrations with internal bucket storage' or similar to clarify the feature's scope.
Description check ❓ Inconclusive The PR description contains only the contribution template with no implementation details, test plan, or completed checklist items provided by the author. Complete the PR description with details about what the CSV export feature does, the testing methodology used, and any related issues, then check the contribution checklist.
✨ 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-csv-export

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.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Nov 13, 2025

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
libxml2 2.13.8-r0 CVE-2025-49794 CRITICAL
libxml2 2.13.8-r0 CVE-2025-49796 CRITICAL
libxml2 2.13.8-r0 CVE-2025-49795 HIGH
libxml2 2.13.8-r0 CVE-2025-6021 HIGH
github.com/containerd/containerd/v2 v2.0.2 CVE-2024-25621 HIGH
golang.org/x/crypto v0.31.0 CVE-2025-22869 HIGH
golang.org/x/oauth2 v0.24.0 CVE-2025-22868 HIGH
stdlib 1.22.10 CVE-2025-47907 HIGH
stdlib 1.22.10 CVE-2025-58183 HIGH
stdlib 1.22.10 CVE-2025-58186 HIGH
stdlib 1.22.10 CVE-2025-58187 HIGH
stdlib 1.22.10 CVE-2025-58188 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

@github-actions
Copy link
Copy Markdown

github-actions bot commented Nov 13, 2025

✨ Benchmark results

  • Requests per second: 1,195
  • Requests with 200 status code: 215,134
  • P99 latency: 0.164876996

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,195 1,204
200 215,134 216,747
P99 0.164876996 0.170244113

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Nov 13, 2025

Open in StackBlitz

npm i https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@10813

commit: 72c7178

Copy link
Copy Markdown
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

🧹 Nitpick comments (1)
src/Appwrite/Platform/Workers/Migrations.php (1)

570-573: Simplify the empty user check logic.

Calling getInternalId() on an empty Document in the warning message is unnecessary and potentially confusing since the user is already empty.

Apply this diff to simplify:

 if ($user->isEmpty()) {
-    Console::warning("User not found for CSV export notification: {$user->getInternalId()}");
+    Console::warning("User not found for CSV export notification");
     return;
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c78da61 and 3df2efb.

⛔ Files ignored due to path filters (4)
  • app/config/specs/open-api3-1.8.x-console.json is excluded by !app/config/specs/**
  • app/config/specs/open-api3-latest-console.json is excluded by !app/config/specs/**
  • app/config/specs/swagger2-1.8.x-console.json is excluded by !app/config/specs/**
  • app/config/specs/swagger2-latest-console.json is excluded by !app/config/specs/**
📒 Files selected for processing (3)
  • app/controllers/api/storage.php (2 hunks)
  • src/Appwrite/Platform/Workers/Migrations.php (12 hunks)
  • tests/e2e/Services/Migrations/MigrationsBase.php (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/Services/Migrations/MigrationsBase.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-23T08:06:38.889Z
Learnt from: abnegate
Repo: appwrite/appwrite PR: 10546
File: src/Appwrite/Platform/Workers/Migrations.php:144-148
Timestamp: 2025-10-23T08:06:38.889Z
Learning: In the Appwrite codebase, migration workers receive already-validated data from queued jobs. Query validation using Query::parseQueries() happens at the API endpoint level (with try-catch for QueryException) before jobs are queued, so workers in src/Appwrite/Platform/Workers/Migrations.php don't need redundant validation.

Applied to files:

  • src/Appwrite/Platform/Workers/Migrations.php
🧬 Code graph analysis (1)
app/controllers/api/storage.php (4)
src/Appwrite/Platform/Workers/Migrations.php (1)
  • action (93-131)
src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php (1)
  • action (69-117)
src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php (1)
  • action (62-120)
src/Appwrite/Auth/Auth.php (3)
  • Auth (18-515)
  • isAppUser (441-448)
  • isPrivilegedUser (421-432)
🔇 Additional comments (6)
src/Appwrite/Platform/Workers/Migrations.php (4)

14-22: LGTM: Import changes align with new permission handling.

The updated imports properly support the new platform-scoped bucket and file permissions functionality.


492-516: LGTM: Platform file document created with proper user-scoped permissions.

The file document is correctly created in the platform database with explicit read permissions for the requesting user using Permission::read(Role::user(...)).


522-527: LGTM: JWT payload correctly includes internal flag.

The 'internal' => true flag in the JWT payload is essential for the storage endpoints to differentiate between platform-scoped and project-scoped files.


449-452: No issues found—verification confirms the 'default' bucket is properly initialized.

The 'default' bucket is created during platform initialization in app/http.php, with a defensive check ensuring it only creates if absent. The error handling at lines 451-452 is a reasonable safeguard for unexpected scenarios. The code functions as designed.

app/controllers/api/storage.php (2)

1480-1484: LGTM: Platform database injection added correctly.

The dbForPlatform dependency is properly injected and added to the action signature following Appwrite conventions.


1501-1513: LGTM: Internal file routing logic is correct and backward compatible.

The implementation properly:

  • Extracts the internal flag from JWT with a safe default (false)
  • Selects the appropriate database (platform vs. project) based on the flag
  • Maintains authorization skip for both internal and external file access paths

Copy link
Copy Markdown
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

🧹 Nitpick comments (1)
app/controllers/api/storage.php (1)

1502-1508: I need to examine the actual code context in storage.php to understand how the JWT is decoded and validated. Let me search the repository:

Based on my verification, I can now provide the rewritten review comment. The key findings are:

  1. The JWT with the internal flag is only generated in the trusted Migrations worker process using the system's OpenSSL key (a server-side secret)
  2. JWTs with HS256 cannot be forged without the server secret key
  3. The concern about variable naming remains valid and should be addressed

Rename the reassigned database variable for clarity.

The reassignment of $dbForProject on line 1503 is misleading—the variable may reference either the platform or project database depending on the internal flag. To improve code maintainability, use a more descriptive name like $dbForStorage:

-        $isInternal = $decoded['internal'] ?? false;
-        $dbForProject = $isInternal ? $dbForPlatform : $dbForProject;
+        $isInternal = $decoded['internal'] ?? false;
+        $dbForStorage = $isInternal ? $dbForPlatform : $dbForProject;

-        $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
+        $bucket = Authorization::skip(fn () => $dbForStorage->getDocument('buckets', $bucketId));

Apply this change to line 1513 for the file lookup as well.

The JWT signature is verified server-side using the system's OpenSSL key, so forging the internal flag is not possible without access to the server secret.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3df2efb and b9211d7.

⛔ Files ignored due to path filters (4)
  • app/config/specs/open-api3-1.8.x-console.json is excluded by !app/config/specs/**
  • app/config/specs/open-api3-latest-console.json is excluded by !app/config/specs/**
  • app/config/specs/swagger2-1.8.x-console.json is excluded by !app/config/specs/**
  • app/config/specs/swagger2-latest-console.json is excluded by !app/config/specs/**
📒 Files selected for processing (4)
  • app/controllers/api/storage.php (2 hunks)
  • app/init/constants.php (1 hunks)
  • src/Appwrite/Platform/Tasks/Maintenance.php (2 hunks)
  • src/Appwrite/Platform/Workers/Deletes.php (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-09T04:06:04.338Z
Learnt from: ItzNotABug
Repo: appwrite/appwrite PR: 9693
File: src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php:88-95
Timestamp: 2025-05-09T04:06:04.338Z
Learning: It's acceptable to use DATABASE_TYPE_DELETE_COLLECTION event type for both collections and tables because workers process these events based on whether setCollection() or setTable() was called, not on the event type itself.

Applied to files:

  • app/init/constants.php
🧬 Code graph analysis (1)
app/controllers/api/storage.php (5)
src/Appwrite/Platform/Tasks/Maintenance.php (1)
  • action (35-100)
src/Appwrite/Platform/Workers/Deletes.php (1)
  • action (73-196)
src/Appwrite/Platform/Workers/Migrations.php (1)
  • action (93-131)
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/XList.php (1)
  • action (73-128)
src/Appwrite/Auth/Auth.php (4)
  • Auth (18-515)
  • isAppUser (441-448)
  • getRoles (456-502)
  • isPrivilegedUser (421-432)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: E2E Service Test (Messaging)
  • GitHub Check: E2E Service Test (VCS)
  • GitHub Check: E2E Service Test (Webhooks)
  • GitHub Check: E2E Service Test (FunctionsSchedule)
  • GitHub Check: E2E Service Test (Storage)
  • GitHub Check: E2E Service Test (GraphQL)
  • GitHub Check: E2E Service Test (Sites)
  • GitHub Check: E2E Service Test (Realtime)
  • GitHub Check: E2E Service Test (Functions)
  • GitHub Check: E2E Service Test (Databases/TablesDB)
  • GitHub Check: E2E Service Test (Databases/Legacy)
  • GitHub Check: E2E Service Test (Console)
  • GitHub Check: E2E Service Test (Account)
  • GitHub Check: E2E Service Test (Avatars)
  • GitHub Check: E2E Service Test (Site Screenshots)
  • GitHub Check: E2E Service Test (Dev Keys)
  • GitHub Check: E2E General Test
  • GitHub Check: Unit Test
  • GitHub Check: Setup & Build SDK
  • GitHub Check: scan
🔇 Additional comments (4)
app/controllers/api/storage.php (1)

1481-1481: LGTM: Platform database injection added correctly.

The injection of dbForPlatform follows the established pattern and is necessary for supporting internal bucket access in CSV export flows.

Also applies to: 1485-1485

app/init/constants.php (1)

141-141: LGTM: New deletion type constant added correctly.

The DELETE_TYPE_CSV_EXPORTS constant follows the established naming convention and is properly positioned with other deletion type constants.

src/Appwrite/Platform/Tasks/Maintenance.php (1)

98-98: LGTM: CSV export deletion maintenance task added correctly.

The notifyDeleteCSVExports method follows the same pattern as other maintenance notification methods and is properly integrated into the maintenance loop.

Also applies to: 110-115

src/Appwrite/Platform/Workers/Deletes.php (1)

182-184: LGTM: CSV exports deletion case added correctly.

The new case for DELETE_TYPE_CSV_EXPORTS is properly integrated into the deletion worker's switch statement.

Copy link
Copy Markdown
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

♻️ Duplicate comments (1)
src/Appwrite/Platform/Workers/Migrations.php (1)

437-447: Validate userInternalId before use and avoid exposing internal IDs.

The code doesn't validate that userInternalId exists or is non-empty before querying, and the error message exposes the internal user ID.

This issue was previously flagged. Apply this diff to add validation and improve the error message:

 $bucketId = 'default'; // Always use platform default bucket
 $filename = $options['filename'] ?? 'export_' . \time();
 $userInternalId = $options['userInternalId'] ?? '';
+
+if (empty($userInternalId)) {
+    throw new \Exception('User internal ID is required for CSV export');
+}
+
 $user = $this->dbForPlatform->findOne('users', [
     Query::equal('$sequence', [$userInternalId])
 ]);

 if ($user->isEmpty()) {
-    throw new \Exception('User ' . $userInternalId . ' not found');
+    throw new \Exception('User not found');
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0588fb4 and 72c7178.

📒 Files selected for processing (1)
  • src/Appwrite/Platform/Workers/Migrations.php (12 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-23T08:06:38.889Z
Learnt from: abnegate
Repo: appwrite/appwrite PR: 10546
File: src/Appwrite/Platform/Workers/Migrations.php:144-148
Timestamp: 2025-10-23T08:06:38.889Z
Learning: In the Appwrite codebase, migration workers receive already-validated data from queued jobs. Query validation using Query::parseQueries() happens at the API endpoint level (with try-catch for QueryException) before jobs are queued, so workers in src/Appwrite/Platform/Workers/Migrations.php don't need redundant validation.

Applied to files:

  • src/Appwrite/Platform/Workers/Migrations.php
📚 Learning: 2025-06-19T09:20:03.312Z
Learnt from: ItzNotABug
Repo: appwrite/appwrite PR: 9693
File: src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Update.php:57-59
Timestamp: 2025-06-19T09:20:03.312Z
Learning: In table-related endpoints (such as `src/Appwrite/Platform/Modules/Databases/Http/Databases/Tables/Update.php`), parameter descriptions should use "table" and "row" terminology instead of "collection" and "document" for clarity and consistency.

Applied to files:

  • src/Appwrite/Platform/Workers/Migrations.php
🧬 Code graph analysis (1)
src/Appwrite/Platform/Workers/Migrations.php (1)
src/Appwrite/Utopia/Response/Model/Document.php (1)
  • Document (8-115)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Unit Test
  • GitHub Check: E2E General Test
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: Check if utopia-php/database changed
  • GitHub Check: Setup & Build SDK
  • GitHub Check: Linter
  • GitHub Check: scan
🔇 Additional comments (6)
src/Appwrite/Platform/Workers/Migrations.php (6)

14-22: LGTM! Authorization and permission imports are appropriate.

The new imports for AuthorizationException, Permission, Role, and Authorization validator align with the platform DB access patterns and explicit permission handling introduced in this PR.


449-449: LGTM! Authorization skip is appropriate for platform bucket access.

Using Authorization::skip() to access the platform's default bucket is correct, as this is an internal operation that needs to bypass normal authorization checks.


492-516: LGTM! Platform DB usage and explicit permissions are correctly implemented.

The document creation in the platform DB with explicit read permissions for the exporting user (Permission::read(Role::user($user->getId()))) properly secures access to the CSV export file.


519-527: LGTM! JWT internal flag is appropriate.

Adding 'internal' => true to the JWT payload correctly marks this as an internal/platform resource, which aligns with the platform DB storage approach.


646-653: LGTM! Filename sanitization is appropriate.

The sanitizeFilename method correctly removes problematic filesystem characters and handles edge cases with a fallback to 'export'.


549-549: Fix the parameter type in docblock.

The docblock incorrectly states @param string $user when the actual parameter type is Document $user.

Apply this diff:

-     * @param Document $user The user who triggered the operation
+     * @param Document $user The user who triggered the operation

Wait, I see the docblock on line 549 already says @param Document $user. Let me re-check... Actually, looking at the code, line 549 shows:

     * @param Document $user The user who triggered the operation

This appears to already be correct. The past review comment may have been addressed. Let me skip this as it's already fixed.

@abnegate abnegate merged commit e783f4c into 1.8.x Nov 14, 2025
44 of 45 checks passed
@abnegate abnegate deleted the feat-csv-export branch November 14, 2025 04:16
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