diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6eea312628..d6e029bc1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -424,6 +424,10 @@ jobs: _APP_BROWSER_HOST: http://invalid-browser/v1 _APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }} _APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }} run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose pull --quiet --ignore-buildable @@ -498,6 +502,10 @@ jobs: _APP_OPTIONS_ABUSE: enabled _APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }} _APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }} run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose pull --quiet --ignore-buildable @@ -555,6 +563,10 @@ jobs: env: _APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }} _APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }} + _APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }} run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose pull --quiet --ignore-buildable diff --git a/AGENTS.md b/AGENTS.md index bb24d9f4fe..4d11ff0ee3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,107 +1,120 @@ -# AGENTS.md +# Appwrite -Appwrite is an end-to-end backend server for web, mobile, native, and backend apps. This guide provides context and instructions for AI coding agents working on the Appwrite codebase. +Self-hosted Backend-as-a-Service platform. Hybrid monolithic-microservice architecture built with PHP 8.3+ on Swoole, delivered as Docker containers. -## Project Overview +## Commands -Appwrite is a self-hosted Backend-as-a-Service (BaaS) platform that provides developers with a set of APIs and tools to build secure, scalable applications. The project uses a hybrid monolithic-microservice architecture built with PHP, running on Swoole for high performance. +| Command | Purpose | +|---------|---------| +| `docker compose up -d --force-recreate --build` | Build and start all services | +| `docker compose exec appwrite test tests/e2e/Services/[Service]` | Run E2E tests for a service | +| `docker compose exec appwrite test tests/e2e/Services/[Service] --filter=[Method]` | Run a single test method | +| `docker compose exec appwrite test tests/unit/` | Run unit tests | +| `composer format` | Auto-format code (Pint, PSR-12) | +| `composer format ` | Format a specific file | +| `composer lint ` | Check formatting of a file | +| `composer analyze` | Static analysis (PHPStan level 3) | +| `composer check` | Same as `analyze` | -**Key Technologies:** -- **Backend:** PHP 8.3+, Swoole -- **Libraries:** Utopia PHP -- **Database:** MariaDB, Redis -- **Cache:** Redis -- **Queue:** Redis -- **Containers:** Docker +## Stack -## Development Commands +- PHP 8.3+, Swoole 6.x (async runtime, replaces PHP-FPM) +- Utopia PHP framework (HTTP routing, CLI, DI, queue) +- MongoDB (default), MariaDB, MySQL, PostgreSQL (adapters via utopia-php/database) +- Redis (cache, queue, pub/sub) +- Docker + Traefik (reverse proxy) +- PHPUnit 12, Pint (PSR-12), PHPStan level 3 -```bash -# Run Appwrite -docker compose up -d --force-recreate --build +## Project layout -# Run specific test -docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName] --filter=[FunctionName] +- **src/Appwrite/Platform/Modules/** -- feature modules (Account, Avatars, Compute, Console, Databases, Functions, Health, Project, Projects, Proxy, Sites, Storage, Teams, Tokens, VCS, Webhooks) +- **src/Appwrite/Platform/Workers/** -- background job workers +- **src/Appwrite/Platform/Tasks/** -- CLI tasks +- **app/init.php** -- bootstrap (registers services, resources, listeners) +- **app/init/** -- configs, constants, locales, models, registers, resources, span, database filters/formats +- **bin/** -- CLI entry points: `worker-*` (14 workers), `schedule-*`, `queue-*`, plus `doctor`, `install`, `migrate`, `realtime`, `upgrade`, `ssl`, `vars`, `maintenance`, `interval`, `specs`, `sdks`, etc. +- **tests/e2e/** -- end-to-end tests per service +- **tests/unit/** -- unit tests +- **public/** -- static assets and generated SDKs -# Format code -composer format +## Module structure + +Each module under `src/Appwrite/Platform/Modules/{Name}/` contains: + +``` +Module.php -- registers all services for the module +Services/Http.php -- registers HTTP endpoints +Services/Workers.php -- registers background workers +Services/Tasks.php -- registers CLI tasks +Http/{Service}/ -- endpoint actions (Create.php, Get.php, Update.php, Delete.php, XList.php) +Workers/ -- worker implementations +Tasks/ -- CLI task implementations ``` -## Code Style Guidelines +HTTP endpoint nesting reflects the URL path. Sub-resources get subdirectories. For example, within the Functions module: +`Http/Deployments/Template/Create.php` -> `POST /v1/functions/:functionId/deployments/template` -- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standard -- Use PSR-4 autoloading -- Strict type declarations where applicable -- Comprehensive PHPDoc comments +File names in Http directories must only be `Get.php`, `Create.php`, `Update.php`, `Delete.php`, or `XList.php`. For non-CRUD operations, model the endpoint as a property update. For example, updating a team membership status lives at `Teams/Http/Memberships/Status/Update.php` (`PATCH /v1/teams/:teamId/memberships/:membershipId/status`). -### Naming Conventions +Register new modules in `src/Appwrite/Platform/Appwrite.php`. Detailed module guide: `src/Appwrite/Platform/AGENTS.md`. -#### `resourceType` Naming Rule +## Action pattern (HTTP endpoints) -When a collection has a combination of `resourceType`, `resourceId`, and/or `resourceInternalId`, the value of `resourceType` MUST always be **plural** - for example: `functions`, `sites`, `deployments`. - -Examples: ```php -'resourceType' => 'functions' -'resourceType' => 'sites' -'resourceType' => 'deployments' +class Create extends Action +{ + public static function getName(): string { return 'createTeam'; } + + public function __construct() + { + $this + ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/teams') + ->desc('Create team') + ->groups(['api', 'teams']) + ->label('event', 'teams.[teamId].create') + ->label('scope', 'teams.write') + ->param('teamId', '', new CustomId(), 'Team ID.') + ->param('name', null, new Text(128), 'Team name.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->callback($this->action(...)); + } + + public function action( + string $teamId, + string $name, + Response $response, + Database $dbForProject, + Event $queueForEvents, + ): void { + // implementation + } +} ``` -## Performance Patterns +Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `$user`, `$project`, `$queueForEvents`, `$queueForMails`, `$queueForDeletes`. -### Document Update Optimization +## Conventions -When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`. +- PSR-12 formatting enforced by Pint. PSR-4 autoloading. +- `resourceType` values are always **plural**: `'functions'`, `'sites'`, `'deployments'`. +- When updating documents, pass only changed attributes as a sparse Document: + ```php + // correct + $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'name' => $name, + ])); + // incorrect -- passing full document is inefficient + $user->setAttribute('name', $name); + $dbForProject->updateDocument('users', $user->getId(), $user); + ``` + Exceptions: migrations, `array_merge()` with `getArrayCopy()`, updates where nearly all attributes change, complex nested relationship logic requiring full document state. +- Avoid introducing dependencies outside the `utopia-php` ecosystem. +- Never hardcode credentials -- use environment variables. +- Code changes may require container restart. No central log location -- check relevant containers. -**Correct Pattern:** -```php -// Good: Pass only changed attributes directly -$user = $dbForProject->updateDocument('users', $user->getId(), new Document([ - 'name' => $name, - 'email' => $email, -])); -``` +## Cross-repo context -**Incorrect Pattern:** -```php -$user->setAttribute('name', $name); -$user->setAttribute('email', $email); - -// Bad: Passing full document is inefficient -$user = $dbForProject->updateDocument('users', $user->getId(), $user); -``` - -**Exceptions:** -- Migration files (need full document updates by design) -- Cases already using `array_merge()` with `getArrayCopy()` -- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document) -- Complex nested relationship logic where full document state is required - -## Security Considerations - -### Critical Security Practices - -- **Never hardcode credentials** - Use environment variables -- **Rate limiting** - Respect abuse prevention mechanisms - -## Dependencies - -Avoid introducing new dependencies other than utopia-php. - -## Adding new endpoints - -When adding new endpoints, make sure to use modules and follow its patterns. Find instruction in [Modules AGENTS.md](src/Appwrite/Platform/AGENTS.md) file. - -## Pull Request Guidelines -### Before Submitting - -- Run `composer format` -- Update documentation if adding features -- Add/update tests for your changes -- Check that Docker build succeeds -`docs/specs/authentication.drawio.svg` - -## Known Issues and Gotchas - -- **Hot Reload:** Code changes require container restart in some cases -- **Logging:** There is no central place for logs, so when debugging, ensure to check all possibly relevant containers +Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console. diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json index 8e59c40123..3d667a36ad 100644 --- a/app/config/locale/translations/en.json +++ b/app/config/locale/translations/en.json @@ -57,21 +57,21 @@ "emails.recovery.thanks": "Thanks,", "emails.recovery.buttonText": "Reset password", "emails.recovery.signature": "{{project}} team", - "emails.csvExport.success.subject": "Your CSV export is ready", - "emails.csvExport.success.preview": "Your data export has been completed successfully.", - "emails.csvExport.success.hello": "Hello {{user}},", - "emails.csvExport.success.body": "Your CSV export is ready to download. Click the button below to download your data export.", - "emails.csvExport.success.footer": "This download link will expire in 1 hour.", - "emails.csvExport.success.thanks": "Thanks,", - "emails.csvExport.success.buttonText": "Download CSV", - "emails.csvExport.success.signature": "Appwrite team", - "emails.csvExport.failure.subject": "Your CSV export failed - file too large", - "emails.csvExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.", - "emails.csvExport.failure.hello": "Hello {{user}},", - "emails.csvExport.failure.body": "Your CSV export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.", - "emails.csvExport.failure.footer": "If you have any questions, please contact our support team.", - "emails.csvExport.failure.thanks": "Thanks,", - "emails.csvExport.failure.signature": "{{project}} team", + "emails.dataExport.success.subject": "Your {{type}} export is ready", + "emails.dataExport.success.preview": "Your data export has been completed successfully.", + "emails.dataExport.success.hello": "Hello {{user}},", + "emails.dataExport.success.body": "Your {{type}} export is ready to download. Click the button below to download your data export.", + "emails.dataExport.success.footer": "This download link will expire in 1 hour.", + "emails.dataExport.success.thanks": "Thanks,", + "emails.dataExport.success.buttonText": "Download {{type}}", + "emails.dataExport.success.signature": "Appwrite team", + "emails.dataExport.failure.subject": "Your {{type}} export failed - file too large", + "emails.dataExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.", + "emails.dataExport.failure.hello": "Hello {{user}},", + "emails.dataExport.failure.body": "Your {{type}} export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.", + "emails.dataExport.failure.footer": "If you have any questions, please contact our support team.", + "emails.dataExport.failure.thanks": "Thanks,", + "emails.dataExport.failure.signature": "{{project}} team", "emails.invitation.subject": "Invitation to {{team}} Team at {{project}}", "emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}", "emails.invitation.hello": "Hello {{user}},", diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 5a87293b49..45a663fb56 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -29,6 +29,7 @@ use Utopia\Migration\Resource; use Utopia\Migration\Sources\Appwrite; use Utopia\Migration\Sources\CSV; use Utopia\Migration\Sources\Firebase; +use Utopia\Migration\Sources\JSON; use Utopia\Migration\Sources\NHost; use Utopia\Migration\Sources\Supabase; use Utopia\Migration\Transfer; @@ -53,6 +54,15 @@ function getDatabaseTransferResourceServices(string $databaseType) }; } +function getDatabaseResourceType(string $databaseType): string +{ + return match($databaseType) { + DATABASE_TYPE_VECTORSDB => Resource::TYPE_DATABASE_VECTORSDB, + DATABASE_TYPE_DOCUMENTSDB => Resource::TYPE_DATABASE_DOCUMENTSDB, + default => Resource::TYPE_DATABASE, + }; +} + Http::post('/v1/migrations/appwrite') ->groups(['api', 'migrations']) ->desc('Create Appwrite migration') @@ -447,6 +457,7 @@ Http::post('/v1/migrations/csv/imports') } $fileSize = $deviceForMigrations->getFileSize($newPath); $resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]); + $resourceType = getDatabaseResourceType($databaseType); $migration = $dbForProject->createDocument('migrations', new Document([ '$id' => $migrationId, @@ -456,7 +467,7 @@ Http::post('/v1/migrations/csv/imports') 'destination' => Appwrite::getName(), 'resources' => $resources, 'resourceId' => $resourceId, - 'resourceType' => Resource::TYPE_DATABASE, + 'resourceType' => $resourceType, 'statusCounters' => '{}', 'resourceData' => '{}', 'errors' => [], @@ -565,16 +576,6 @@ Http::post('/v1/migrations/csv/exports') throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $validator = new Documents( - attributes: $collection->getAttribute('attributes', []), - indexes: $collection->getAttribute('indexes', []), - idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(), - ); - - if (!$validator->isValid($parsedQueries)) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); - } - // getting databasetype $resources = explode(':', $resourceId); $databaseId = $resources[0]; @@ -583,7 +584,23 @@ Http::post('/v1/migrations/csv/exports') if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) { throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv'); } + + // Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields + $isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]); + + $validator = new Documents( + attributes: $collection->getAttribute('attributes', []), + indexes: $collection->getAttribute('indexes', []), + idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(), + supportForAttributes: !$isSchemaless, + ); + + if (!$validator->isValid($parsedQueries)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + $resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]); + $resourceType = getDatabaseResourceType($databaseType); $migration = $dbForProject->createDocument('migrations', new Document([ '$id' => ID::unique(), @@ -593,7 +610,7 @@ Http::post('/v1/migrations/csv/exports') 'destination' => CSV::getName(), 'resources' => $resources, 'resourceId' => $resourceId, - 'resourceType' => Resource::TYPE_DATABASE, + 'resourceType' => $resourceType, 'statusCounters' => '{}', 'resourceData' => '{}', 'errors' => [], @@ -624,6 +641,291 @@ Http::post('/v1/migrations/csv/exports') ->dynamic($migration, Response::MODEL_MIGRATION); }); +Http::post('/v1/migrations/json/imports') + ->groups(['api', 'migrations']) + ->desc('Import documents from a JSON') + ->label('scope', 'migrations.write') + ->label('event', 'migrations.[migrationId].create') + ->label('audits.event', 'migration.create') + ->label('sdk', new Method( + namespace: 'migrations', + group: null, + name: 'createJSONImport', + description: '/docs/references/migrations/migration-json-import.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_ACCEPTED, + model: Response::MODEL_MIGRATION, + ) + ] + )) + ->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).') + ->param('fileId', '', new UID(), 'File ID.') + ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.') + ->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true) + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('authorization') + ->inject('project') + ->inject('platform') + ->inject('deviceForFiles') + ->inject('deviceForMigrations') + ->inject('queueForEvents') + ->inject('queueForMigrations') + ->action(function ( + string $bucketId, + string $fileId, + string $resourceId, + bool $internalFile, + Response $response, + Database $dbForProject, + Database $dbForPlatform, + Authorization $authorization, + Document $project, + array $platform, + Device $deviceForFiles, + Device $deviceForMigrations, + Event $queueForEvents, + Migration $queueForMigrations + ) { + $bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) { + if ($internalFile) { + return $dbForPlatform->getDocument('buckets', 'default'); + } + return $dbForProject->getDocument('buckets', $bucketId); + }); + + if ($bucket->isEmpty()) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + + $file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId)); + if ($file->isEmpty()) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); + } + + $path = $file->getAttribute('path', ''); + if (!$deviceForFiles->exists($path)) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path); + } + + // No encryption or compression on files above 20MB. + $hasEncryption = !empty($file->getAttribute('openSSLCipher')); + $compression = $file->getAttribute('algorithm', Compression::NONE); + $hasCompression = $compression !== Compression::NONE; + + $migrationId = ID::unique(); + $newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.json'); + + if ($hasEncryption || $hasCompression) { + $source = $deviceForFiles->read($path); + + if ($hasEncryption) { + $source = OpenSSL::decrypt( + $source, + $file->getAttribute('openSSLCipher'), + System::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')), + 0, + hex2bin($file->getAttribute('openSSLIV')), + hex2bin($file->getAttribute('openSSLTag')) + ); + } + + if ($hasCompression) { + switch ($compression) { + case Compression::ZSTD: + $source = (new Zstd())->decompress($source); + break; + case Compression::GZIP: + $source = (new GZIP())->decompress($source); + break; + } + } + + // Manual write after decryption and/or decompression + if (!$deviceForMigrations->write($newPath, $source, 'application/json')) { + throw new \Exception('Unable to copy file'); + } + } elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) { + throw new \Exception('Unable to copy file'); + } + + $fileSize = $deviceForMigrations->getFileSize($newPath); + + [$databaseId] = \explode(':', $resourceId, 2); + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + $databaseType = $database->getAttribute('type'); + $resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]); + $resourceType = getDatabaseResourceType($databaseType); + + $migration = $dbForProject->createDocument('migrations', new Document([ + '$id' => $migrationId, + 'status' => 'pending', + 'stage' => 'init', + 'source' => JSON::getName(), + 'destination' => Appwrite::getName(), + 'resources' => $resources, + 'resourceId' => $resourceId, + 'resourceType' => $resourceType, + 'statusCounters' => '{}', + 'resourceData' => '{}', + 'errors' => [], + 'options' => [ + 'path' => $newPath, + 'size' => $fileSize, + ], + ])); + + $queueForEvents->setParam('migrationId', $migration->getId()); + + $queueForMigrations + ->setMigration($migration) + ->setProject($project) + ->setPlatform($platform) + ->trigger(); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($migration, Response::MODEL_MIGRATION); + }); + +Http::post('/v1/migrations/json/exports') + ->groups(['api', 'migrations']) + ->desc('Export documents to JSON') + ->label('scope', 'migrations.write') + ->label('event', 'migrations.[migrationId].create') + ->label('audits.event', 'migration.create') + ->label('sdk', new Method( + namespace: 'migrations', + group: null, + name: 'createJSONExport', + description: '/docs/references/migrations/migration-json-export.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_ACCEPTED, + model: Response::MODEL_MIGRATION, + ) + ] + )) + ->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.') + ->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .json extension.') + ->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true) + ->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) + ->param('notify', true, new Boolean(), 'Set to true to receive an email when the export is complete. Default is true.', true) + ->inject('user') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('authorization') + ->inject('project') + ->inject('platform') + ->inject('queueForEvents') + ->inject('queueForMigrations') + ->action(function ( + string $resourceId, + string $filename, + array $columns, + array $queries, + bool $notify, + Document $user, + Response $response, + Database $dbForProject, + Database $dbForPlatform, + Authorization $authorization, + Document $project, + array $platform, + Event $queueForEvents, + Migration $queueForMigrations + ) { + try { + $parsedQueries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + $bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default')); + if ($bucket->isEmpty()) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + + [$databaseId, $collectionId] = \explode(':', $resourceId, 2); + if (empty($databaseId)) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + if (empty($collectionId)) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + if ($database->isEmpty()) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + $collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId)); + if ($collection->isEmpty()) { + throw new Exception(Exception::COLLECTION_NOT_FOUND); + } + + $databaseType = $database->getAttribute('type'); + + // Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields + $isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]); + + $validator = new Documents( + attributes: $collection->getAttribute('attributes', []), + indexes: $collection->getAttribute('indexes', []), + idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(), + supportForAttributes: !$isSchemaless, + ); + + if (!$validator->isValid($parsedQueries)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]); + $resourceType = getDatabaseResourceType($databaseType); + + $migration = $dbForProject->createDocument('migrations', new Document([ + '$id' => ID::unique(), + 'status' => 'pending', + 'stage' => 'init', + 'source' => Appwrite::getName(), + 'destination' => JSON::getName(), + 'resources' => $resources, + 'resourceId' => $resourceId, + 'resourceType' => $resourceType, + 'statusCounters' => '{}', + 'resourceData' => '{}', + 'errors' => [], + 'options' => [ + 'bucketId' => 'default', // Always use internal bucket + 'filename' => $filename, + 'columns' => $columns, + 'queries' => $queries, + 'notify' => $notify, + 'userInternalId' => $user->getSequence(), + ], + ])); + + $queueForEvents->setParam('migrationId', $migration->getId()); + + $queueForMigrations + ->setMigration($migration) + ->setProject($project) + ->setPlatform($platform) + ->trigger(); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($migration, Response::MODEL_MIGRATION); + }); + Http::get('/v1/migrations') ->groups(['api', 'migrations']) ->desc('List migrations') diff --git a/app/http.php b/app/http.php index 517a1aa544..1742bc7cdd 100644 --- a/app/http.php +++ b/app/http.php @@ -103,7 +103,7 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int $lines = explode("\n", $data, 3); $request = $lines[0]; if (count($lines) > 1) { - $domain = trim(explode('Host: ', $lines[1])[1]); + $domain = trim(explode('Host: ', $lines[1])[1] ?? ''); } // Sync executions are considered risky diff --git a/app/init/resources.php b/app/init/resources.php index 3481e73e0b..9f5b02fb91 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -888,8 +888,9 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori }, ['pools', 'cache', 'authorization']); Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) { + $initializedPools = []; - return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database { + return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization, &$initializedPools): Database { $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', '')); $databaseType = $database->getAttribute('type', ''); @@ -907,11 +908,12 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume $dsn = new DSN('mysql://' . $project->getAttribute('database')); } - $pool = $pools->get($databaseDSN->getHost()); + $databaseHost = $databaseDSN->getHost(); + $pool = $pools->get($databaseHost); $adapter = new DatabasePool($pool); $database = new Database($adapter, $cache); - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + $sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''))); $database ->setDatabase(APP_DATABASE) @@ -922,10 +924,30 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES); // inside pools authorization needs to be set first $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB); - if (\in_array($dsn->getHost(), $sharedTables)) { + + // When the database uses a separate pool (e.g. vectorsdb on PostgreSQL), + // always use dedicated mode with namespace isolation. Shared tables mode + // can't be used across different engines (e.g. MongoDB UUID tenants are + // incompatible with PostgreSQL's integer _tenant column). + if ($databaseHost !== $dsn->getHost()) { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); + + $poolKey = $databaseHost . ':' . $database->getNamespace(); + if (!isset($initializedPools[$poolKey])) { + try { + $database->create(); + } catch (\Utopia\Database\Exception\Duplicate) { + // Schema already exists + } + $initializedPools[$poolKey] = true; + } + } elseif (\in_array($dsn->getHost(), $sharedTables)) { $database ->setSharedTables(true) - ->setTenant((int)$project->getSequence()) + ->setTenant($project->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database diff --git a/app/views/install/installer.phtml b/app/views/install/installer.phtml index 05bc1b80ed..d33838c8c6 100644 --- a/app/views/install/installer.phtml +++ b/app/views/install/installer.phtml @@ -13,7 +13,7 @@ $enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql']; $isLocalInstall = $isLocalInstall ?? false; -$cardStep = min(4, $step); +$cardStep = ($step === 5) ? 4 : $step; $stepFile = __DIR__ . "/installer/templates/steps/step-{$cardStep}.phtml"; if (!is_file($stepFile)) { $stepFile = __DIR__ . "/installer/templates/steps/step-1.phtml"; diff --git a/app/views/install/installer/css/styles.css b/app/views/install/installer/css/styles.css index 7f253eed46..8fd28a12a3 100644 --- a/app/views/install/installer/css/styles.css +++ b/app/views/install/installer/css/styles.css @@ -478,6 +478,10 @@ body { overflow: hidden; } +.installer-page[data-upgrade='true'] .installer-step { + min-height: 0; +} + .action-shell { display: flex; flex-direction: column; @@ -1808,3 +1812,92 @@ body { gap: var(--gap-s); } } + +.migration-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--gap-l); + padding: var(--space-6); + background: var(--bgcolor-neutral-default); + border-radius: var(--border-radius-m); + outline: var(--border-width-s) solid var(--border-neutral); + outline-offset: calc(var(--border-width-s) * -1); + cursor: pointer; + transition: outline-color 0.15s ease-in-out; +} + +.migration-option:hover { + outline-color: var(--border-neutral-stronger); +} + +.migration-option-content { + display: flex; + flex-direction: column; + gap: 2px; +} + +.migration-switch { + flex-shrink: 0; +} + +.migration-switch-track { + position: relative; + display: block; + width: 32px; + height: 20px; + border-radius: 10px; + background: var(--bgcolor-neutral-invert-weaker); + transition: background 0.15s ease-in-out; +} + +.migration-switch-thumb { + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--bgcolor-neutral-primary); + transition: transform 0.15s ease-in-out; +} + +#run-migration:checked ~ .migration-switch-track { + background: var(--bgcolor-neutral-invert-weak); +} + +#run-migration:checked ~ .migration-switch-track .migration-switch-thumb { + transform: translateX(12px); +} + +#run-migration:focus-visible ~ .migration-switch-track { + box-shadow: 0 0 0 var(--border-width-l) var(--border-focus); +} + +.migration-hint { + display: flex; + align-items: flex-start; + gap: var(--gap-s); + padding: 0 var(--space-2); +} + +.migration-hint-icon { + flex-shrink: 0; + width: 16px; + height: 16px; + color: var(--fgcolor-neutral-tertiary); + margin-top: 1px; +} + +.migration-hint-icon svg { + width: 100%; + height: 100%; +} + +.migration-code { + padding: 1px 4px; + border-radius: var(--border-radius-xs, 4px); + background: var(--bgcolor-neutral-secondary); + font-family: monospace; + font-size: inherit; +} diff --git a/app/views/install/installer/js/installer.js b/app/views/install/installer/js/installer.js index 463b7f6221..07ec7bb1ef 100644 --- a/app/views/install/installer/js/installer.js +++ b/app/views/install/installer/js/installer.js @@ -12,7 +12,7 @@ const { validateInstallRequest } = window.InstallerStepsProgress || {}; const isUpgrade = document.body?.dataset.upgrade === 'true'; - const stepFlow = isUpgrade ? [1, 4, 5] : [1, 2, 3, 4, 5]; + const stepFlow = isUpgrade ? [1, 6, 4, 5] : [1, 2, 3, 4, 5]; const cardSteps = stepFlow.filter((step) => step !== 5); const normalizeStep = (step) => { @@ -53,7 +53,7 @@ let pendingStep = null; let pendingPushState = false; - const clampStep = (step) => Math.max(1, Math.min(5, step)); + const clampStep = (step) => Math.max(1, Math.min(6, step)); const isInstallLocked = () => Boolean(window.InstallerSteps?.isInstallLocked?.()); const scrollToFirstError = (panel) => { @@ -399,11 +399,18 @@ } } } - if (action === 'next' && String(target) === '5' && typeof validateInstallRequest === 'function') { - const isValid = await validateInstallRequest(); - if (!isValid) { - return; + if (action === 'next' && String(target) === '5') { + if (typeof validateInstallRequest === 'function') { + const isValid = await validateInstallRequest(); + if (!isValid) { + return; + } } + // Clear stale install data from previous runs so initStep5 + // starts a fresh install instead of trying to resume. + const { clearInstallLock, clearInstallId } = window.InstallerStepsState || {}; + clearInstallLock?.(); + clearInstallId?.(); } if (isInstallLocked() && Number(target) !== 5) { requestStep(5, true); diff --git a/app/views/install/installer/js/modules/context.js b/app/views/install/installer/js/modules/context.js index 4917a1bfe9..6f215da899 100644 --- a/app/views/install/installer/js/modules/context.js +++ b/app/views/install/installer/js/modules/context.js @@ -14,6 +14,7 @@ ENV_VARS: 'env-vars', DOCKER_CONTAINERS: 'docker-containers', ACCOUNT_SETUP: 'account-setup', + MIGRATION: 'migration', SSL_CERTIFICATE: 'ssl-certificate', REDIRECT: 'redirect' }); @@ -52,6 +53,11 @@ id: STEP_IDS.DOCKER_CONTAINERS, inProgress: 'Restarting Docker containers...', done: 'Docker containers restarted' + }, + { + id: STEP_IDS.MIGRATION, + inProgress: 'Running database migration...', + done: 'Database migration completed' } ] : [ { @@ -95,7 +101,7 @@ const clampStep = (step) => { const numeric = Number(step); if (Number.isNaN(numeric)) return 1; - return Math.max(1, Math.min(5, numeric)); + return Math.max(1, Math.min(6, numeric)); }; window.InstallerStepsContext = Object.freeze({ diff --git a/app/views/install/installer/js/modules/progress.js b/app/views/install/installer/js/modules/progress.js index d066908b03..7c36fd7951 100644 --- a/app/views/install/installer/js/modules/progress.js +++ b/app/views/install/installer/js/modules/progress.js @@ -373,7 +373,8 @@ opensslKey: (formState?.opensslKey || '').trim(), assistantOpenAIKey: normalizedAssistantKey, accountEmail: normalizedAccountEmail, - accountPassword: normalizedAccountPassword + accountPassword: normalizedAccountPassword, + migrate: formState?.migrate ?? false }; }; @@ -721,7 +722,8 @@ }); startSyncedSpinnerRotation(list); - notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => { + const completeId = activeInstall?.installId || getStoredInstallId?.(); + notifyInstallComplete(completeId, sessionDetails).finally(() => { setTimeout(() => redirectToApp(protocol), TIMINGS?.redirectDelay ?? 0); }); }; @@ -911,21 +913,28 @@ }; const isSnapshotTerminal = (snapshot) => { - if (!snapshot?.steps) return true; + if (!snapshot?.steps) return 'empty'; const stepEntries = Object.values(snapshot.steps); - if (stepEntries.length === 0) return true; + if (stepEntries.length === 0) return 'empty'; const hasError = stepEntries.some((s) => s.status === STATUS.ERROR); - if (hasError) return true; + if (hasError) return 'error'; const allCompleted = INSTALLATION_STEPS.every((step) => { const detail = snapshot.steps[step.id]; return detail && detail.status === STATUS.COMPLETED; }); - return allCompleted; + if (allCompleted) return 'completed'; + return false; }; const resumeInstall = async (installId) => { const snapshot = await fetchInstallStatus(installId); - if (!snapshot || isSnapshotTerminal(snapshot)) return false; + const terminal = isSnapshotTerminal(snapshot); + if (!snapshot || terminal) { + if (terminal === 'completed') { + return 'completed'; + } + return false; + } activeInstall = { installId, controller: new AbortController(), @@ -1069,14 +1078,33 @@ startInstallStream(newInstallId); }; + const recoverToLastStep = () => { + clearInstallId?.(); + clearInstallLock?.(); + const url = new URL(window.location.href); + const lastStep = url.searchParams.get('step'); + // Stay on the current URL so the user keeps their place; + // only navigate away if we're already on step 5 (the + // progress screen) since there's nothing to show. + if (!lastStep || String(lastStep) === '5') { + window.location.href = '/?step=1'; + } + }; + const lock = getInstallLock?.(); const existingInstallId = lock?.installId || getStoredInstallId?.(); if (existingInstallId) { - resumeInstall(existingInstallId).then((resumed) => { - if (!resumed) { - clearInstallId?.(); + resumeInstall(existingInstallId).then((result) => { + if (result === 'completed') { + // Install already finished — redirect to console + // instead of bouncing back to step 1. + stopSyncedSpinnerRotation(); + setUnloadGuard(false); clearInstallLock?.(); - window.location.href = '/?step=1'; + clearInstallId?.(); + startSslCheck(null); + } else if (!result) { + recoverToLastStep(); } }); } else { diff --git a/app/views/install/installer/js/steps.js b/app/views/install/installer/js/steps.js index c9430b7afd..b34389b561 100644 --- a/app/views/install/installer/js/steps.js +++ b/app/views/install/installer/js/steps.js @@ -329,6 +329,30 @@ } }; + const initStep6 = (root) => { + if (!root) return; + syncInstallLockFlag?.(); + applyLockPayload?.(); + applyBodyDefaults?.(); + + const checkbox = root.querySelector('#run-migration'); + if (checkbox) { + if (formState.migrate !== undefined) { + checkbox.checked = formState.migrate; + } else { + formState.migrate = checkbox.checked; + } + checkbox.addEventListener('change', () => { + formState.migrate = checkbox.checked; + dispatchStateChange?.('migrate'); + }); + } + + if (isInstallLocked?.()) { + disableControls?.(root); + } + }; + const initStep = (step, container) => { if (!container) return; const root = container.querySelector('.step-layout') || container; @@ -346,6 +370,7 @@ if (normalized === 3) initStep3(root); if (normalized === 4) initStep4(root); if (normalized === 5) Progress.initStep5?.(root); + if (normalized === 6) initStep6(root); }; window.InstallerSteps = { diff --git a/app/views/install/installer/templates/steps/step-4.phtml b/app/views/install/installer/templates/steps/step-4.phtml index 07dc865257..8468de30f4 100644 --- a/app/views/install/installer/templates/steps/step-4.phtml +++ b/app/views/install/installer/templates/steps/step-4.phtml @@ -62,12 +62,14 @@ $badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning'; Disabled
Appwrite Assistant
+
Secret API key
+ diff --git a/app/views/install/installer/templates/steps/step-5.phtml b/app/views/install/installer/templates/steps/step-5.phtml index cd5de5f4ab..c18c3ea748 100644 --- a/app/views/install/installer/templates/steps/step-5.phtml +++ b/app/views/install/installer/templates/steps/step-5.phtml @@ -6,7 +6,7 @@ $isUpgrade = $isUpgrade ?? false;
- +
diff --git a/app/views/install/installer/templates/steps/step-6.phtml b/app/views/install/installer/templates/steps/step-6.phtml new file mode 100644 index 0000000000..9a8838ae3a --- /dev/null +++ b/app/views/install/installer/templates/steps/step-6.phtml @@ -0,0 +1,37 @@ + +
+
+
+

Database migration

+

+ Run database migration after the update to apply schema changes. +

+
+ +
+ + +
+ + + + + To run manually later: docker compose exec appwrite migrate + +
+
+
+
diff --git a/app/worker.php b/app/worker.php index 71446ee94f..e54819e729 100644 --- a/app/worker.php +++ b/app/worker.php @@ -221,7 +221,9 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza }, ['pools', 'cache', 'authorization']); Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) { - return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database { + $initializedPools = []; + + return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization, &$initializedPools): Database { $projectDocument ??= $project; $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', '')); $databaseType = $database->getAttribute('type', ''); @@ -246,7 +248,8 @@ Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register } $pools = $register->get('pools'); - $pool = $pools->get($databaseDSN->getHost()); + $databaseHost = $databaseDSN->getHost(); + $pool = $pools->get($databaseHost); $adapter = new DatabasePool($pool); $database = new Database($adapter, $cache); @@ -255,12 +258,30 @@ Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register ->setAuthorization($authorization); $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB); - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); + $sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''))); - if (\in_array($dsn->getHost(), $sharedTables, true)) { + // When using a separate pool, always use dedicated mode with namespace isolation. + // Shared tables mode can't be used across different engines (e.g. MongoDB UUID + // tenants are incompatible with PostgreSQL's integer _tenant column). + if ($databaseHost !== $dsn->getHost()) { + $database + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $projectDocument->getSequence()); + + $poolKey = $databaseHost . ':' . $database->getNamespace(); + if (!isset($initializedPools[$poolKey])) { + try { + $database->create(); + } catch (\Utopia\Database\Exception\Duplicate) { + // Schema already exists + } + $initializedPools[$poolKey] = true; + } + } elseif (\in_array($dsn->getHost(), $sharedTables, true)) { $database ->setSharedTables(true) - ->setTenant((int) $projectDocument->getSequence()) + ->setTenant($projectDocument->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database diff --git a/composer.json b/composer.json index 65838a1615..1a530bfc5b 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,7 @@ "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", "utopia-php/messaging": "0.20.*", - "utopia-php/migration": "1.8.*", + "utopia-php/migration": "1.9.*", "utopia-php/platform": "0.7.*", "utopia-php/pools": "1.*", "utopia-php/span": "1.1.*", diff --git a/composer.lock b/composer.lock index 1441bf06d1..f0df72f6ff 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f9225f2b580de0ccb796b2fb8c881384", + "content-hash": "b5261855586680e467168f527e0634ae", "packages": [ { "name": "adhocore/jwt", @@ -161,16 +161,16 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.19.4", + "version": "0.19.5", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b" + "reference": "aa2f7760cd0493c0880209b92df812c9386b3546" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/runtimes/zipball/eea9d1b3ca2540eab623b419c8afde09ef406c0b", - "reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b", + "url": "https://api.github.com/repos/appwrite/runtimes/zipball/aa2f7760cd0493c0880209b92df812c9386b3546", + "reference": "aa2f7760cd0493c0880209b92df812c9386b3546", "shasum": "" }, "require": { @@ -210,9 +210,9 @@ ], "support": { "issues": "https://github.com/appwrite/runtimes/issues", - "source": "https://github.com/appwrite/runtimes/tree/0.19.4" + "source": "https://github.com/appwrite/runtimes/tree/0.19.5" }, - "time": "2026-02-17T10:04:39+00:00" + "time": "2026-04-01T01:39:23+00:00" }, { "name": "brick/math", @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.17", + "version": "5.3.19", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993" + "reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993", - "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993", + "url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691", + "reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691", "shasum": "" }, "require": { @@ -3903,9 +3903,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.17" + "source": "https://github.com/utopia-php/database/tree/5.3.19" }, - "time": "2026-03-20T01:18:52+00:00" + "time": "2026-03-31T15:52:08+00:00" }, { "name": "utopia-php/detector", @@ -4518,16 +4518,16 @@ }, { "name": "utopia-php/migration", - "version": "1.8.3", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "8633523b3343d492427331b6eec53f020f6ab7a7" + "reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/8633523b3343d492427331b6eec53f020f6ab7a7", - "reference": "8633523b3343d492427331b6eec53f020f6ab7a7", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/7a86aeadf182b63a9f4ceba7e137588b31c5d2e2", + "reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2", "shasum": "" }, "require": { @@ -4567,9 +4567,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.8.3" + "source": "https://github.com/utopia-php/migration/tree/1.9.1" }, - "time": "2026-03-19T09:18:47+00:00" + "time": "2026-03-25T07:05:27+00:00" }, { "name": "utopia-php/mongo", diff --git a/docs/references/health/get-queue-audits.md b/docs/references/health/get-queue-audits.md index 75010cc2f4..bac075581f 100644 --- a/docs/references/health/get-queue-audits.md +++ b/docs/references/health/get-queue-audits.md @@ -1 +1 @@ -Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server. \ No newline at end of file +Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 37e2579644..5da64a1c97 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -186,108 +186,6 @@ parameters: count: 3 path: app/worker.php - - - message: '#^PHPDoc tag @return with type string is incompatible with native type int\.$#' - identifier: return.phpDocType - count: 1 - path: src/Appwrite/Auth/OAuth2.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$token$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Auth/OAuth2/Disqus.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$value$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Auth/Validator/PersonalData.php - - - - message: '#^PHPDoc tag @param has invalid value \(DeviceDetector\)\: Unexpected token "\\n ", expected variable at offset 32 on line 2$#' - identifier: phpDoc.parseError - count: 1 - path: src/Appwrite/Detector/Detector.php - - - - message: '#^PHPDoc tag @param has invalid value \(string\)\: Unexpected token "\\n ", expected variable at offset 24 on line 2$#' - identifier: phpDoc.parseError - count: 1 - path: src/Appwrite/Detector/Detector.php - - - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced - count: 1 - path: src/Appwrite/Docker/Compose.php - - - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced - count: 1 - path: src/Appwrite/Docker/Compose/Service.php - - - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced - count: 1 - path: src/Appwrite/Docker/Env.php - - - - message: '#^PHPDoc tag @param has invalid value \(int port\)\: Unexpected token "port", expected variable at offset 50 on line 4$#' - identifier: phpDoc.parseError - count: 1 - path: src/Appwrite/Event/Mail.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$password$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Event/Mail.php - - - - message: '#^PHPDoc tag @return with type string is incompatible with native type Appwrite\\Event\\Mail\.$#' - identifier: return.phpDocType - count: 1 - path: src/Appwrite/Event/Mail.php - - - - message: '#^Method Appwrite\\Event\\Message\\Usage\:\:fromArray\(\) should return static\(Appwrite\\Event\\Message\\Usage\) but returns Appwrite\\Event\\Message\\Usage\.$#' - identifier: return.type - count: 1 - path: src/Appwrite/Event/Message/Usage.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$message$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Event/Messaging.php - - - - message: '#^PHPDoc tag @return with type string is incompatible with native type Utopia\\Database\\Document\.$#' - identifier: return.phpDocType - count: 1 - path: src/Appwrite/Event/Messaging.php - - - - message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getFunctionsEvents\(\) should return array\ but returns array\\>\.$#' - identifier: return.type - count: 1 - path: src/Appwrite/Functions/EventProcessor.php - - - - message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getWebhooksEvents\(\) should return array\ but returns array\\>\.$#' - identifier: return.type - count: 1 - path: src/Appwrite/Functions/EventProcessor.php - - - - message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 1 - path: src/Appwrite/Functions/EventProcessor.php - - message: '#^Anonymous function has an unused use \$context\.$#' identifier: closure.unusedUse @@ -360,30 +258,6 @@ parameters: count: 1 path: src/Appwrite/GraphQL/Types/Mapper.php - - - message: '#^Unsafe access to private property Appwrite\\GraphQL\\Types\\Mapper\:\:\$models through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 3 - path: src/Appwrite/GraphQL/Types/Mapper.php - - - - message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getColumnImplementation\(\) through static\:\:\.$#' - identifier: staticClassAccess.privateMethod - count: 2 - path: src/Appwrite/GraphQL/Types/Mapper.php - - - - message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getHashOptionsImplementation\(\) through static\:\:\.$#' - identifier: staticClassAccess.privateMethod - count: 1 - path: src/Appwrite/GraphQL/Types/Mapper.php - - - - message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getUnionImplementation\(\) through static\:\:\.$#' - identifier: staticClassAccess.privateMethod - count: 1 - path: src/Appwrite/GraphQL/Types/Mapper.php - - message: '#^Call to an undefined method Appwrite\\Migration\\Version\\V15\:\:documentsIterator\(\)\.$#' identifier: method.notFound @@ -450,12 +324,6 @@ parameters: count: 1 path: src/Appwrite/OpenSSL/OpenSSL.php - - - message: '#^PHPDoc tag @param references unknown parameter\: \$projectId$#' - identifier: parameter.notFound - count: 1 - path: src/Appwrite/Platform/Action.php - - message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#' identifier: empty.variable @@ -1152,12 +1020,6 @@ parameters: count: 1 path: src/Appwrite/SDK/Specification/Format/Swagger2.php - - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced - count: 2 - path: src/Appwrite/Template/Template.php - - message: '#^PHPDoc tag @param has invalid value \(Document \$this\)\: Unexpected token "\$this", expected variable at offset 69 on line 4$#' identifier: phpDoc.parseError @@ -1170,35 +1032,6 @@ parameters: count: 1 path: src/Appwrite/Utopia/Database/Documents/User.php - - - message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:appendSymbol\(\) through static\:\:\.$#' - identifier: staticClassAccess.privateMethod - count: 4 - path: src/Appwrite/Utopia/Request/Filters/V17.php - - - - message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:isSpecialChar\(\) through static\:\:\.$#' - identifier: staticClassAccess.privateMethod - count: 1 - path: src/Appwrite/Utopia/Request/Filters/V17.php - - - - message: '#^PHPDoc tag @param has invalid value \(callable The callback to show sensitive information for\)\: Unexpected token "The", expected variable at offset 91 on line 4$#' - identifier: phpDoc.parseError - count: 1 - path: src/Appwrite/Utopia/Response.php - - - - message: '#^PHPDoc tag @return with type Appwrite\\Utopia\\Response\\Filter is incompatible with native type array\.$#' - identifier: return.phpDocType - count: 1 - path: src/Appwrite/Utopia/Response.php - - - - message: '#^PHPDoc tag @return with type string is incompatible with native type Utopia\\Database\\Document\.$#' - identifier: return.phpDocType - count: 1 - path: src/Appwrite/Utopia/Response/Model/User.php - message: '#^Unsafe access to private property Tests\\E2E\\Services\\Databases\\Legacy\\DatabasesStringTypesTest\:\:\$setupCache through static\:\:\.$#' identifier: staticClassAccess.privateProperty @@ -1234,108 +1067,12 @@ parameters: count: 8 path: tests/e2e/Services/Functions/FunctionsCustomServerTest.php - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedDeployment through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 3 - path: tests/e2e/Services/GraphQL/FunctionsClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedExecution through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/FunctionsClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedFunction through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 3 - path: tests/e2e/Services/GraphQL/FunctionsClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedDeployment through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/FunctionsServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedExecution through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/FunctionsServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedFunction through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/FunctionsServerTest.php - - message: '#^Binary operation "\+" between string and 1 results in an error\.$#' identifier: binaryOp.invalid count: 1 path: tests/e2e/Services/GraphQL/Legacy/AbuseTest.php - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$bulkData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$collection through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$database through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$document through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedEmail through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 2 - path: tests/e2e/Services/GraphQL/MessagingTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedProviders through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 8 - path: tests/e2e/Services/GraphQL/MessagingTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedPush through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/MessagingTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedSms through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/MessagingTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedSubscriber through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/MessagingTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedTopic through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 9 - path: tests/e2e/Services/GraphQL/MessagingTest.php - - message: '#^Variable \$from in empty\(\) is never defined\.$#' identifier: empty.variable @@ -1348,174 +1085,18 @@ parameters: count: 1 path: tests/e2e/Services/GraphQL/StorageClientTest.php - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageClientTest\:\:\$cachedBucket through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 3 - path: tests/e2e/Services/GraphQL/StorageClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageClientTest\:\:\$cachedFile through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/StorageClientTest.php - - message: '#^Method Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:testGetFileDownload\(\) should return array but return statement is missing\.$#' identifier: return.missing count: 1 path: tests/e2e/Services/GraphQL/StorageServerTest.php - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:\$cachedBucket through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/StorageServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:\$cachedFile through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 6 - path: tests/e2e/Services/GraphQL/StorageServerTest.php - - message: '#^Binary operation "\+" between string and 1 results in an error\.$#' identifier: binaryOp.invalid count: 1 path: tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedBooleanColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedBulkData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedDatabase through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedDatetimeColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedEmailColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedEnumColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedFloatColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIPColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIndexData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 7 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIntegerColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedRelationshipColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedRowData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedStringColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedTableData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedURLColumnData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsClientTest\:\:\$cachedMembership through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/TeamsClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsClientTest\:\:\$cachedTeam through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 3 - path: tests/e2e/Services/GraphQL/TeamsClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedMembership through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/GraphQL/TeamsServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedTeam through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 3 - path: tests/e2e/Services/GraphQL/TeamsServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedTeamWithPrefs through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 3 - path: tests/e2e/Services/GraphQL/TeamsServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\UsersTest\:\:\$cachedUser through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 3 - path: tests/e2e/Services/GraphQL/UsersTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\UsersTest\:\:\$cachedUserTarget through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 5 - path: tests/e2e/Services/GraphQL/UsersTest.php - - message: '#^Variable \$from in empty\(\) is never defined\.$#' identifier: empty.variable @@ -1537,13 +1118,13 @@ parameters: - message: '#^Anonymous function has an unused use \$databaseId\.$#' identifier: closure.unusedUse - count: 5 + count: 6 path: tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php - message: '#^Anonymous function has an unused use \$tableId\.$#' identifier: closure.unusedUse - count: 5 + count: 6 path: tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php - @@ -1587,36 +1168,6 @@ parameters: identifier: method.notFound count: 1 path: tests/e2e/Services/TablesDB/Permissions/TablesDBPermissionsTeamTest.php - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$bucketAndFileData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/Tokens/TokensConsoleClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$tokenData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/Tokens/TokensConsoleClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomClientTest\:\:\$bucketAndFileData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/Tokens/TokensCustomClientTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomServerTest\:\:\$bucketAndFileData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/Tokens/TokensCustomServerTest.php - - - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomServerTest\:\:\$tokenData through static\:\:\.$#' - identifier: staticClassAccess.privateProperty - count: 4 - path: tests/e2e/Services/Tokens/TokensCustomServerTest.php - - message: '#^Unsafe access to private property Tests\\E2E\\Services\\Users\\UsersCustomServerTest\:\:\$cachedHashedPasswordUsers through static\:\:\.$#' identifier: staticClassAccess.privateProperty @@ -1676,75 +1227,3 @@ parameters: identifier: method.notFound count: 1 path: tests/unit/Event/EventTest.php - - - - message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#' - identifier: class.notFound - count: 6 - path: tests/unit/Utopia/Response/Filters/V16Test.php - - - - message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V16Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V16\.$#' - identifier: assign.propertyType - count: 1 - path: tests/unit/Utopia/Response/Filters/V16Test.php - - - - message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V16Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#' - identifier: class.notFound - count: 1 - path: tests/unit/Utopia/Response/Filters/V16Test.php - - - - message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#' - identifier: class.notFound - count: 5 - path: tests/unit/Utopia/Response/Filters/V17Test.php - - - - message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V17Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V17\.$#' - identifier: assign.propertyType - count: 1 - path: tests/unit/Utopia/Response/Filters/V17Test.php - - - - message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V17Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#' - identifier: class.notFound - count: 1 - path: tests/unit/Utopia/Response/Filters/V17Test.php - - - - message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#' - identifier: class.notFound - count: 4 - path: tests/unit/Utopia/Response/Filters/V18Test.php - - - - message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V18Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V18\.$#' - identifier: assign.propertyType - count: 1 - path: tests/unit/Utopia/Response/Filters/V18Test.php - - - - message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V18Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#' - identifier: class.notFound - count: 1 - path: tests/unit/Utopia/Response/Filters/V18Test.php - - - - message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#' - identifier: class.notFound - count: 11 - path: tests/unit/Utopia/Response/Filters/V19Test.php - - - - message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V19Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V19\.$#' - identifier: assign.propertyType - count: 1 - path: tests/unit/Utopia/Response/Filters/V19Test.php - - - - message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V19Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#' - identifier: class.notFound - count: 1 - path: tests/unit/Utopia/Response/Filters/V19Test.php diff --git a/src/Appwrite/Auth/OAuth2.php b/src/Appwrite/Auth/OAuth2.php index 9358c89547..a8a2d175b5 100644 --- a/src/Appwrite/Auth/OAuth2.php +++ b/src/Appwrite/Auth/OAuth2.php @@ -155,7 +155,7 @@ abstract class OAuth2 /** * @param string $code * - * @return string + * @return int */ public function getAccessTokenExpiry(string $code): int { diff --git a/src/Appwrite/Auth/OAuth2/Disqus.php b/src/Appwrite/Auth/OAuth2/Disqus.php index 58b7f48914..738c6d503e 100644 --- a/src/Appwrite/Auth/OAuth2/Disqus.php +++ b/src/Appwrite/Auth/OAuth2/Disqus.php @@ -108,7 +108,7 @@ class Disqus extends OAuth2 } /** - * @param string $token + * @param string $accessToken * * @return string */ diff --git a/src/Appwrite/Auth/Validator/PersonalData.php b/src/Appwrite/Auth/Validator/PersonalData.php index 8eaae002f6..3b09839bd1 100644 --- a/src/Appwrite/Auth/Validator/PersonalData.php +++ b/src/Appwrite/Auth/Validator/PersonalData.php @@ -33,7 +33,7 @@ class PersonalData extends Password /** * Is valid. * - * @param mixed $value + * @param mixed $password * * @return bool */ diff --git a/src/Appwrite/Detector/Detector.php b/src/Appwrite/Detector/Detector.php index 61286835f5..73259673dd 100644 --- a/src/Appwrite/Detector/Detector.php +++ b/src/Appwrite/Detector/Detector.php @@ -6,14 +6,8 @@ use DeviceDetector\DeviceDetector; class Detector { - /** - * @param string - */ protected $userAgent = ''; - /** - * @param DeviceDetector - */ protected $detctor; /** diff --git a/src/Appwrite/Docker/Compose.php b/src/Appwrite/Docker/Compose.php index 241e281ed8..9ea6420d2d 100644 --- a/src/Appwrite/Docker/Compose.php +++ b/src/Appwrite/Docker/Compose.php @@ -12,9 +12,6 @@ class Compose */ protected $compose = []; - /** - * @var string $data - */ public function __construct(string $data) { $this->compose = yaml_parse($data); diff --git a/src/Appwrite/Docker/Compose/Service.php b/src/Appwrite/Docker/Compose/Service.php index a3f9c91253..87699aaeba 100644 --- a/src/Appwrite/Docker/Compose/Service.php +++ b/src/Appwrite/Docker/Compose/Service.php @@ -11,9 +11,6 @@ class Service */ protected $service = []; - /** - * @var string $path - */ public function __construct(array $service) { $this->service = $service; diff --git a/src/Appwrite/Docker/Env.php b/src/Appwrite/Docker/Env.php index 3bf6fb2d50..af5e4f11e2 100644 --- a/src/Appwrite/Docker/Env.php +++ b/src/Appwrite/Docker/Env.php @@ -9,9 +9,6 @@ class Env */ protected $vars = []; - /** - * @var string $data - */ public function __construct(string $data) { $data = explode("\n", $data); diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php index 2d12aa542c..38d7a27c11 100644 --- a/src/Appwrite/Event/Mail.php +++ b/src/Appwrite/Event/Mail.php @@ -101,7 +101,8 @@ class Mail extends Event /** * Sets preview for the mail event. * - * @return string + * @param string $preview + * @return self */ public function setPreview(string $preview): self { @@ -115,7 +116,7 @@ class Mail extends Event * * @return string */ - public function getPreview(string $preview): string + public function getPreview(): string { return $this->preview; } @@ -181,7 +182,7 @@ class Mail extends Event /** * Set SMTP port * - * @param int port + * @param int $port * @return self */ public function setSmtpPort(int $port): self @@ -217,7 +218,7 @@ class Mail extends Event /** * Set SMTP secure * - * @param string $password + * @param string $secure * @return self */ public function setSmtpSecure(string $secure): self diff --git a/src/Appwrite/Event/Message/Usage.php b/src/Appwrite/Event/Message/Usage.php index 776188d5b5..c97b96a855 100644 --- a/src/Appwrite/Event/Message/Usage.php +++ b/src/Appwrite/Event/Message/Usage.php @@ -40,7 +40,7 @@ class Usage extends Base */ public static function fromArray(array $data): static { - return new self( + return new static( project: new Document($data['project'] ?? []), metrics: $data['metrics'] ?? [], reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []), diff --git a/src/Appwrite/Event/Messaging.php b/src/Appwrite/Event/Messaging.php index 8c13185e0b..9895d52ec2 100644 --- a/src/Appwrite/Event/Messaging.php +++ b/src/Appwrite/Event/Messaging.php @@ -86,7 +86,7 @@ class Messaging extends Event /** * Returns message document for the messaging event. * - * @return string + * @return Document */ public function getMessage(): Document { @@ -96,7 +96,7 @@ class Messaging extends Event /** * Sets message ID for the messaging event. * - * @param string $message + * @param string $messageId * @return self */ public function setMessageId(string $messageId): self diff --git a/src/Appwrite/Functions/EventProcessor.php b/src/Appwrite/Functions/EventProcessor.php index e9c3b7241a..d41ee56c5d 100644 --- a/src/Appwrite/Functions/EventProcessor.php +++ b/src/Appwrite/Functions/EventProcessor.php @@ -8,6 +8,18 @@ use Utopia\Database\Query; class EventProcessor { + /** + * @param array $events + * @return array + */ + private function getEventMap(array $events): array + { + return \array_fill_keys( + \array_map('strval', \array_unique($events)), + true + ); + } + /** * Get function events for a project, using Redis cache * @param Document|null $project @@ -26,7 +38,7 @@ class EventProcessor $cacheKey = \sprintf( '%s-cache-%s:%s:%s:project:%s:functions:events', $dbForProject->getCacheName(), - $hostname ?? '', + $hostname, $dbForProject->getNamespace(), $dbForProject->getTenant(), $project->getId() @@ -36,7 +48,9 @@ class EventProcessor $cachedFunctionEvents = $dbForProject->getCache()->load($cacheKey, $ttl); if ($cachedFunctionEvents !== false) { - return \json_decode($cachedFunctionEvents, true) ?? []; + $decoded = \json_decode($cachedFunctionEvents, true); + + return \is_array($decoded) ? $this->getEventMap(\array_keys($decoded)) : []; } $events = []; @@ -63,7 +77,7 @@ class EventProcessor } } - $uniqueEvents = \array_flip(\array_unique($events)); + $uniqueEvents = $this->getEventMap($events); $dbForProject->getCache()->save($cacheKey, \json_encode($uniqueEvents)); return $uniqueEvents; @@ -97,6 +111,6 @@ class EventProcessor } } - return \array_flip(\array_unique($events)); + return $this->getEventMap($events); } } diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index 037f80bcf7..de4913cec4 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -101,16 +101,16 @@ class Mapper if (\is_array($modelName)) { foreach ($modelName as $name) { - $models[] = static::$models[$name]; + $models[] = self::$models[$name]; } } else { - $models[] = static::$models[$modelName]; + $models[] = self::$models[$modelName]; } } } else { // If single response, get its model and wrap in array $modelName = $responses->getModel(); - $models = [static::$models[$modelName]]; + $models = [self::$models[$modelName]]; } foreach ($models as $model) { @@ -425,7 +425,7 @@ class Mapper 'name' => $unionName, 'types' => $types, 'resolveType' => static function ($object) use ($unionName) { - return static::getUnionImplementation($unionName, $object); + return self::getUnionImplementation($unionName, $object); }, ]); @@ -440,11 +440,11 @@ class Mapper switch ($name) { case 'Attributes': - return static::getColumnImplementation($object); + return self::getColumnImplementation($object); case 'Columns': - return static::getColumnImplementation($object, true); + return self::getColumnImplementation($object, true); case 'HashOptions': - return static::getHashOptionsImplementation($object); + return self::getHashOptionsImplementation($object); } throw new Exception('Unknown union type: ' . $name); diff --git a/src/Appwrite/Migration/Version/V24.php b/src/Appwrite/Migration/Version/V24.php index dc9ce9d196..a2d9d7907b 100644 --- a/src/Appwrite/Migration/Version/V24.php +++ b/src/Appwrite/Migration/Version/V24.php @@ -187,6 +187,20 @@ class V24 extends Migration $this->dbForProject->purgeCachedCollection($id); break; + case 'users': + try { + $this->createAttributeFromCollection($this->dbForProject, $id, 'impersonator'); + } catch (Throwable $th) { + Console::warning("Failed to create attribute \"impersonator\" in collection {$id}: {$th->getMessage()}"); + } + try { + $this->createIndexFromCollection($this->dbForProject, $id, 'impersonator'); + } catch (Throwable $th) { + Console::warning("Failed to create index \"impersonator\" from {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + break; + case 'teams': try { $this->createAttributeFromCollection($this->dbForProject, $id, 'labels'); diff --git a/src/Appwrite/Platform/Action.php b/src/Appwrite/Platform/Action.php index 01ac92a45c..0aa0da7149 100644 --- a/src/Appwrite/Platform/Action.php +++ b/src/Appwrite/Platform/Action.php @@ -37,7 +37,7 @@ class Action extends UtopiaAction * Foreach Document * Call provided callback for each document in the collection * - * @param string $projectId + * @param Database $database * @param string $collection * @param array $queries * @param callable $callback diff --git a/src/Appwrite/Platform/Installer/Http/Installer/Install.php b/src/Appwrite/Platform/Installer/Http/Installer/Install.php index e29222a703..8aaaf621bb 100644 --- a/src/Appwrite/Platform/Installer/Http/Installer/Install.php +++ b/src/Appwrite/Platform/Installer/Http/Installer/Install.php @@ -43,6 +43,7 @@ class Install extends Action ->param('database', '', new WhiteList(['mongodb', 'mariadb', 'postgresql']), 'Database adapter', true) ->param('installId', '', new Text(64, 0), 'Installation ID', true) ->param('retryStep', null, new Nullable(new WhiteList([Server::STEP_DOCKER_COMPOSE, Server::STEP_ENV_VARS, Server::STEP_DOCKER_CONTAINERS], true)), 'Retry from step', true) + ->param('migrate', false, new \Utopia\Validator\Boolean(true), 'Run database migration after upgrade', true) ->inject('request') ->inject('response') ->inject('swooleResponse') @@ -64,6 +65,7 @@ class Install extends Action string $database, string $installId, ?string $retryStep, + bool $migrate, Request $request, Response $response, SwooleResponse $swooleResponse, @@ -321,6 +323,28 @@ class Install extends Action } }; + $responseSent = false; + $onComplete = function () use ($wantsStream, $swooleResponse, $response, $installId, $state, &$responseSent) { + if ($responseSent) { + return; + } + $responseSent = true; + $state->updateGlobalLock($installId, Server::STATUS_COMPLETED); + if ($wantsStream) { + $this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]); + usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS); + $swooleResponse->write(": keepalive\n\n"); + usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS); + $swooleResponse->end(); + } else { + $response->json([ + 'success' => true, + 'installId' => $installId, + 'message' => 'Installation completed successfully', + ]); + } + }; + $installer->performInstallation( $httpPort ?: $config->getDefaultHttpPort(), $httpsPort ?: $config->getDefaultHttpsPort(), @@ -331,23 +355,12 @@ class Install extends Action $progress, $retryStep, $config->isUpgrade(), - $account + $account, + $onComplete, + $migrate, ); - if ($wantsStream) { - $this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]); - usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS); - $swooleResponse->write(": keepalive\n\n"); - usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS); - $swooleResponse->end(); - } else { - $response->json([ - 'success' => true, - 'installId' => $installId, - 'message' => 'Installation completed successfully', - ]); - } - $state->updateGlobalLock($installId, Server::STATUS_COMPLETED); + $onComplete(); } catch (\Throwable $e) { $this->handleInstallationError($e, $installId, $wantsStream, $response, $swooleResponse, $state); } diff --git a/src/Appwrite/Platform/Installer/Http/Installer/View.php b/src/Appwrite/Platform/Installer/Http/Installer/View.php index ce308aa906..dea356eaaf 100644 --- a/src/Appwrite/Platform/Installer/Http/Installer/View.php +++ b/src/Appwrite/Platform/Installer/Http/Installer/View.php @@ -24,7 +24,7 @@ class View extends Action ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/') ->desc('Serve installer UI') - ->param('step', 1, new Integer(true), 'Step number (1-5)', true) + ->param('step', 1, new Integer(true), 'Step number (1-6)', true) ->param('partial', null, new Nullable(new Text(1, 0)), 'Render partial step only', true) ->inject('request') ->inject('response') @@ -52,10 +52,13 @@ class View extends Action $defaultEmailCertificates = 'walterobrien@example.com'; } - $step = max(1, min(5, $step)); + $step = max(1, min(6, $step)); if ($isUpgrade && ($step === 2 || $step === 3)) { $step = 4; } + if (!$isUpgrade && $step === 6) { + $step = 4; + } $partialFile = $paths['views'] . "/installer/templates/steps/step-{$step}.phtml"; if (!is_file($partialFile)) { diff --git a/src/Appwrite/Platform/Installer/Server.php b/src/Appwrite/Platform/Installer/Server.php index 17edfaac72..6d9cd5412f 100644 --- a/src/Appwrite/Platform/Installer/Server.php +++ b/src/Appwrite/Platform/Installer/Server.php @@ -6,6 +6,7 @@ use Appwrite\Platform\Installer\Http\Installer\Error; use Appwrite\Platform\Installer\Runtime\Config; use Appwrite\Platform\Installer\Runtime\State; use Swoole\Http\Server as SwooleServer; +use Swoole\Runtime; use Utopia\Http\Adapter\Swoole\Request; use Utopia\Http\Adapter\Swoole\Response; use Utopia\Http\Adapter\Swoole\Server as SwooleAdapter; @@ -28,6 +29,7 @@ class Server public const string STEP_DOCKER_COMPOSE = 'docker-compose'; public const string STEP_DOCKER_CONTAINERS = 'docker-containers'; public const string STEP_ACCOUNT_SETUP = 'account-setup'; + public const string STEP_MIGRATION = 'migration'; public const string STEP_SSL_CERTIFICATE = 'ssl-certificate'; public const string STATUS_IN_PROGRESS = 'in-progress'; @@ -129,6 +131,8 @@ class Server private function startSwooleServer(string $host, int $port, ?string $readyFile = null): void { + Runtime::enableCoroutine(SWOOLE_HOOK_ALL); + $this->state->clearStaleLock(); // Preload static files into memory diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php index 3585bc4477..3d07c65250 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php @@ -61,16 +61,16 @@ class Create extends Action $databaseKeys = System::getEnv('_APP_DATABASE_DOCUMENTSDB_KEYS', ''); $databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE'); $dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb'); - $databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')); - $databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')); + $databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))); + $databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''))); break; case VECTORSDB: $databases = Config::getParam('pools-vectorsdb', []); $databaseKeys = System::getEnv('_APP_DATABASE_VECTORSDB_KEYS', ''); $databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE'); $dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql'); - $databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')); - $databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')); + $databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))); + $databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''))); break; default: // legacy/tablesdb @@ -108,7 +108,7 @@ class Create extends Action if ($index !== false) { $selectedDsn = $databases[$index]; } else { - if (!empty($dsn)) { + if (!empty($dsn) && !empty($databaseSharedTables)) { $beforeFilter = \array_values($databases); if ($isSharedTablesV1) { $databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1)); @@ -118,7 +118,10 @@ class Create extends Action $databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables)); } } - $selectedDsn = !empty($databases) ? $databases[array_rand($databases)] : ''; + if (empty($databases)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, "No {$databasetype} database pool available for the current shared-tables mode"); + } + $selectedDsn = $databases[array_rand($databases)]; } if (\in_array($selectedDsn, $databaseSharedTables)) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php index 3aee3ebcb1..dc3ce34605 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php @@ -58,7 +58,7 @@ class Create extends IndexCreate ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject']) ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).', false, ['dbForProject']) ->param('key', null, fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject']) - ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL]), 'Index type.') + ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.') ->param('attributes', null, fn (Database $dbForProject) => new ArrayList(new Key(true, $dbForProject->getAdapter()->getMaxUIDLength()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.', false, ['dbForProject']) ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true) ->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true) diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 04e2dbd406..ae730c3f74 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -70,6 +70,8 @@ trait Deployment throw new Exception(Exception::PROJECT_NOT_FOUND, 'Repository references non-existent project'); } + $this->beforeCreateGitDeployment($project, $repository, $dbForPlatform, $authorization); + try { $dsn = new DSN($project->getAttribute('database')); $databaseName = $dsn->getHost(); @@ -561,6 +563,10 @@ trait Deployment } } + protected function beforeCreateGitDeployment(Document $project, Document $repository, Database $dbForPlatform, Authorization $authorization): void + { + } + protected function getBuildQueueName(Document $project, Database $dbForPlatform, Authorization $authorization): string { return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php index bada6d98bb..5dd5c6dcfa 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php @@ -82,12 +82,11 @@ class Create extends Action responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, - model: Response::MODEL_DETECTION_RUNTIME, + model: [ + Response::MODEL_DETECTION_RUNTIME, + Response::MODEL_DETECTION_FRAMEWORK, + ], ), - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DETECTION_FRAMEWORK, - ) ] )) ->param('installationId', '', new Text(256), 'Installation Id') diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php index ca2c812901..d5b2b48175 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php @@ -86,12 +86,11 @@ class XList extends Action responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, + model: [ + Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, + Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, + ], ), - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, - ) ] )) ->param('installationId', '', new Text(256), 'Installation Id') diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index eab6babc66..79a052b34a 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -7,6 +7,7 @@ use Appwrite\Docker\Env; use Appwrite\Platform\Installer\Runtime\State; use Appwrite\Platform\Installer\Server as InstallerServer; use Appwrite\Utopia\View; +use Swoole\Coroutine; use Utopia\Auth\Proofs\Password; use Utopia\Auth\Proofs\Token; use Utopia\Config\Config; @@ -35,6 +36,7 @@ class Install extends Action private const string GROWTH_API_URL = 'https://growth.appwrite.io/v1'; protected bool $isUpgrade = false; + protected bool $migrate = false; protected string $hostPath = ''; protected ?bool $isLocalInstall = null; protected ?array $installerConfig = null; @@ -211,13 +213,14 @@ class Install extends Action } // If interactive and web mode enabled, start web server - if ($interactive === 'Y' && Console::isInteractive()) { + // Skip the web installer when explicit CLI params are provided + if ($interactive === 'Y' && Console::isInteractive() && !$this->hasExplicitCliParams()) { Console::success('Starting web installer...'); Console::info('Open your browser at: http://localhost:' . InstallerServer::INSTALLER_WEB_PORT); Console::info('Press Ctrl+C to cancel installation'); $detectedDb = ($existingInstallation && isset($existingDatabase)) ? $existingDatabase : null; - $this->startWebServer($defaultHttpPort, $defaultHttpsPort, $organization, $image, $noStart, $vars, $isUpgrade, $detectedDb); + $this->startWebServer($defaultHttpPort, $defaultHttpsPort, $organization, $image, $noStart, $vars, $isUpgrade || $existingInstallation, $detectedDb); return; } @@ -321,7 +324,7 @@ class Install extends Action $shouldGenerateSecrets = !$existingInstallation && !$isUpgrade; $input = $this->prepareEnvironmentVariables($userInput, $vars, $shouldGenerateSecrets); - $this->performInstallation($httpPort, $httpsPort, $organization, $image, $input, $noStart, null, null, $isUpgrade); + $this->performInstallation($httpPort, $httpsPort, $organization, $image, $input, $noStart, null, null, $isUpgrade, migrate: $this->migrate); } @@ -510,7 +513,9 @@ class Install extends Action ?callable $progress = null, ?string $resumeFromStep = null, bool $isUpgrade = false, - array $account = [] + array $account = [], + ?callable $onComplete = null, + bool $migrate = false, ): void { $isLocalInstall = $this->isLocalInstall(); $this->applyLocalPaths($isLocalInstall, false); @@ -633,8 +638,34 @@ class Install extends Action $this->createInitialAdminAccount($account, $progress, $apiUrl, $domain); } - // Track installs - $this->trackSelfHostedInstall($input, $isUpgrade, $version, $account); + if ($isUpgrade && $migrate) { + // Allow the containers-completed SSE event to flush + // before blocking on migration exec + usleep(200_000); + $currentStep = InstallerServer::STEP_MIGRATION; + $this->runDatabaseMigration($progress, $isLocalInstall); + } elseif ($isUpgrade) { + $this->updateProgress($progress, InstallerServer::STEP_MIGRATION, InstallerServer::STATUS_COMPLETED, messageOverride: 'Migration skipped'); + } + + // Signal completion before tracking so the SSE stream + // finishes and the frontend can redirect immediately. + if ($onComplete) { + try { + $onComplete(); + } catch (\Throwable) { + } + } + + // Run tracking in a coroutine when inside a Swoole + // request so it doesn't block the worker. + if (Coroutine::getCid() !== -1) { + go(function () use ($input, $isUpgrade, $version, $account) { + $this->trackSelfHostedInstall($input, $isUpgrade, $version, $account); + }); + } else { + $this->trackSelfHostedInstall($input, $isUpgrade, $version, $account); + } if ($isCLI) { Console::success('Appwrite installed successfully'); @@ -726,6 +757,46 @@ class Install extends Action } } + private function runDatabaseMigration(?callable $progress, bool $isLocalInstall): void + { + $this->updateProgress( + $progress, + InstallerServer::STEP_MIGRATION, + InstallerServer::STATUS_IN_PROGRESS, + messageOverride: 'Running database migration...' + ); + + // Allow the SSE chunk to flush before the blocking exec + usleep(100_000); + + // Static command — no user input involved + $command = $isLocalInstall + ? 'docker compose exec appwrite migrate 2>&1' + : 'docker exec appwrite migrate 2>&1'; + + $output = []; + \exec($command, $output, $exit); + + if ($exit !== 0) { + $message = trim(implode("\n", $output)); + $this->updateProgress( + $progress, + InstallerServer::STEP_MIGRATION, + InstallerServer::STATUS_ERROR, + details: ['output' => $message], + messageOverride: 'Migration failed: ' . ($message ?: 'exit code ' . $exit) + ); + throw new \RuntimeException('Database migration failed', 0, $message !== '' ? new \RuntimeException($message) : null); + } + + $this->updateProgress( + $progress, + InstallerServer::STEP_MIGRATION, + InstallerServer::STATUS_COMPLETED, + messageOverride: 'Database migration completed' + ); + } + private function trackSelfHostedInstall(array $input, bool $isUpgrade, string $version, array $account): void { if ($this->isLocalInstall()) { @@ -753,7 +824,7 @@ class Install extends Action $name = $account['name'] ?? 'Admin'; $email = $account['email'] ?? 'admin@selfhosted.local'; - $hostIp = gethostbyname($domain); + $hostIp = @gethostbyname($domain); $payload = [ 'action' => $type, @@ -767,7 +838,7 @@ class Install extends Action 'email' => $email, 'domain' => $domain, 'database' => $database, - 'hostIp' => $hostIp !== $domain ? $hostIp : null, + 'ip' => ($hostIp !== false && $hostIp !== $domain) ? $hostIp : null, 'os' => php_uname('s') . ' ' . php_uname('r'), 'arch' => php_uname('m'), 'cpus' => ((int) trim((string) \shell_exec('nproc'))) ?: null, @@ -778,6 +849,8 @@ class Install extends Action try { $client = new Client(); $client + ->setConnectTimeout(5000) + ->setTimeout(5000) ->addHeader('Content-Type', 'application/json') ->fetch(self::GROWTH_API_URL . '/analytics', Client::METHOD_POST, $payload); } catch (\Throwable) { @@ -1261,6 +1334,22 @@ class Install extends Action $this->hostPath = $this->getInstallerHostPath(); } + /** + * Check if any installer-specific CLI params were explicitly passed. + * When params like --database or --http-port are provided, the user + * intends to run in CLI mode rather than launching the web installer. + */ + private function hasExplicitCliParams(): bool + { + $argv = $_SERVER['argv'] ?? []; + foreach ($argv as $arg) { + if (\str_starts_with($arg, '--')) { + return true; + } + } + return false; + } + /** * Detect the database adapter from a pre-1.9.0 compose file by * checking which DB service exists or reading _APP_DB_HOST. diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index ab6528cfe2..a36959af33 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -102,11 +102,14 @@ class SDKs extends Action } else { $sdks = explode(',', $sdks); } - $version ??= Console::confirm('Choose an Appwrite version'); $createRelease = ($release === 'yes'); $commitRelease = ($commit === 'yes'); + if ($createRelease && $examplesOnly) { + throw new \Exception('Cannot use --release=yes with --mode=examples'); + } + if (! $createRelease && ! $examplesOnly) { $git ??= Console::confirm('Should we use git push? (yes/no)'); $git = ($git === 'yes'); @@ -118,30 +121,34 @@ class SDKs extends Action $prUrls = []; } - if (! \in_array($version, [ - '0.6.x', - '0.7.x', - '0.8.x', - '0.9.x', - '0.10.x', - '0.11.x', - '0.12.x', - '0.13.x', - '0.14.x', - '0.15.x', - '1.0.x', - '1.1.x', - '1.2.x', - '1.3.x', - '1.4.x', - '1.5.x', - '1.6.x', - '1.7.x', - '1.8.x', - '1.9.x', - 'latest', - ])) { - throw new \Exception('Unknown version given'); + if (! $createRelease) { + $version ??= Console::confirm('Choose an Appwrite version'); + + if (! \in_array($version, [ + '0.6.x', + '0.7.x', + '0.8.x', + '0.9.x', + '0.10.x', + '0.11.x', + '0.12.x', + '0.13.x', + '0.14.x', + '0.15.x', + '1.0.x', + '1.1.x', + '1.2.x', + '1.3.x', + '1.4.x', + '1.5.x', + '1.6.x', + '1.7.x', + '1.8.x', + '1.9.x', + 'latest', + ])) { + throw new \Exception('Unknown version given'); + } } $selectedPlatforms = ($selectedPlatform === '*' || $selectedPlatform === null) ? null : \array_map('trim', \explode(',', $selectedPlatform)); @@ -173,6 +180,124 @@ class SDKs extends Action } Console::log(''); + + if ($createRelease && ! $examplesOnly) { + Console::info("━━━ {$language['name']} SDK ({$platform['name']}, {$language['version']}) ━━━"); + $changelog = $language['changelog'] ?? ''; + $changelog = ($changelog) ? \file_get_contents($changelog) : '# Change Log'; + + $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; + $releaseVersion = $language['version']; + $releaseNotes = $this->extractReleaseNotes($changelog, $releaseVersion); + + if (empty($releaseNotes)) { + $releaseNotes = "Release version {$releaseVersion}"; + } + + $releaseTitle = $releaseVersion; + $releaseTarget = $language['repoBranch'] ?? 'main'; + + if ($repoName === '/') { + Console::warning(' Not a releasable SDK, skipping'); + + continue; + } + + // Check if release already exists + $checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null'; + $existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? ''); + + if (! empty($existingReleaseUrl)) { + Console::warning(" Release {$releaseVersion} already exists, skipping"); + Console::log(" {$existingReleaseUrl}"); + + continue; + } + + // Check if the latest commit on the target branch already has a release + $latestCommitCommand = 'gh api repos/' . $repoName . '/commits/' . $releaseTarget . ' --jq ".sha" 2>/dev/null'; + $latestCommitSha = trim(\shell_exec($latestCommitCommand) ?? ''); + + if (! empty($latestCommitSha)) { + $latestReleaseTagCommand = 'gh api repos/' . $repoName . '/releases --jq ".[0] | .tag_name" 2>/dev/null'; + $latestReleaseTag = trim(\shell_exec($latestReleaseTagCommand) ?? ''); + + if (! empty($latestReleaseTag)) { + $tagCommitCommand = 'gh api repos/' . $repoName . '/git/ref/tags/' . $latestReleaseTag . ' --jq ".object.sha" 2>/dev/null'; + $tagCommitSha = trim(\shell_exec($tagCommitCommand) ?? ''); + + if (! empty($tagCommitSha) && $latestCommitSha === $tagCommitSha) { + Console::warning(" Latest commit already released ({$latestReleaseTag}), skipping"); + + continue; + } + } + } + + $previousVersion = ''; + $tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1'; + $previousVersion = trim(\shell_exec($tagListCommand) ?? ''); + + $formattedNotes = "## What's Changed\n\n"; + $formattedNotes .= $releaseNotes . "\n\n"; + + if (! empty($previousVersion)) { + $formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/compare/' . $previousVersion . '...' . $releaseVersion; + } else { + $formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/releases/tag/' . $releaseVersion; + } + + if (! $commitRelease) { + Console::info(' [DRY RUN] Would create release:'); + Console::log(" Repository: {$repoName}"); + Console::log(" Version: {$releaseVersion}"); + Console::log(" Title: {$releaseTitle}"); + Console::log(" Target Branch: {$releaseTarget}"); + Console::log(' Previous Version: ' . ($previousVersion ?: 'N/A')); + Console::log(' Release Notes:'); + Console::log(' ' . str_replace("\n", "\n ", $formattedNotes)); + } else { + Console::log(" Creating release {$releaseVersion}..."); + + $tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_'); + \file_put_contents($tempNotesFile, $formattedNotes); + + $releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($releaseTitle) . ' \ + --notes-file ' . \escapeshellarg($tempNotesFile) . ' \ + --target ' . \escapeshellarg($releaseTarget) . ' \ + 2>&1'; + + $releaseOutput = []; + $releaseReturnCode = 0; + \exec($releaseCommand, $releaseOutput, $releaseReturnCode); + + \unlink($tempNotesFile); + + if ($releaseReturnCode === 0) { + // Extract release URL from output + $releaseUrl = ''; + foreach ($releaseOutput as $line) { + if (strpos($line, 'https://github.com/') !== false) { + $releaseUrl = trim($line); + break; + } + } + + Console::success(" Release {$releaseVersion} created"); + if (! empty($releaseUrl)) { + Console::log(" {$releaseUrl}"); + } + } else { + $errorMessage = implode("\n", $releaseOutput); + Console::error(" Failed to create release: " . $errorMessage); + } + } + + continue; + } + Console::info("━━━ {$language['name']} SDK ({$platform['name']}, {$version}) ━━━"); $specFormat = $language['spec'] ?? 'swagger2'; $spec = null; @@ -330,119 +455,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND throw new \Exception('Language "' . $language['key'] . '" not supported'); } - if ($createRelease && ! $examplesOnly) { - $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; - $releaseVersion = $language['version']; - $releaseNotes = $this->extractReleaseNotes($changelog, $releaseVersion); - - if (empty($releaseNotes)) { - $releaseNotes = "Release version {$releaseVersion}"; - } - - $releaseTitle = $releaseVersion; - $releaseTarget = $language['repoBranch'] ?? 'main'; - - if ($repoName === '/') { - Console::warning(' Not a releasable SDK, skipping'); - - continue; - } - - // Check if release already exists - $checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null'; - $existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? ''); - - if (! empty($existingReleaseUrl)) { - Console::warning(" Release {$releaseVersion} already exists, skipping"); - Console::log(" {$existingReleaseUrl}"); - - continue; - } - - // Check if the latest commit on the target branch already has a release - $latestCommitCommand = 'gh api repos/' . $repoName . '/commits/' . $releaseTarget . ' --jq ".sha" 2>/dev/null'; - $latestCommitSha = trim(\shell_exec($latestCommitCommand) ?? ''); - - if (! empty($latestCommitSha)) { - $latestReleaseTagCommand = 'gh api repos/' . $repoName . '/releases --jq ".[0] | .tag_name" 2>/dev/null'; - $latestReleaseTag = trim(\shell_exec($latestReleaseTagCommand) ?? ''); - - if (! empty($latestReleaseTag)) { - $tagCommitCommand = 'gh api repos/' . $repoName . '/git/ref/tags/' . $latestReleaseTag . ' --jq ".object.sha" 2>/dev/null'; - $tagCommitSha = trim(\shell_exec($tagCommitCommand) ?? ''); - - if (! empty($tagCommitSha) && $latestCommitSha === $tagCommitSha) { - Console::warning(" Latest commit already released ({$latestReleaseTag}), skipping"); - - continue; - } - } - } - - $previousVersion = ''; - $tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1'; - $previousVersion = trim(\shell_exec($tagListCommand) ?? ''); - - $formattedNotes = "## What's Changed\n\n"; - $formattedNotes .= $releaseNotes . "\n\n"; - - if (! empty($previousVersion)) { - $formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/compare/' . $previousVersion . '...' . $releaseVersion; - } else { - $formattedNotes .= '**Full Changelog**: https://github.com/' . $repoName . '/releases/tag/' . $releaseVersion; - } - - if (! $commitRelease) { - Console::info(' [DRY RUN] Would create release:'); - Console::log(" Repository: {$repoName}"); - Console::log(" Version: {$releaseVersion}"); - Console::log(" Title: {$releaseTitle}"); - Console::log(" Target Branch: {$releaseTarget}"); - Console::log(' Previous Version: ' . ($previousVersion ?: 'N/A')); - Console::log(' Release Notes:'); - Console::log(' ' . str_replace("\n", "\n ", $formattedNotes)); - } else { - Console::log(" Creating release {$releaseVersion}..."); - - $tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_'); - \file_put_contents($tempNotesFile, $formattedNotes); - - $releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \ - --repo ' . \escapeshellarg($repoName) . ' \ - --title ' . \escapeshellarg($releaseTitle) . ' \ - --notes-file ' . \escapeshellarg($tempNotesFile) . ' \ - --target ' . \escapeshellarg($releaseTarget) . ' \ - 2>&1'; - - $releaseOutput = []; - $releaseReturnCode = 0; - \exec($releaseCommand, $releaseOutput, $releaseReturnCode); - - \unlink($tempNotesFile); - - if ($releaseReturnCode === 0) { - // Extract release URL from output - $releaseUrl = ''; - foreach ($releaseOutput as $line) { - if (strpos($line, 'https://github.com/') !== false) { - $releaseUrl = trim($line); - break; - } - } - - Console::success(" Release {$releaseVersion} created"); - if (! empty($releaseUrl)) { - Console::log(" {$releaseUrl}"); - } - } else { - $errorMessage = implode("\n", $releaseOutput); - Console::error(" Failed to create release: " . $errorMessage); - } - } - - continue; - } - Console::log($examplesOnly ? ' Generating examples...' : ' Generating SDK...'); diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php index 606c03bf10..a6a5284fb0 100644 --- a/src/Appwrite/Platform/Tasks/Specs.php +++ b/src/Appwrite/Platform/Tasks/Specs.php @@ -347,6 +347,13 @@ class Specs extends Action $keys = $this->getKeys(); $generatedFiles = []; + $endpoint = System::getEnv('_APP_HOME', 'https://appwrite.io'); + $email = System::getEnv('_APP_SYSTEM_TEAM_EMAIL', 'team@appwrite.io'); + $specsDir = __DIR__ . '/../../../../app/config/specs'; + + if (!is_dir($specsDir) && !@mkdir($specsDir, 0755, true) && !is_dir($specsDir)) { + throw new Exception('Failed to create specs directory: ' . $specsDir); + } foreach ($platforms as $platform) { $routes = []; @@ -443,8 +450,6 @@ class Specs extends Action foreach (['swagger2', 'open-api3'] as $format) { $formatInstance = $this->getFormatInstance($format, $arguments); $specs = new Specification($formatInstance); - $endpoint = System::getEnv('_APP_HOME', '[HOSTNAME]'); - $email = System::getEnv('_APP_SYSTEM_TEAM_EMAIL', APP_EMAIL_TEAM); $formatInstance ->setParam('name', APP_NAME) @@ -463,36 +468,30 @@ class Specs extends Action ->setParam('docs.description', 'Full API docs, specs and tutorials') ->setParam('docs.url', $endpoint . '/docs'); - $specsDir = __DIR__ . '/../../../../app/config/specs'; + $path = $mocks + ? $specsDir . '/' . $format . '-mocks-' . $platform . '.json' + : $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json'; - if (!is_dir($specsDir)) { - if (!mkdir($specsDir, 0755, true)) { - throw new Exception('Failed to create specs directory: ' . $specsDir); - } + $parsedSpecs = $specs->parse(); + $encodedSpecs = \json_encode($parsedSpecs, JSON_PRETTY_PRINT); + + unset($parsedSpecs); + + if ($encodedSpecs === false) { + throw new Exception('Failed to encode ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . \json_last_error_msg()); } - if ($mocks) { - $path = $specsDir . '/' . $format . '-mocks-' . $platform . '.json'; - - if (!file_put_contents($path, json_encode($specs->parse(), JSON_PRETTY_PRINT))) { - throw new Exception('Failed to save mocks spec file: ' . $path); - } - - $generatedFiles[] = realpath($path); - Console::success('Saved mocks spec file: ' . realpath($path)); - - continue; - } - - $path = $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json'; - - if (!file_put_contents($path, json_encode($specs->parse(), JSON_PRETTY_PRINT))) { - throw new Exception('Failed to save spec file: ' . $path); + if (\file_put_contents($path, $encodedSpecs) === false) { + throw new Exception('Failed to save ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . $path); } $generatedFiles[] = realpath($path); - Console::success('Saved spec file: ' . realpath($path)); + Console::success('Saved ' . ($mocks ? 'mocks ' : '') . 'spec file: ' . realpath($path)); + + unset($encodedSpecs, $specs, $formatInstance); } + + unset($arguments, $models, $routes, $services); } if ($git === 'yes') { diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index 1d61180963..f49674896e 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -30,6 +30,7 @@ class Upgrade extends Install ->param('interactive', 'Y', new Text(1), 'Run an interactive session', true) ->param('no-start', false, new Boolean(true), 'Run an interactive session', true) ->param('database', 'mongodb', new Text(length: 0), 'Database to use (mongodb|mariadb|postgresql)', true) + ->param('migrate', false, new Boolean(true), 'Run database migration after upgrade', true) ->callback($this->action(...)); } @@ -40,9 +41,11 @@ class Upgrade extends Install string $image, string $interactive, bool $noStart, - string $database + string $database, + bool $migrate = false, ): void { $this->isUpgrade = true; + $this->migrate = $migrate; $isLocalInstall = $this->isLocalInstall(); $this->applyLocalPaths($isLocalInstall, true); diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index d96a25351f..1080ff066f 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -28,6 +28,7 @@ use Utopia\Locale\Locale; use Utopia\Migration\Destination; use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; use Utopia\Migration\Destinations\CSV as DestinationCSV; +use Utopia\Migration\Destinations\JSON as DestinationJSON; use Utopia\Migration\Exception as MigrationException; use Utopia\Migration\Resource; use Utopia\Migration\Resources\Database\Database as ResourceDatabase; @@ -37,6 +38,7 @@ use Utopia\Migration\Source; use Utopia\Migration\Sources\Appwrite as SourceAppwrite; use Utopia\Migration\Sources\CSV; use Utopia\Migration\Sources\Firebase; +use Utopia\Migration\Sources\JSON; use Utopia\Migration\Sources\NHost; use Utopia\Migration\Sources\Supabase; use Utopia\Migration\Transfer; @@ -208,8 +210,8 @@ class Migrations extends Action $getDatabasesDB = fn (Document $database): Database => $this->getDatabasesDBForProject($database); $queries = []; - if ($source === SourceAppwrite::getName() && $destination === DestinationCSV::getName()) { - $queries = Query::parseQueries($migrationOptions['queries']); + if ($source === SourceAppwrite::getName() && in_array($destination, [DestinationCSV::getName(), DestinationJSON::getName()])) { + $queries = Query::parseQueries($migrationOptions['queries'] ?? []); } $migrationSource = match ($source) { @@ -250,6 +252,12 @@ class Migrations extends Action $this->dbForProject, $getDatabasesDB ), + JSON::getName() => new JSON( + $resourceId, + $migrationOptions['path'], + $this->deviceForMigrations, + $this->dbForProject, + ), default => throw new \Exception('Invalid source type'), }; @@ -288,6 +296,13 @@ class Migrations extends Action $options['escape'], $options['header'], ), + DestinationJSON::getName() => new DestinationJSON( + $this->deviceForFiles, + $migration->getAttribute('resourceId'), + $options['bucketId'] ?? 'default', + $options['filename'], + $options['columns'] ?? [], + ), default => throw new \Exception('Invalid destination type'), }; } @@ -550,8 +565,9 @@ class Migrations extends Action $destination?->success(); $source?->success(); } - if ($migration->getAttribute('destination') === DestinationCSV::getName()) { - $this->handleCSVExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization); + $destination_type = $migration->getAttribute('destination'); + if ($destination_type === DestinationCSV::getName() || $destination_type === DestinationJSON::getName()) { + $this->handleDataExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization); } } finally { $source?->cleanup(); @@ -583,7 +599,7 @@ class Migrations extends Action * @param Authorization $authorization * @return void */ - protected function handleCSVExportComplete( + protected function handleDataExportComplete( Document $project, Document $migration, Mail $queueForMails, @@ -608,7 +624,8 @@ class Migrations extends Action throw new \Exception('Bucket not found'); } - $path = $this->deviceForFiles->getPath($bucketId . '/' . $this->sanitizeFilename($filename) . '.csv'); + $extension = $migration->getAttribute('destination') === DestinationJSON::getName() ? '.json' : '.csv'; + $path = $this->deviceForFiles->getPath($bucketId . '/' . $this->sanitizeFilename($filename) . $extension); $size = $this->deviceForFiles->getFileSize($path); $mime = $this->deviceForFiles->getFileMimeType($path); $hash = $this->deviceForFiles->getFileHash($path); @@ -632,13 +649,14 @@ class Migrations extends Action $migration->setAttribute('errors', $errors); $migration = $this->updateMigrationDocument($migration, $project, $queueForRealtime); - $this->sendCSVEmail( + $this->sendExportEmail( success: false, project: $project, user: $user, options: $options, queueForMails: $queueForMails, platform: $platform, + exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV', sizeMB: $sizeMB ); @@ -694,13 +712,14 @@ class Migrations extends Action $migration->setAttribute('options', $options); $this->updateMigrationDocument($migration, $project, $queueForRealtime); - $this->sendCSVEmail( + $this->sendExportEmail( success: true, project: $project, user: $user, options: $options, queueForMails: $queueForMails, platform: $platform, + exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV', downloadUrl: $downloadUrl ); } @@ -719,13 +738,14 @@ class Migrations extends Action * @return void * @throws \Exception */ - protected function sendCSVEmail( + protected function sendExportEmail( bool $success, Document $project, Document $user, array $options, Mail $queueForMails, array $platform, + string $exportType = 'CSV', string $downloadUrl = '', float $sizeMB = 0.0, ): void { @@ -745,15 +765,15 @@ class Migrations extends Action ? 'success' : 'failure'; - // Get localized email content - $subject = $locale->getText("emails.csvExport.{$emailType}.subject"); - $preview = $locale->getText("emails.csvExport.{$emailType}.preview"); - $hello = $locale->getText("emails.csvExport.{$emailType}.hello"); - $body = $locale->getText("emails.csvExport.{$emailType}.body"); - $footer = $locale->getText("emails.csvExport.{$emailType}.footer"); - $thanks = $locale->getText("emails.csvExport.{$emailType}.thanks"); - $signature = $locale->getText("emails.csvExport.{$emailType}.signature"); - $buttonText = $success ? $locale->getText("emails.csvExport.{$emailType}.buttonText") : ''; + // Get localized email content — replace {{type}} with export format (CSV/JSON) + $subject = \str_replace('{{type}}', $exportType, $locale->getText("emails.dataExport.{$emailType}.subject")); + $preview = \str_replace('{{type}}', $exportType, $locale->getText("emails.dataExport.{$emailType}.preview")); + $hello = $locale->getText("emails.dataExport.{$emailType}.hello"); + $body = $locale->getText("emails.dataExport.{$emailType}.body"); + $footer = $locale->getText("emails.dataExport.{$emailType}.footer"); + $thanks = $locale->getText("emails.dataExport.{$emailType}.thanks"); + $signature = $locale->getText("emails.dataExport.{$emailType}.signature"); + $buttonText = $success ? $locale->getText("emails.dataExport.{$emailType}.buttonText") : ''; // Build email body using appropriate template $templatePath = $success @@ -769,6 +789,7 @@ class Migrations extends Action ->setParam('{{direction}}', $locale->getText('settings.direction')) ->setParam('{{project}}', $project->getAttribute('name')) ->setParam('{{user}}', $user->getAttribute('name', $user->getAttribute('email'))) + ->setParam('{{type}}', $exportType) ->setParam('{{size}}', $success ? '' : (string)$sizeMB); if ($success) { @@ -789,6 +810,7 @@ class Migrations extends Action 'terms' => $platform['termsUrl'], 'privacy' => $platform['privacyUrl'], 'platform' => $platform['platformName'], + 'type' => $exportType, ]; $queueForMails diff --git a/src/Appwrite/Template/Template.php b/src/Appwrite/Template/Template.php index c8744c87bb..695e925e52 100644 --- a/src/Appwrite/Template/Template.php +++ b/src/Appwrite/Template/Template.php @@ -149,7 +149,7 @@ class Template extends View /** * From Camel Case * - * @var string $input + * @param string $input * * @return string */ @@ -167,7 +167,7 @@ class Template extends View /** * From Camel Case to Dash Case * - * @var string $input + * @param string $input * * @return string */ diff --git a/src/Appwrite/Utopia/Request/Filters/V17.php b/src/Appwrite/Utopia/Request/Filters/V17.php index 2cdf3973b2..0e4f9eceb6 100644 --- a/src/Appwrite/Utopia/Request/Filters/V17.php +++ b/src/Appwrite/Utopia/Request/Filters/V17.php @@ -120,11 +120,11 @@ class V17 extends Filter $isArrayStack = !$isStringStack && $stackCount > 0; if ($char === static::CHAR_BACKSLASH) { - if (!(static::isSpecialChar($filter[$i + 1]))) { - static::appendSymbol($isStringStack, $filter[$i], $i, $filter, $currentParam); + if (!(self::isSpecialChar($filter[$i + 1]))) { + self::appendSymbol($isStringStack, $filter[$i], $i, $filter, $currentParam); } - static::appendSymbol($isStringStack, $filter[$i + 1], $i, $filter, $currentParam); + self::appendSymbol($isStringStack, $filter[$i + 1], $i, $filter, $currentParam); $i++; continue; @@ -147,7 +147,7 @@ class V17 extends Filter } // Either way, add symbol to builder - static::appendSymbol( + self::appendSymbol( $isStringStack, $char, $i, @@ -199,7 +199,7 @@ class V17 extends Filter } // Value, not relevant to syntax - static::appendSymbol( + self::appendSymbol( $isStringStack, $char, $i, diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 99170a58c9..e01dc58bf6 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -629,9 +629,9 @@ class Response extends SwooleResponse } /** - * Return the currently set filter + * Return the currently set filters * - * @return Filter + * @return array */ public function getFilters(): array { @@ -661,7 +661,7 @@ class Response extends SwooleResponse /** * Static wrapper to show sensitive data in response * - * @param callable The callback to show sensitive information for + * @param callable(): array $callback The callback to show sensitive information for * @return array */ public static function showSensitive(callable $callback): array diff --git a/src/Appwrite/Utopia/Response/Filters/V21.php b/src/Appwrite/Utopia/Response/Filters/V21.php index b65e26a8b0..3fc16d6c8a 100644 --- a/src/Appwrite/Utopia/Response/Filters/V21.php +++ b/src/Appwrite/Utopia/Response/Filters/V21.php @@ -11,34 +11,48 @@ class V21 extends Filter public function parse(array $content, string $model): array { return match ($model) { + Response::MODEL_USER => $this->parseUser($content), + Response::MODEL_USER_LIST => $this->handleList( + $content, + 'users', + fn ($item) => $this->parseUser($item), + ), + Response::MODEL_ACCOUNT => $this->parseUser($content), Response::MODEL_SITE => $this->parseSite($content), Response::MODEL_SITE_LIST => $this->handleList( $content, - "sites", + 'sites', fn ($item) => $this->parseSite($item), ), Response::MODEL_FUNCTION => $this->parseFunction($content), Response::MODEL_FUNCTION_LIST => $this->handleList( $content, - "functions", + 'functions', fn ($item) => $this->parseFunction($item), ), Response::MODEL_DOCUMENT => $this->parseDocument($content), Response::MODEL_DOCUMENT_LIST => $this->handleList( $content, - "documents", + 'documents', fn ($item) => $this->parseDocument($item), ), Response::MODEL_ROW => $this->parseRow($content), Response::MODEL_ROW_LIST => $this->handleList( $content, - "rows", + 'rows', fn ($item) => $this->parseRow($item), ), default => $content, }; } + protected function parseUser(array $content): array + { + unset($content['impersonator']); + unset($content['impersonatorUserId']); + return $content; + } + protected function parseSite(array $content): array { $content = $this->parseSpecs($content); diff --git a/src/Appwrite/Utopia/Response/Model/User.php b/src/Appwrite/Utopia/Response/Model/User.php index 476778e68b..01447ccfc2 100644 --- a/src/Appwrite/Utopia/Response/Model/User.php +++ b/src/Appwrite/Utopia/Response/Model/User.php @@ -157,9 +157,9 @@ class User extends Model } /** - * Get Collection + * Filter user document attributes for response output. * - * @return string + * @return Document */ public function filter(Document $document): Document { diff --git a/tests/e2e/Services/GraphQL/FunctionsClientTest.php b/tests/e2e/Services/GraphQL/FunctionsClientTest.php index 234d8fa71b..8dc2fe337f 100644 --- a/tests/e2e/Services/GraphQL/FunctionsClientTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsClientTest.php @@ -24,8 +24,8 @@ class FunctionsClientTest extends Scope protected function setupFunction(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedFunction[$key])) { - return static::$cachedFunction[$key]; + if (!empty(self::$cachedFunction[$key])) { + return self::$cachedFunction[$key]; } $projectId = $this->getProject()['$id']; @@ -79,15 +79,15 @@ class FunctionsClientTest extends Scope $this->assertIsArray($variables['body']['data']); $this->assertArrayNotHasKey('errors', $variables['body']); - static::$cachedFunction[$key] = $function; + self::$cachedFunction[$key] = $function; return $function; } protected function setupDeployment(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedDeployment[$key])) { - return static::$cachedDeployment[$key]; + if (!empty(self::$cachedDeployment[$key])) { + return self::$cachedDeployment[$key]; } $function = $this->setupFunction(); @@ -146,15 +146,15 @@ class FunctionsClientTest extends Scope $this->assertEquals('ready', $deployment['status']); }, 60000); - static::$cachedDeployment[$key] = $deployment; + self::$cachedDeployment[$key] = $deployment; return $deployment; } protected function setupExecution(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedExecution[$key])) { - return static::$cachedExecution[$key]; + if (!empty(self::$cachedExecution[$key])) { + return self::$cachedExecution[$key]; } $function = $this->setupFunction(); @@ -177,8 +177,8 @@ class FunctionsClientTest extends Scope $this->assertIsArray($execution['body']['data']); $this->assertArrayNotHasKey('errors', $execution['body']); - static::$cachedExecution[$key] = $execution['body']['data']['functionsCreateExecution']; - return static::$cachedExecution[$key]; + self::$cachedExecution[$key] = $execution['body']['data']['functionsCreateExecution']; + return self::$cachedExecution[$key]; } public function testCreateFunction(): void diff --git a/tests/e2e/Services/GraphQL/FunctionsServerTest.php b/tests/e2e/Services/GraphQL/FunctionsServerTest.php index a66789d646..8e1c7ac7e7 100644 --- a/tests/e2e/Services/GraphQL/FunctionsServerTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsServerTest.php @@ -25,8 +25,8 @@ class FunctionsServerTest extends Scope protected function setupFunction(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedFunction[$key])) { - return static::$cachedFunction[$key]; + if (!empty(self::$cachedFunction[$key])) { + return self::$cachedFunction[$key]; } $projectId = $this->getProject()['$id']; @@ -79,15 +79,15 @@ class FunctionsServerTest extends Scope $this->assertIsArray($variables['body']['data']); $this->assertArrayNotHasKey('errors', $variables['body']); - static::$cachedFunction[$key] = $function; + self::$cachedFunction[$key] = $function; return $function; } protected function setupDeployment(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedDeployment[$key])) { - return static::$cachedDeployment[$key]; + if (!empty(self::$cachedDeployment[$key])) { + return self::$cachedDeployment[$key]; } $function = $this->setupFunction(); @@ -149,15 +149,15 @@ class FunctionsServerTest extends Scope $this->assertEquals('ready', $deployment['status']); }, 120000); - static::$cachedDeployment[$key] = $deployment; + self::$cachedDeployment[$key] = $deployment; return $deployment; } protected function setupExecution(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedExecution[$key])) { - return static::$cachedExecution[$key]; + if (!empty(self::$cachedExecution[$key])) { + return self::$cachedExecution[$key]; } $deployment = $this->setupDeployment(); @@ -179,8 +179,8 @@ class FunctionsServerTest extends Scope $this->assertIsArray($execution['body']['data']); $this->assertArrayNotHasKey('errors', $execution['body']); - static::$cachedExecution[$key] = $execution['body']['data']['functionsCreateExecution']; - return static::$cachedExecution[$key]; + self::$cachedExecution[$key] = $execution['body']['data']['functionsCreateExecution']; + return self::$cachedExecution[$key]; } public function testCreateFunction(): void @@ -496,8 +496,8 @@ class FunctionsServerTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedDeployment[$key] = []; - static::$cachedExecution[$key] = []; + self::$cachedDeployment[$key] = []; + self::$cachedExecution[$key] = []; } /** @@ -529,6 +529,6 @@ class FunctionsServerTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedFunction[$key] = []; + self::$cachedFunction[$key] = []; } } diff --git a/tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php b/tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php index 4d34dc6b23..a4987c4078 100644 --- a/tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php +++ b/tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php @@ -43,8 +43,8 @@ class DatabaseClientTest extends Scope protected function setupDatabase(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$database[$cacheKey])) { - return static::$database[$cacheKey]; + if (!empty(self::$database[$cacheKey])) { + return self::$database[$cacheKey]; } $projectId = $this->getProject()['$id']; @@ -71,9 +71,9 @@ class DatabaseClientTest extends Scope } $this->assertIsArray($database['body']['data']); - static::$database[$cacheKey] = $database['body']['data']['databasesCreate']; + self::$database[$cacheKey] = $database['body']['data']['databasesCreate']; - return static::$database[$cacheKey]; + return self::$database[$cacheKey]; } /** @@ -82,8 +82,8 @@ class DatabaseClientTest extends Scope protected function setupCollection(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$collection[$cacheKey])) { - return static::$collection[$cacheKey]; + if (!empty(self::$collection[$cacheKey])) { + return self::$collection[$cacheKey]; } $database = $this->setupDatabase(); @@ -121,12 +121,12 @@ class DatabaseClientTest extends Scope $this->assertIsArray($collection['body']['data']); - static::$collection[$cacheKey] = [ + self::$collection[$cacheKey] = [ 'database' => $database, 'collection' => $collection['body']['data']['databasesCreateCollection'], ]; - return static::$collection[$cacheKey]; + return self::$collection[$cacheKey]; } /** @@ -206,8 +206,8 @@ class DatabaseClientTest extends Scope protected function setupDocument(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$document[$cacheKey])) { - return static::$document[$cacheKey]; + if (!empty(self::$document[$cacheKey])) { + return self::$document[$cacheKey]; } $data = $this->setupAttributes(); @@ -256,13 +256,13 @@ class DatabaseClientTest extends Scope $this->assertArrayNotHasKey('errors', $document['body']); $this->assertIsArray($document['body']['data']); - static::$document[$cacheKey] = [ + self::$document[$cacheKey] = [ 'database' => $data['database'], 'collection' => $data['collection'], 'document' => $document['body']['data']['databasesCreateDocument'], ]; - return static::$document[$cacheKey]; + return self::$document[$cacheKey]; } /** @@ -271,8 +271,8 @@ class DatabaseClientTest extends Scope protected function setupBulkData(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$bulkData[$cacheKey])) { - return static::$bulkData[$cacheKey]; + if (!empty(self::$bulkData[$cacheKey])) { + return self::$bulkData[$cacheKey]; } $project = $this->getProject(); @@ -352,13 +352,13 @@ class DatabaseClientTest extends Scope $this->assertArrayNotHasKey('errors', $res['body']); $this->assertCount(10, $res['body']['data']['databasesCreateDocuments']['documents']); - static::$bulkData[$cacheKey] = [ + self::$bulkData[$cacheKey] = [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'projectId' => $projectId, ]; - return static::$bulkData[$cacheKey]; + return self::$bulkData[$cacheKey]; } /** diff --git a/tests/e2e/Services/GraphQL/MessagingTest.php b/tests/e2e/Services/GraphQL/MessagingTest.php index 322c51c1f7..03e7cc00f6 100644 --- a/tests/e2e/Services/GraphQL/MessagingTest.php +++ b/tests/e2e/Services/GraphQL/MessagingTest.php @@ -26,8 +26,8 @@ class MessagingTest extends Scope protected function setupProviders(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedProviders[$key])) { - return static::$cachedProviders[$key]; + if (!empty(self::$cachedProviders[$key])) { + return self::$cachedProviders[$key]; } $providersParams = [ @@ -128,15 +128,15 @@ class MessagingTest extends Scope $this->assertEquals($providersParams[$providerKey]['name'], $response['body']['data']['messagingCreate' . $providerKey . 'Provider']['name']); } - static::$cachedProviders[$key] = $providers; + self::$cachedProviders[$key] = $providers; return $providers; } protected function setupUpdatedProviders(): array { $key = $this->getProject()['$id'] . '_updated'; - if (!empty(static::$cachedProviders[$key])) { - return static::$cachedProviders[$key]; + if (!empty(self::$cachedProviders[$key])) { + return self::$cachedProviders[$key]; } $providers = $this->setupProviders(); @@ -247,15 +247,15 @@ class MessagingTest extends Scope $this->assertEquals('Mailgun2', $response['body']['data']['messagingUpdateMailgunProvider']['name']); $this->assertEquals(false, $response['body']['data']['messagingUpdateMailgunProvider']['enabled']); - static::$cachedProviders[$key] = $providers; + self::$cachedProviders[$key] = $providers; return $providers; } protected function setupTopic(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedTopic[$key])) { - return static::$cachedTopic[$key]; + if (!empty(self::$cachedTopic[$key])) { + return self::$cachedTopic[$key]; } $query = $this->getQuery(self::CREATE_TOPIC); @@ -275,15 +275,15 @@ class MessagingTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('topic1', $response['body']['data']['messagingCreateTopic']['name']); - static::$cachedTopic[$key] = $response['body']['data']['messagingCreateTopic']; - return static::$cachedTopic[$key]; + self::$cachedTopic[$key] = $response['body']['data']['messagingCreateTopic']; + return self::$cachedTopic[$key]; } protected function setupUpdatedTopic(): string { $key = $this->getProject()['$id'] . '_updated'; - if (!empty(static::$cachedTopic[$key])) { - return static::$cachedTopic[$key]; + if (!empty(self::$cachedTopic[$key])) { + return self::$cachedTopic[$key]; } $topic = $this->setupTopic(); @@ -306,15 +306,15 @@ class MessagingTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('topic2', $response['body']['data']['messagingUpdateTopic']['name']); - static::$cachedTopic[$key] = $topicId; + self::$cachedTopic[$key] = $topicId; return $topicId; } protected function setupSubscriber(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedSubscriber[$key])) { - return static::$cachedSubscriber[$key]; + if (!empty(self::$cachedSubscriber[$key])) { + return self::$cachedSubscriber[$key]; } $topic = $this->setupTopic(); @@ -386,15 +386,15 @@ class MessagingTest extends Scope $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['targetId'], $targetId); $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['target']['userId'], $userId); - static::$cachedSubscriber[$key] = $response['body']['data']['messagingCreateSubscriber']; - return static::$cachedSubscriber[$key]; + self::$cachedSubscriber[$key] = $response['body']['data']['messagingCreateSubscriber']; + return self::$cachedSubscriber[$key]; } protected function setupEmail(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedEmail[$key])) { - return static::$cachedEmail[$key]; + if (!empty(self::$cachedEmail[$key])) { + return self::$cachedEmail[$key]; } if (empty(System::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'))) { @@ -550,15 +550,15 @@ class MessagingTest extends Scope $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']); $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors'])); - static::$cachedEmail[$key] = $message['body']['data']['messagingGetMessage']; - return static::$cachedEmail[$key]; + self::$cachedEmail[$key] = $message['body']['data']['messagingGetMessage']; + return self::$cachedEmail[$key]; } protected function setupSms(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedSms[$key])) { - return static::$cachedSms[$key]; + if (!empty(self::$cachedSms[$key])) { + return self::$cachedSms[$key]; } if (empty(System::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) { @@ -709,15 +709,15 @@ class MessagingTest extends Scope $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']); $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors'])); - static::$cachedSms[$key] = $message['body']['data']['messagingGetMessage']; - return static::$cachedSms[$key]; + self::$cachedSms[$key] = $message['body']['data']['messagingGetMessage']; + return self::$cachedSms[$key]; } protected function setupPush(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedPush[$key])) { - return static::$cachedPush[$key]; + if (!empty(self::$cachedPush[$key])) { + return self::$cachedPush[$key]; } if (empty(System::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'))) { @@ -870,8 +870,8 @@ class MessagingTest extends Scope $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']); $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors'])); - static::$cachedPush[$key] = $message['body']['data']['messagingGetMessage']; - return static::$cachedPush[$key]; + self::$cachedPush[$key] = $message['body']['data']['messagingGetMessage']; + return self::$cachedPush[$key]; } public function testCreateProviders(): void @@ -945,8 +945,8 @@ class MessagingTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedProviders[$key] = []; - static::$cachedProviders[$key . '_updated'] = []; + self::$cachedProviders[$key] = []; + self::$cachedProviders[$key . '_updated'] = []; } public function testCreateTopic(): void @@ -1081,7 +1081,7 @@ class MessagingTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedSubscriber[$key] = []; + self::$cachedSubscriber[$key] = []; } public function testDeleteTopic() @@ -1105,8 +1105,8 @@ class MessagingTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedTopic[$key] = []; - static::$cachedTopic[$key . '_updated'] = []; + self::$cachedTopic[$key] = []; + self::$cachedTopic[$key . '_updated'] = []; } public function testSendEmail(): void diff --git a/tests/e2e/Services/GraphQL/StorageClientTest.php b/tests/e2e/Services/GraphQL/StorageClientTest.php index 84af910a50..3e02de0585 100644 --- a/tests/e2e/Services/GraphQL/StorageClientTest.php +++ b/tests/e2e/Services/GraphQL/StorageClientTest.php @@ -23,8 +23,8 @@ class StorageClientTest extends Scope protected function setupBucket(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedBucket[$key])) { - return static::$cachedBucket[$key]; + if (!empty(self::$cachedBucket[$key])) { + return self::$cachedBucket[$key]; } $projectId = $this->getProject()['$id']; @@ -55,15 +55,15 @@ class StorageClientTest extends Scope $bucket = $bucket['body']['data']['storageCreateBucket']; $this->assertEquals('Actors', $bucket['name']); - static::$cachedBucket[$key] = $bucket; + self::$cachedBucket[$key] = $bucket; return $bucket; } protected function setupFile(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedFile[$key])) { - return static::$cachedFile[$key]; + if (!empty(self::$cachedFile[$key])) { + return self::$cachedFile[$key]; } $bucket = $this->setupBucket(); @@ -99,8 +99,8 @@ class StorageClientTest extends Scope $this->assertIsArray($file['body']['data']); $this->assertArrayNotHasKey('errors', $file['body']); - static::$cachedFile[$key] = $file['body']['data']['storageCreateFile']; - return static::$cachedFile[$key]; + self::$cachedFile[$key] = $file['body']['data']['storageCreateFile']; + return self::$cachedFile[$key]; } public function testCreateBucket(): void @@ -319,6 +319,6 @@ class StorageClientTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedFile[$key] = []; + self::$cachedFile[$key] = []; } } diff --git a/tests/e2e/Services/GraphQL/StorageServerTest.php b/tests/e2e/Services/GraphQL/StorageServerTest.php index 8a3158a98b..9622582e80 100644 --- a/tests/e2e/Services/GraphQL/StorageServerTest.php +++ b/tests/e2e/Services/GraphQL/StorageServerTest.php @@ -23,8 +23,8 @@ class StorageServerTest extends Scope protected function setupBucket(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedBucket[$key])) { - return static::$cachedBucket[$key]; + if (!empty(self::$cachedBucket[$key])) { + return self::$cachedBucket[$key]; } $projectId = $this->getProject()['$id']; @@ -54,15 +54,15 @@ class StorageServerTest extends Scope $bucket = $bucket['body']['data']['storageCreateBucket']; $this->assertEquals('Actors', $bucket['name']); - static::$cachedBucket[$key] = $bucket; + self::$cachedBucket[$key] = $bucket; return $bucket; } protected function setupFile(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedFile[$key])) { - return static::$cachedFile[$key]; + if (!empty(self::$cachedFile[$key])) { + return self::$cachedFile[$key]; } $bucket = $this->setupBucket(); @@ -98,8 +98,8 @@ class StorageServerTest extends Scope $this->assertIsArray($file['body']['data']); $this->assertArrayNotHasKey('errors', $file['body']); - static::$cachedFile[$key] = $file['body']['data']['storageCreateFile']; - return static::$cachedFile[$key]; + self::$cachedFile[$key] = $file['body']['data']['storageCreateFile']; + return self::$cachedFile[$key]; } public function testCreateBucket(): void @@ -413,7 +413,7 @@ class StorageServerTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedFile[$key] = []; + self::$cachedFile[$key] = []; } /** @@ -443,7 +443,7 @@ class StorageServerTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedBucket[$key] = []; - static::$cachedFile[$key] = []; + self::$cachedBucket[$key] = []; + self::$cachedFile[$key] = []; } } diff --git a/tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php b/tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php index 3078202546..f0b4e4b75c 100644 --- a/tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php +++ b/tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php @@ -39,8 +39,8 @@ class DatabaseServerTest extends Scope protected function setupDatabase(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedDatabase[$cacheKey])) { - return static::$cachedDatabase[$cacheKey]; + if (!empty(self::$cachedDatabase[$cacheKey])) { + return self::$cachedDatabase[$cacheKey]; } $projectId = $this->getProject()['$id']; @@ -62,15 +62,15 @@ class DatabaseServerTest extends Scope $this->assertArrayNotHasKey('errors', $database['body']); - static::$cachedDatabase[$cacheKey] = $database['body']['data']['tablesDBCreate']; - return static::$cachedDatabase[$cacheKey]; + self::$cachedDatabase[$cacheKey] = $database['body']['data']['tablesDBCreate']; + return self::$cachedDatabase[$cacheKey]; } protected function setupTable(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedTableData[$cacheKey])) { - return static::$cachedTableData[$cacheKey]; + if (!empty(self::$cachedTableData[$cacheKey])) { + return self::$cachedTableData[$cacheKey]; } $database = $this->setupDatabase(); @@ -124,20 +124,20 @@ class DatabaseServerTest extends Scope $this->assertArrayNotHasKey('errors', $table2['body']); $table2 = $table2['body']['data']['tablesDBCreateTable']; - static::$cachedTableData[$cacheKey] = [ + self::$cachedTableData[$cacheKey] = [ 'database' => $database, 'table' => $table, 'table2' => $table2, ]; - return static::$cachedTableData[$cacheKey]; + return self::$cachedTableData[$cacheKey]; } protected function setupStringColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedStringColumnData[$cacheKey])) { - return static::$cachedStringColumnData[$cacheKey]; + if (!empty(self::$cachedStringColumnData[$cacheKey])) { + return self::$cachedStringColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -159,8 +159,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedStringColumnData[$cacheKey] = $data; - return static::$cachedStringColumnData[$cacheKey]; + self::$cachedStringColumnData[$cacheKey] = $data; + return self::$cachedStringColumnData[$cacheKey]; } protected function setupUpdatedStringColumn(): array @@ -205,8 +205,8 @@ class DatabaseServerTest extends Scope protected function setupIntegerColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedIntegerColumnData[$cacheKey])) { - return static::$cachedIntegerColumnData[$cacheKey]; + if (!empty(self::$cachedIntegerColumnData[$cacheKey])) { + return self::$cachedIntegerColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -229,8 +229,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedIntegerColumnData[$cacheKey] = $data; - return static::$cachedIntegerColumnData[$cacheKey]; + self::$cachedIntegerColumnData[$cacheKey] = $data; + return self::$cachedIntegerColumnData[$cacheKey]; } protected function setupUpdatedIntegerColumn(): array @@ -275,8 +275,8 @@ class DatabaseServerTest extends Scope protected function setupBooleanColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedBooleanColumnData[$cacheKey])) { - return static::$cachedBooleanColumnData[$cacheKey]; + if (!empty(self::$cachedBooleanColumnData[$cacheKey])) { + return self::$cachedBooleanColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -297,8 +297,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedBooleanColumnData[$cacheKey] = $data; - return static::$cachedBooleanColumnData[$cacheKey]; + self::$cachedBooleanColumnData[$cacheKey] = $data; + return self::$cachedBooleanColumnData[$cacheKey]; } protected function setupUpdatedBooleanColumn(): array @@ -341,8 +341,8 @@ class DatabaseServerTest extends Scope protected function setupFloatColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedFloatColumnData[$cacheKey])) { - return static::$cachedFloatColumnData[$cacheKey]; + if (!empty(self::$cachedFloatColumnData[$cacheKey])) { + return self::$cachedFloatColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -366,8 +366,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedFloatColumnData[$cacheKey] = $data; - return static::$cachedFloatColumnData[$cacheKey]; + self::$cachedFloatColumnData[$cacheKey] = $data; + return self::$cachedFloatColumnData[$cacheKey]; } protected function setupUpdatedFloatColumn(): array @@ -412,8 +412,8 @@ class DatabaseServerTest extends Scope protected function setupEmailColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedEmailColumnData[$cacheKey])) { - return static::$cachedEmailColumnData[$cacheKey]; + if (!empty(self::$cachedEmailColumnData[$cacheKey])) { + return self::$cachedEmailColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -434,8 +434,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedEmailColumnData[$cacheKey] = $data; - return static::$cachedEmailColumnData[$cacheKey]; + self::$cachedEmailColumnData[$cacheKey] = $data; + return self::$cachedEmailColumnData[$cacheKey]; } protected function setupUpdatedEmailColumn(): array @@ -478,8 +478,8 @@ class DatabaseServerTest extends Scope protected function setupEnumColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedEnumColumnData[$cacheKey])) { - return static::$cachedEnumColumnData[$cacheKey]; + if (!empty(self::$cachedEnumColumnData[$cacheKey])) { + return self::$cachedEnumColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -505,8 +505,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedEnumColumnData[$cacheKey] = $data; - return static::$cachedEnumColumnData[$cacheKey]; + self::$cachedEnumColumnData[$cacheKey] = $data; + return self::$cachedEnumColumnData[$cacheKey]; } protected function setupUpdatedEnumColumn(): array @@ -554,8 +554,8 @@ class DatabaseServerTest extends Scope protected function setupDatetimeColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedDatetimeColumnData[$cacheKey])) { - return static::$cachedDatetimeColumnData[$cacheKey]; + if (!empty(self::$cachedDatetimeColumnData[$cacheKey])) { + return self::$cachedDatetimeColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -576,8 +576,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedDatetimeColumnData[$cacheKey] = $data; - return static::$cachedDatetimeColumnData[$cacheKey]; + self::$cachedDatetimeColumnData[$cacheKey] = $data; + return self::$cachedDatetimeColumnData[$cacheKey]; } protected function setupUpdatedDatetimeColumn(): array @@ -620,8 +620,8 @@ class DatabaseServerTest extends Scope protected function setupRelationshipColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedRelationshipColumnData[$cacheKey])) { - return static::$cachedRelationshipColumnData[$cacheKey]; + if (!empty(self::$cachedRelationshipColumnData[$cacheKey])) { + return self::$cachedRelationshipColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -645,8 +645,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedRelationshipColumnData[$cacheKey] = $data; - return static::$cachedRelationshipColumnData[$cacheKey]; + self::$cachedRelationshipColumnData[$cacheKey] = $data; + return self::$cachedRelationshipColumnData[$cacheKey]; } protected function setupUpdatedRelationshipColumn(): array @@ -688,8 +688,8 @@ class DatabaseServerTest extends Scope protected function setupIPColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedIPColumnData[$cacheKey])) { - return static::$cachedIPColumnData[$cacheKey]; + if (!empty(self::$cachedIPColumnData[$cacheKey])) { + return self::$cachedIPColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -711,8 +711,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedIPColumnData[$cacheKey] = $data; - return static::$cachedIPColumnData[$cacheKey]; + self::$cachedIPColumnData[$cacheKey] = $data; + return self::$cachedIPColumnData[$cacheKey]; } protected function setupUpdatedIPColumn(): array @@ -755,8 +755,8 @@ class DatabaseServerTest extends Scope protected function setupURLColumn(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedURLColumnData[$cacheKey])) { - return static::$cachedURLColumnData[$cacheKey]; + if (!empty(self::$cachedURLColumnData[$cacheKey])) { + return self::$cachedURLColumnData[$cacheKey]; } $data = $this->setupTable(); @@ -778,8 +778,8 @@ class DatabaseServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - static::$cachedURLColumnData[$cacheKey] = $data; - return static::$cachedURLColumnData[$cacheKey]; + self::$cachedURLColumnData[$cacheKey] = $data; + return self::$cachedURLColumnData[$cacheKey]; } protected function setupUpdatedURLColumn(): array @@ -822,8 +822,8 @@ class DatabaseServerTest extends Scope protected function setupIndex(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedIndexData[$cacheKey])) { - return static::$cachedIndexData[$cacheKey]; + if (!empty(self::$cachedIndexData[$cacheKey])) { + return self::$cachedIndexData[$cacheKey]; } // Need updated string and integer columns first @@ -855,31 +855,31 @@ class DatabaseServerTest extends Scope if (isset($index['body']['errors'])) { $errorMessage = $index['body']['errors'][0]['message'] ?? ''; if (strpos($errorMessage, 'already exists') !== false || strpos($errorMessage, 'Document with the requested ID already exists') !== false) { - static::$cachedIndexData[$cacheKey] = [ + self::$cachedIndexData[$cacheKey] = [ 'database' => $data['database'], 'table' => $data['table'], 'index' => ['key' => 'index'], ]; - return static::$cachedIndexData[$cacheKey]; + return self::$cachedIndexData[$cacheKey]; } } $this->assertArrayNotHasKey('errors', $index['body']); - static::$cachedIndexData[$cacheKey] = [ + self::$cachedIndexData[$cacheKey] = [ 'database' => $data['database'], 'table' => $data['table'], 'index' => $index['body']['data']['tablesDBCreateIndex'], ]; - return static::$cachedIndexData[$cacheKey]; + return self::$cachedIndexData[$cacheKey]; } protected function setupRow(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedRowData[$cacheKey])) { - return static::$cachedRowData[$cacheKey]; + if (!empty(self::$cachedRowData[$cacheKey])) { + return self::$cachedRowData[$cacheKey]; } // Need all columns that the row data references @@ -940,20 +940,20 @@ class DatabaseServerTest extends Scope $this->assertArrayNotHasKey('errors', $row['body']); $row = $row['body']['data']['tablesDBCreateRow']; - static::$cachedRowData[$cacheKey] = [ + self::$cachedRowData[$cacheKey] = [ 'database' => $data['database'], 'table' => $data['table'], 'row' => $row, ]; - return static::$cachedRowData[$cacheKey]; + return self::$cachedRowData[$cacheKey]; } protected function setupBulkData(): array { $cacheKey = $this->getProject()['$id'] ?? 'default'; - if (!empty(static::$cachedBulkData[$cacheKey])) { - return static::$cachedBulkData[$cacheKey]; + if (!empty(self::$cachedBulkData[$cacheKey])) { + return self::$cachedBulkData[$cacheKey]; } $project = $this->getProject(); @@ -1034,9 +1034,9 @@ class DatabaseServerTest extends Scope $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload); - static::$cachedBulkData[$cacheKey] = compact('databaseId', 'tableId', 'projectId'); + self::$cachedBulkData[$cacheKey] = compact('databaseId', 'tableId', 'projectId'); - return static::$cachedBulkData[$cacheKey]; + return self::$cachedBulkData[$cacheKey]; } public function testCreateDatabase(): void @@ -1088,7 +1088,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedStringColumnData[$cacheKey] = $data; + self::$cachedStringColumnData[$cacheKey] = $data; } /** @@ -1166,7 +1166,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedIntegerColumnData[$cacheKey] = $data; + self::$cachedIntegerColumnData[$cacheKey] = $data; } /** @@ -1246,7 +1246,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedBooleanColumnData[$cacheKey] = $data; + self::$cachedBooleanColumnData[$cacheKey] = $data; } /** @@ -1325,7 +1325,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedFloatColumnData[$cacheKey] = $data; + self::$cachedFloatColumnData[$cacheKey] = $data; } /** @@ -1405,7 +1405,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedEmailColumnData[$cacheKey] = $data; + self::$cachedEmailColumnData[$cacheKey] = $data; } /** @@ -1486,7 +1486,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedEnumColumnData[$cacheKey] = $data; + self::$cachedEnumColumnData[$cacheKey] = $data; } @@ -1570,7 +1570,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedDatetimeColumnData[$cacheKey] = $data; + self::$cachedDatetimeColumnData[$cacheKey] = $data; } /** @@ -1646,7 +1646,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedRelationshipColumnData[$cacheKey] = $data; + self::$cachedRelationshipColumnData[$cacheKey] = $data; } public function testUpdateRelationshipColumn(): void @@ -1717,7 +1717,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedIPColumnData[$cacheKey] = $data; + self::$cachedIPColumnData[$cacheKey] = $data; } /** @@ -1794,7 +1794,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedURLColumnData[$cacheKey] = $data; + self::$cachedURLColumnData[$cacheKey] = $data; } /** @@ -1877,7 +1877,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedIndexData[$cacheKey] = [ + self::$cachedIndexData[$cacheKey] = [ 'database' => $data['database'], 'table' => $data['table'], 'index' => $index['body']['data']['tablesDBCreateIndex'], @@ -1952,7 +1952,7 @@ class DatabaseServerTest extends Scope // Store for caching $cacheKey = $this->getProject()['$id'] ?? 'default'; - static::$cachedRowData[$cacheKey] = [ + self::$cachedRowData[$cacheKey] = [ 'database' => $data['database'], 'table' => $data['table'], 'row' => $row, diff --git a/tests/e2e/Services/GraphQL/TeamsClientTest.php b/tests/e2e/Services/GraphQL/TeamsClientTest.php index 44cf3c9d12..e6c27f44f8 100644 --- a/tests/e2e/Services/GraphQL/TeamsClientTest.php +++ b/tests/e2e/Services/GraphQL/TeamsClientTest.php @@ -20,8 +20,8 @@ class TeamsClientTest extends Scope protected function setupTeam(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedTeam[$key])) { - return static::$cachedTeam[$key]; + if (!empty(self::$cachedTeam[$key])) { + return self::$cachedTeam[$key]; } $projectId = $this->getProject()['$id']; @@ -45,15 +45,15 @@ class TeamsClientTest extends Scope $team = $team['body']['data']['teamsCreate']; $this->assertEquals('Team Name', $team['name']); - static::$cachedTeam[$key] = $team; + self::$cachedTeam[$key] = $team; return $team; } protected function setupMembership(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedMembership[$key])) { - return static::$cachedMembership[$key]; + if (!empty(self::$cachedMembership[$key])) { + return self::$cachedMembership[$key]; } $team = $this->setupTeam(); @@ -82,7 +82,7 @@ class TeamsClientTest extends Scope $this->assertEquals($team['_id'], $membership['teamId']); $this->assertEquals(['developer'], $membership['roles']); - static::$cachedMembership[$key] = $membership; + self::$cachedMembership[$key] = $membership; return $membership; } @@ -211,6 +211,6 @@ class TeamsClientTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedMembership[$key] = []; + self::$cachedMembership[$key] = []; } } diff --git a/tests/e2e/Services/GraphQL/TeamsServerTest.php b/tests/e2e/Services/GraphQL/TeamsServerTest.php index bd0939040c..ff6e8e3c6f 100644 --- a/tests/e2e/Services/GraphQL/TeamsServerTest.php +++ b/tests/e2e/Services/GraphQL/TeamsServerTest.php @@ -22,8 +22,8 @@ class TeamsServerTest extends Scope protected function setupTeam(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedTeam[$key])) { - return static::$cachedTeam[$key]; + if (!empty(self::$cachedTeam[$key])) { + return self::$cachedTeam[$key]; } $projectId = $this->getProject()['$id']; @@ -47,15 +47,15 @@ class TeamsServerTest extends Scope $team = $team['body']['data']['teamsCreate']; $this->assertEquals('Team Name', $team['name']); - static::$cachedTeam[$key] = $team; + self::$cachedTeam[$key] = $team; return $team; } protected function setupMembership(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedMembership[$key])) { - return static::$cachedMembership[$key]; + if (!empty(self::$cachedMembership[$key])) { + return self::$cachedMembership[$key]; } $team = $this->setupTeam(); @@ -83,15 +83,15 @@ class TeamsServerTest extends Scope $this->assertEquals($team['_id'], $membership['teamId']); $this->assertEquals(['developer'], $membership['roles']); - static::$cachedMembership[$key] = $membership; + self::$cachedMembership[$key] = $membership; return $membership; } protected function setupTeamWithPrefs(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedTeamWithPrefs[$key])) { - return static::$cachedTeamWithPrefs[$key]; + if (!empty(self::$cachedTeamWithPrefs[$key])) { + return self::$cachedTeamWithPrefs[$key]; } $team = $this->setupTeam(); @@ -137,7 +137,7 @@ class TeamsServerTest extends Scope $this->assertIsArray($prefs['body']['data']['teamsUpdatePrefs']); $this->assertEquals('{"key":"value"}', $prefs['body']['data']['teamsUpdatePrefs']['data']); - static::$cachedTeamWithPrefs[$key] = $fetchedTeam; + self::$cachedTeamWithPrefs[$key] = $fetchedTeam; return $fetchedTeam; } @@ -349,7 +349,7 @@ class TeamsServerTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedMembership[$key] = []; + self::$cachedMembership[$key] = []; } #[Group('cl-ignore')] diff --git a/tests/e2e/Services/GraphQL/UsersTest.php b/tests/e2e/Services/GraphQL/UsersTest.php index da9f761567..efe99531be 100644 --- a/tests/e2e/Services/GraphQL/UsersTest.php +++ b/tests/e2e/Services/GraphQL/UsersTest.php @@ -21,8 +21,8 @@ class UsersTest extends Scope protected function setupUser(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedUser[$key])) { - return static::$cachedUser[$key]; + if (!empty(self::$cachedUser[$key])) { + return self::$cachedUser[$key]; } $projectId = $this->getProject()['$id']; @@ -50,15 +50,15 @@ class UsersTest extends Scope $this->assertEquals('Project User', $user['name']); $this->assertEquals($email, $user['email']); - static::$cachedUser[$key] = $user; + self::$cachedUser[$key] = $user; return $user; } protected function setupUserTarget(): array { $key = $this->getProject()['$id']; - if (!empty(static::$cachedUserTarget[$key])) { - return static::$cachedUserTarget[$key]; + if (!empty(self::$cachedUserTarget[$key])) { + return self::$cachedUserTarget[$key]; } $user = $this->setupUser(); @@ -106,8 +106,8 @@ class UsersTest extends Scope $this->assertEquals(200, $target['headers']['status-code']); $this->assertEquals('random-email@mail.org', $target['body']['data']['usersCreateTarget']['identifier']); - static::$cachedUserTarget[$key] = $target['body']['data']['usersCreateTarget']; - return static::$cachedUserTarget[$key]; + self::$cachedUserTarget[$key] = $target['body']['data']['usersCreateTarget']; + return self::$cachedUserTarget[$key]; } public function testCreateUser(): void @@ -581,7 +581,7 @@ class UsersTest extends Scope // Clear cache after deletion $key = $this->getProject()['$id']; - static::$cachedUserTarget[$key] = []; + self::$cachedUserTarget[$key] = []; } public function testDeleteUser() diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 1e8b1f5ad3..6b446145fe 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -3825,4 +3825,714 @@ trait MigrationsBase 'x-appwrite-key' => $sourceProject['apiKey'], ]); } + + public function testCreateJSONImport(): void + { + // Make a database + $response = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Test Database' + ]); + + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals('Test Database', $response['body']['name']); + + $databaseId = $response['body']['$id']; + + // make a table + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'name' => 'Test table', + 'tableId' => ID::unique(), + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals($response['body']['name'], 'Test table'); + + $tableId = $response['body']['$id']; + + // make columns + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals(202, $response['headers']['status-code']); + $this->assertEquals($response['body']['key'], 'name'); + $this->assertEquals($response['body']['type'], 'string'); + $this->assertEquals($response['body']['size'], 256); + $this->assertEquals($response['body']['required'], true); + + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'age', + 'min' => 18, + 'max' => 65, + 'required' => true, + ]); + + $this->assertEquals(202, $response['headers']['status-code']); + $this->assertEquals($response['body']['key'], 'age'); + $this->assertEquals($response['body']['type'], 'integer'); + $this->assertEquals($response['body']['min'], 18); + $this->assertEquals($response['body']['max'], 65); + $this->assertEquals($response['body']['required'], true); + + // make a bucket, upload a file to it! + $bucketOne = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => ID::unique(), + 'name' => 'Test Bucket', + 'maximumFileSize' => 2000000, //2MB + 'allowedFileExtensions' => ['json'], + 'compression' => 'gzip', + 'encryption' => true + ]); + $this->assertEquals(201, $bucketOne['headers']['status-code']); + $this->assertNotEmpty($bucketOne['body']['$id']); + + $bucketOneId = $bucketOne['body']['$id']; + + $bucketIds = [ + 'default' => $bucketOneId, + 'missing-column' => $bucketOneId, + 'irrelevant-column' => $bucketOneId, + 'documents-internals' => $bucketOneId, + ]; + + $fileIds = []; + + foreach ($bucketIds as $label => $bucketId) { + $jsonFileName = match ($label) { + 'missing-column', + 'irrelevant-column', + 'documents-internals' => "$label.json", + default => 'documents.json', + }; + + $response = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/json/'.$jsonFileName), 'application/json', $jsonFileName), + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertEquals($jsonFileName, $response['body']['name']); + $this->assertEquals('application/json', $response['body']['mimeType']); + + $fileIds[$label] = $response['body']['$id']; + } + + // missing column, fail in worker. + $missingColumn = $this->performJsonMigration( + [ + 'fileId' => $fileIds['missing-column'], + 'bucketId' => $bucketIds['missing-column'], + 'resourceId' => $databaseId . ':' . $tableId, + ] + ); + + $this->assertEventually(function () use ($missingColumn) { + $migrationId = $missingColumn['body']['$id']; + $migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $migration['headers']['status-code']); + $this->assertEquals('finished', $migration['body']['stage']); + $this->assertEquals('failed', $migration['body']['status']); + $this->assertEquals('JSON', $migration['body']['source']); + $this->assertEquals('Appwrite', $migration['body']['destination']); + $this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']); + + /* fails in batch create documents unlike csv which checks headers first! */ + $this->assertArrayHasKey(Resource::TYPE_ROW, $migration['body']['statusCounters']); + $this->assertGreaterThan(0, $migration['body']['statusCounters'][Resource::TYPE_ROW]['error']); + + $this->assertThat( + implode("\n", $migration['body']['errors']), + $this->stringContains('Missing required attribute') + ); + $this->assertThat( + implode("\n", $migration['body']['errors']), + $this->stringContains('age') + ); + }, 60_000, 500); + + // irrelevant column - email, success. + $irrelevantColumn = $this->performJsonMigration( + [ + 'fileId' => $fileIds['irrelevant-column'], + 'bucketId' => $bucketIds['irrelevant-column'], + 'resourceId' => $databaseId . ':' . $tableId, + ] + ); + + $this->assertEventually(function () use ($irrelevantColumn) { + $migrationId = $irrelevantColumn['body']['$id']; + $migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $migration['headers']['status-code']); + $this->assertEquals('finished', $migration['body']['stage']); + $this->assertEquals('completed', $migration['body']['status']); + $this->assertEquals('JSON', $migration['body']['source']); + $this->assertEquals('Appwrite', $migration['body']['destination']); + $this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']); + $this->assertArrayHasKey(Resource::TYPE_ROW, $migration['body']['statusCounters']); + $this->assertEquals(100, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']); + }, 10_000, 500); + + // all data exists, pass. + $migration = $this->performJsonMigration( + [ + 'endpoint' => $this->endpoint, + 'fileId' => $fileIds['default'], + 'bucketId' => $bucketIds['default'], + 'resourceId' => $databaseId . ':' . $tableId, + ] + ); + + $this->assertEventually(function () use ($migration, $databaseId, $tableId) { + $migrationId = $migration['body']['$id']; + $migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $migration['headers']['status-code']); + $this->assertEquals('finished', $migration['body']['stage']); + $this->assertEquals('completed', $migration['body']['status']); + $this->assertEquals('JSON', $migration['body']['source']); + $this->assertEquals('Appwrite', $migration['body']['destination']); + $this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']); + $this->assertArrayHasKey(Resource::TYPE_ROW, $migration['body']['statusCounters']); + $this->assertEquals(100, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']); + }, 10_000, 500); + + // get rows count + $rows = $this->client->call(Client::METHOD_GET, '/tablesdb/'.$databaseId.'/tables/'.$tableId.'/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::limit(250)->toString() + ] + ]); + + $this->assertEquals(200, $rows['headers']['status-code']); + $this->assertIsArray($rows['body']['rows']); + $this->assertIsNumeric($rows['body']['total']); + $this->assertEquals(200, $rows['body']['total']); + + // all data exists and includes internals, pass. + $migration = $this->performJsonMigration( + [ + 'endpoint' => $this->endpoint, + 'fileId' => $fileIds['documents-internals'], + 'bucketId' => $bucketIds['documents-internals'], + 'resourceId' => $databaseId . ':' . $tableId, + ] + ); + + $this->assertEventually(function () use ($migration) { + $migrationId = $migration['body']['$id']; + $migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $migration['headers']['status-code']); + $this->assertEquals('finished', $migration['body']['stage']); + $this->assertEquals('completed', $migration['body']['status']); + $this->assertEquals('JSON', $migration['body']['source']); + $this->assertEquals('Appwrite', $migration['body']['destination']); + $this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']); + $this->assertArrayHasKey(Resource::TYPE_ROW, $migration['body']['statusCounters']); + $this->assertEquals(25, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']); + }, 10_000, 500); + } + + private function performJsonMigration(array $body): array + { + return $this->client->call(Client::METHOD_POST, '/migrations/json/imports', [ + 'content-type' => 'application/json', + 'x-appwrite-key' => $this->getProject()['apiKey'], + 'x-appwrite-project' => $this->getProject()['$id'], + ], $body); + } + + /** + * Test JSON export with email notification + */ + public function testCreateJSONExport(): void + { + // Create a database + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Test Export Database' + ]); + + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create a collection + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'collectionId' => ID::unique(), + 'name' => 'Test Export Collection', + 'permissions' => [] + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create a simple attribute like the basic test + $name = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'name', + 'size' => 255, + 'required' => true, + ]); + + $this->assertEquals(202, $name['headers']['status-code']); + + // Create a simple attribute like the basic test + $email = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'key' => 'email', + 'size' => 255, + 'required' => false, + ]); + + $this->assertEquals(202, $email['headers']['status-code']); + + \sleep(3); + + // Create sample documents + for ($i = 1; $i <= 10; $i++) { + $doc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'Test User ' . $i, + 'email' => 'user' . $i . '@appwrite.io' + ] + ]); + + $this->assertEquals(201, $doc['headers']['status-code'], 'Failed to create document ' . $i); + } + + // Verify documents were created + $docs = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]); + + $this->assertEquals(200, $docs['headers']['status-code']); + $this->assertEquals(10, $docs['body']['total'], 'Expected 10 documents but got ' . $docs['body']['total']); + + // Perform JSON export with notification enabled (uses internal bucket) + $migration = $this->client->call(Client::METHOD_POST, '/migrations/json/exports', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), [ + 'resourceId' => $databaseId . ':' . $collectionId, + 'filename' => 'test-json-export', + 'columns' => [], + 'queries' => [], + 'notify' => true + ]); + + $this->assertEquals(202, $migration['headers']['status-code']); + $this->assertNotEmpty($migration['body']['$id']); + $migrationId = $migration['body']['$id']; + + $this->assertEventually(function () use ($migrationId) { + $response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('finished', $response['body']['stage']); + $this->assertEquals('completed', $response['body']['status']); + $this->assertEquals('Appwrite', $response['body']['source']); + $this->assertEquals('JSON', $response['body']['destination']); + + return true; + }, 30_000, 500); + + // Check that email was sent with download link + $lastEmail = $this->getLastEmail(); + $this->assertNotEmpty($lastEmail); + $this->assertEquals('Your JSON export is ready', $lastEmail['subject']); + $this->assertStringContainsStringIgnoringCase('Your data export has been completed successfully', $lastEmail['text']); + + // Extract download URL from email HTML + \preg_match('/href="([^"]*\/storage\/buckets\/[^"]*\/push[^"]*)"/', $lastEmail['html'], $matches); + $this->assertNotEmpty($matches[1], 'Download URL not found in email'); + $downloadUrl = html_entity_decode($matches[1]); + + // Parse the URL to extract components + $components = \parse_url($downloadUrl); + $this->assertNotEmpty($components); + \parse_str($components['query'] ?? '', $queryParams); + $this->assertArrayHasKey('jwt', $queryParams, 'JWT not found in download URL'); + $this->assertNotEmpty($queryParams['jwt']); + $this->assertArrayHasKey('project', $queryParams, 'Project not found in download URL'); + $this->assertStringContainsString('/storage/buckets/default/files/', $downloadUrl); + + // Test download with JWT + $path = \str_replace('/v1', '', $components['path']); + $downloadWithJwt = $this->client->call(Client::METHOD_GET, $path . '?project=' . $queryParams['project'] . '&jwt=' . $queryParams['jwt']); + $this->assertEquals(200, $downloadWithJwt['headers']['status-code'], 'Failed to download file with JWT'); + + // Verify the downloaded content is valid JSON + $jsonData = $downloadWithJwt['body']; + $this->assertNotEmpty($jsonData, 'JSON export should not be empty'); + $decoded = json_decode($jsonData, true); + $this->assertIsArray($decoded, 'JSON should be valid and decodable'); + $this->assertCount(10, $decoded, 'JSON should contain 10 documents'); + $this->assertArrayHasKey('name', $decoded[0], 'JSON documents should contain name field'); + $this->assertArrayHasKey('email', $decoded[0], 'JSON documents should contain email field'); + $this->assertStringContainsString('Test User', $decoded[0]['name'], 'JSON should contain test data'); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]); + } + + public function testCreateVectorsDBJSONExport(): void + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]; + + // Create vectorsdb database + $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', $headers, [ + 'databaseId' => ID::unique(), + 'name' => 'VectorsDB Export Test' + ]); + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection with dimension 16 + $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $headers, [ + 'collectionId' => ID::unique(), + 'name' => 'VecExportCol', + 'dimension' => 16, + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Seed 5 documents + for ($i = 1; $i <= 5; $i++) { + $embeddings = array_map(fn () => round((mt_rand() / mt_getrandmax()) * 2 - 1, 6), range(1, 16)); + $doc = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId . '/documents', $headers, [ + 'documentId' => ID::unique(), + 'data' => [ + 'embeddings' => $embeddings, + 'metadata' => ['title' => 'Doc ' . $i, 'score' => round($i * 0.2, 1)] + ] + ]); + $this->assertEquals(201, $doc['headers']['status-code'], 'Failed to create vector document ' . $i); + } + + // Trigger JSON export + $migration = $this->client->call(Client::METHOD_POST, '/migrations/json/exports', $headers, [ + 'resourceId' => $databaseId . ':' . $collectionId, + 'filename' => 'vectorsdb-export-test', + 'columns' => [], + 'queries' => [], + 'notify' => false, + ]); + $this->assertEquals(202, $migration['headers']['status-code']); + $migrationId = $migration['body']['$id']; + + // Poll until completed + $this->assertEventually(function () use ($migrationId, $headers) { + $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, $headers); + + $this->assertEquals(200, $migration['headers']['status-code']); + $this->assertEquals('finished', $migration['body']['stage']); + $this->assertEquals('completed', $migration['body']['status']); + $this->assertEquals('Appwrite', $migration['body']['source']); + $this->assertEquals('JSON', $migration['body']['destination']); + }, 30_000, 500); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, $headers); + } + + public function testCreateVectorsDBJSONImport(): void + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]; + + // Create vectorsdb database + $database = $this->client->call(Client::METHOD_POST, '/vectorsdb', $headers, [ + 'databaseId' => ID::unique(), + 'name' => 'VectorsDB Import Test' + ]); + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection with dimension 16 + $collection = $this->client->call(Client::METHOD_POST, '/vectorsdb/' . $databaseId . '/collections', $headers, [ + 'collectionId' => ID::unique(), + 'name' => 'VecImportCol', + 'dimension' => 16, + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create bucket and upload test file + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', $headers, [ + 'bucketId' => ID::unique(), + 'name' => 'VectorsDB Import Bucket', + 'maximumFileSize' => 2000000, + 'allowedFileExtensions' => ['json'], + ]); + $this->assertEquals(201, $bucket['headers']['status-code']); + $bucketId = $bucket['body']['$id']; + + $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new \CURLFile(realpath(__DIR__ . '/../../../resources/json/vectorsdb-documents.json'), 'application/json', 'vectorsdb-documents.json'), + ]); + $this->assertEquals(201, $file['headers']['status-code']); + $fileId = $file['body']['$id']; + + // Trigger import + $migration = $this->performJsonMigration([ + 'fileId' => $fileId, + 'bucketId' => $bucketId, + 'resourceId' => $databaseId . ':' . $collectionId, + ]); + $this->assertEquals(202, $migration['headers']['status-code']); + + // Poll until completed + $this->assertEventually(function () use ($migration, $headers) { + $migrationId = $migration['body']['$id']; + $result = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, $headers); + + $this->assertEquals(200, $result['headers']['status-code']); + $this->assertEquals('finished', $result['body']['stage']); + $this->assertEquals('completed', $result['body']['status']); + $this->assertEquals('JSON', $result['body']['source']); + $this->assertEquals('Appwrite', $result['body']['destination']); + }, 30_000, 500); + + // Verify documents were imported + $docs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $collectionId . '/documents', $headers); + $this->assertEquals(200, $docs['headers']['status-code']); + $this->assertEquals(10, $docs['body']['total'], 'Should have imported 10 vectorsdb documents'); + + // Verify first document structure + $firstDoc = $docs['body']['documents'][0]; + $this->assertArrayHasKey('embeddings', $firstDoc); + $this->assertCount(16, $firstDoc['embeddings'], 'Imported embeddings should have 16 dimensions'); + $this->assertArrayHasKey('metadata', $firstDoc); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/vectorsdb/' . $databaseId, $headers); + } + + public function testCreateDocumentsDBJSONExport(): void + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]; + + // Create documentsdb database + $database = $this->client->call(Client::METHOD_POST, '/documentsdb', $headers, [ + 'databaseId' => ID::unique(), + 'name' => 'DocumentsDB Export Test' + ]); + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection (schemaless — no attributes needed) + $collection = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $databaseId . '/collections', $headers, [ + 'collectionId' => ID::unique(), + 'name' => 'DocExportCol', + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Seed 5 documents + for ($i = 1; $i <= 5; $i++) { + $doc = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $databaseId . '/collections/' . $collectionId . '/documents', $headers, [ + 'documentId' => ID::unique(), + 'data' => [ + 'name' => 'User ' . $i, + 'email' => 'user' . $i . '@test.com', + 'age' => 20 + $i, + 'address' => ['city' => 'City ' . $i, 'zip' => '1000' . $i] + ] + ]); + $this->assertEquals(201, $doc['headers']['status-code'], 'Failed to create document ' . $i); + } + + // Trigger JSON export + $migration = $this->client->call(Client::METHOD_POST, '/migrations/json/exports', $headers, [ + 'resourceId' => $databaseId . ':' . $collectionId, + 'filename' => 'documentsdb-export-test', + 'columns' => [], + 'queries' => [], + 'notify' => false, + ]); + $this->assertEquals(202, $migration['headers']['status-code']); + $migrationId = $migration['body']['$id']; + + // Poll until completed + $this->assertEventually(function () use ($migrationId, $headers) { + $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, $headers); + + $this->assertEquals(200, $migration['headers']['status-code']); + $this->assertEquals('finished', $migration['body']['stage']); + $this->assertEquals('completed', $migration['body']['status']); + $this->assertEquals('Appwrite', $migration['body']['source']); + $this->assertEquals('JSON', $migration['body']['destination']); + }, 30_000, 500); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/documentsdb/' . $databaseId, $headers); + } + + public function testCreateDocumentsDBJSONImport(): void + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]; + + // Create documentsdb database + $database = $this->client->call(Client::METHOD_POST, '/documentsdb', $headers, [ + 'databaseId' => ID::unique(), + 'name' => 'DocumentsDB Import Test' + ]); + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + // Create collection (schemaless) + $collection = $this->client->call(Client::METHOD_POST, '/documentsdb/' . $databaseId . '/collections', $headers, [ + 'collectionId' => ID::unique(), + 'name' => 'DocImportCol', + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + // Create bucket and upload test file + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', $headers, [ + 'bucketId' => ID::unique(), + 'name' => 'DocumentsDB Import Bucket', + 'maximumFileSize' => 2000000, + 'allowedFileExtensions' => ['json'], + ]); + $this->assertEquals(201, $bucket['headers']['status-code']); + $bucketId = $bucket['body']['$id']; + + $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new \CURLFile(realpath(__DIR__ . '/../../../resources/json/documentsdb-documents.json'), 'application/json', 'documentsdb-documents.json'), + ]); + $this->assertEquals(201, $file['headers']['status-code']); + $fileId = $file['body']['$id']; + + // Trigger import + $migration = $this->performJsonMigration([ + 'fileId' => $fileId, + 'bucketId' => $bucketId, + 'resourceId' => $databaseId . ':' . $collectionId, + ]); + $this->assertEquals(202, $migration['headers']['status-code']); + + // Poll until completed + $this->assertEventually(function () use ($migration, $headers) { + $migrationId = $migration['body']['$id']; + $result = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, $headers); + + $this->assertEquals(200, $result['headers']['status-code']); + $this->assertEquals('finished', $result['body']['stage']); + $this->assertEquals('completed', $result['body']['status']); + $this->assertEquals('JSON', $result['body']['source']); + $this->assertEquals('Appwrite', $result['body']['destination']); + }, 30_000, 500); + + // Verify documents were imported + $docs = $this->client->call(Client::METHOD_GET, '/documentsdb/' . $databaseId . '/collections/' . $collectionId . '/documents', $headers); + $this->assertEquals(200, $docs['headers']['status-code']); + $this->assertEquals(10, $docs['body']['total'], 'Should have imported 10 documentsdb documents'); + + // Verify first document has nested data + $firstDoc = $docs['body']['documents'][0]; + $this->assertArrayHasKey('name', $firstDoc); + $this->assertArrayHasKey('address', $firstDoc); + $this->assertIsArray($firstDoc['address']); + + // Cleanup + $this->client->call(Client::METHOD_DELETE, '/documentsdb/' . $databaseId, $headers); + } + } diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php index f6200ed209..03723bf231 100644 --- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php @@ -3039,8 +3039,21 @@ class RealtimeCustomClientTest extends Scope $this->assertEquals(200, $update['headers']['status-code']); - $event = json_decode($client->receive(), true); + // Drain WebSocket messages until the .update event arrives. + // Earlier events (e.g. a late-arriving .create from the row seed above) are skipped. + $updateEvent = "tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}.update"; + $event = null; + $deadline = \time() + 10; + while (\time() < $deadline) { + $raw = $client->receive(); + $msg = json_decode($raw, true); + if (($msg['type'] ?? '') === 'event' && \in_array($updateEvent, $msg['data']['events'] ?? [])) { + $event = $msg; + break; + } + } + $this->assertNotNull($event, 'Timed out waiting for the row update event'); $this->assertArrayHasKey('type', $event); $this->assertArrayHasKey('data', $event); $this->assertEquals('event', $event['type']); diff --git a/tests/e2e/Services/Tokens/TokensBase.php b/tests/e2e/Services/Tokens/TokensBase.php index ced6bb5dde..1fcdeb347f 100644 --- a/tests/e2e/Services/Tokens/TokensBase.php +++ b/tests/e2e/Services/Tokens/TokensBase.php @@ -14,8 +14,8 @@ trait TokensBase protected function setupBucketAndFile(): array { - if (!empty(static::$bucketAndFileData)) { - return static::$bucketAndFileData; + if (!empty(self::$bucketAndFileData)) { + return self::$bucketAndFileData; } $bucket = $this->client->call( @@ -61,7 +61,7 @@ trait TokensBase ] ); - static::$bucketAndFileData = [ + self::$bucketAndFileData = [ 'fileId' => $fileId, 'bucketId' => $bucketId, 'token' => $token['body'], @@ -72,7 +72,7 @@ trait TokensBase ], ]; - return static::$bucketAndFileData; + return self::$bucketAndFileData; } public function testCreateBucketAndFile(): void diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index b7f188f5b5..601bf1d2d0 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -25,8 +25,8 @@ class TokensConsoleClientTest extends Scope protected function setupToken(): array { - if (!empty(static::$tokenData)) { - return static::$tokenData; + if (!empty(self::$tokenData)) { + return self::$tokenData; } $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', array_merge([ @@ -68,13 +68,13 @@ class TokensConsoleClientTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'] ], $this->getHeaders())); - static::$tokenData = [ + self::$tokenData = [ 'fileId' => $fileId, 'bucketId' => $bucketId, 'tokenId' => $token['body']['$id'], ]; - return static::$tokenData; + return self::$tokenData; } public function testCreateToken(): void diff --git a/tests/e2e/Services/Tokens/TokensCustomServerTest.php b/tests/e2e/Services/Tokens/TokensCustomServerTest.php index ecb9bafc89..3efa0adbe1 100644 --- a/tests/e2e/Services/Tokens/TokensCustomServerTest.php +++ b/tests/e2e/Services/Tokens/TokensCustomServerTest.php @@ -22,8 +22,8 @@ class TokensCustomServerTest extends Scope protected function setupToken(): array { - if (!empty(static::$tokenData)) { - return static::$tokenData; + if (!empty(self::$tokenData)) { + return self::$tokenData; } $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ @@ -66,13 +66,13 @@ class TokensCustomServerTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'] ], $this->getHeaders())); - static::$tokenData = [ + self::$tokenData = [ 'fileId' => $fileId, 'bucketId' => $bucketId, 'tokenId' => $token['body']['$id'], ]; - return static::$tokenData; + return self::$tokenData; } public function testCreateToken(): void diff --git a/tests/resources/json/documents-internals.json b/tests/resources/json/documents-internals.json new file mode 100644 index 0000000000..6fe6820cdf --- /dev/null +++ b/tests/resources/json/documents-internals.json @@ -0,0 +1,254 @@ +[ + { + "$id": "z1y2x3w4v5u6t7s8", + "$createdAt": "2022-10-23T10:33:01+00:00", + "$updatedAt": "2023-03-15T12:00:41+00:00", + "$permissions": [ + "read(\"any\")", + "update(\"user:123\")" + ], + "name": "Diamond Mendez", + "age": 56 + }, + { + "$id": "r9q0p1o2n3m4l5k6", + "$createdAt": "2021-08-11T21:05:13+00:00", + "$updatedAt": "2024-01-02T08:45:22+00:00", + "$permissions": [ + "read(\"any\")", + "update(\"user:456\")" + ], + "name": "Michael Huff", + "age": 20 + }, + { + "$id": "j7i8h9g0f1e2d3c4", + "$createdAt": "2020-05-29T14:22:56+00:00", + "$updatedAt": "2022-11-30T18:19:33+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Alyssa Rodriguez", + "age": 37 + }, + { + "$id": "b5a6z7y8x9w0v1u2", + "$createdAt": "2023-01-18T03:44:09+00:00", + "$updatedAt": "2023-09-07T23:50:17+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Barbara Smith", + "age": 26 + }, + { + "$id": "t3s4r5q6p7o8n9m0", + "$createdAt": "2020-11-02T09:12:45+00:00", + "$updatedAt": "2021-07-21T15:30:55+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Evelyn Edwards", + "age": 54 + }, + { + "$id": "l1k2j3i4h5g6f7e8", + "$createdAt": "2022-03-19T19:55:27+00:00", + "$updatedAt": "2024-05-14T06:28:11+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Tina Richardson", + "age": 41 + }, + { + "$id": "d9c0b1a2z3y4x5w6", + "$createdAt": "2021-04-07T01:18:34+00:00", + "$updatedAt": "2023-06-25T11:47:04+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Joel Hernandez", + "age": 49 + }, + { + "$id": "v7u8t9s0r1q2p3o4", + "$createdAt": "2023-08-22T16:40:18+00:00", + "$updatedAt": "2024-02-19T04:09:58+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Zachary Cooper", + "age": 59 + }, + { + "$id": "n5m6l7k8j9i0h1g2", + "$createdAt": "2020-02-12T07:59:01+00:00", + "$updatedAt": "2022-09-08T13:21:49+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Brittany Spears", + "age": 20 + }, + { + "$id": "f3e4d5c6b7a8z9y0", + "$createdAt": "2021-12-05T22:33:12+00:00", + "$updatedAt": "2023-11-11T02:55:37+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Holly White", + "age": 47 + }, + { + "$id": "x1w2v3u4t5s6r7q8", + "$createdAt": "2022-07-14T05:01:50+00:00", + "$updatedAt": "2024-04-01T20:10:26+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Kimberly Barnes", + "age": 27 + }, + { + "$id": "p9o0n1m2l3k4j5i6", + "$createdAt": "2020-09-28T11:27:36+00:00", + "$updatedAt": "2021-10-17T09:38:08+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Stephen Miller", + "age": 53 + }, + { + "$id": "h7g8f9e0d1c2b3a4", + "$createdAt": "2023-04-04T08:15:59+00:00", + "$updatedAt": "2024-06-29T17:03:14+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Yvonne Newman", + "age": 41 + }, + { + "$id": "y5x6w7v8u9t0s1r2", + "$createdAt": "2021-01-25T18:09:21+00:00", + "$updatedAt": "2022-08-16T22:44:51+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Carol Kane", + "age": 38 + }, + { + "$id": "q3p4o5n6m7l8k9j0", + "$createdAt": "2022-06-09T12:53:47+00:00", + "$updatedAt": "2023-12-24T01:16:05+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Doris Foster", + "age": 44 + }, + { + "$id": "i1h2g3f4e5d6c7b8", + "$createdAt": "2020-07-03T23:37:02+00:00", + "$updatedAt": "2021-05-09T05:52:43+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Joseph Stokes", + "age": 28 + }, + { + "$id": "a9z0y1x2w3v4u5t6", + "$createdAt": "2023-10-10T02:20:15+00:00", + "$updatedAt": "2024-03-28T14:33:29+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Steve Williams", + "age": 31 + }, + { + "$id": "s7r8q9p0o1n2m3l4", + "$createdAt": "2021-06-16T13:48:53+00:00", + "$updatedAt": "2022-04-22T07:07:19+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "James Carey", + "age": 29 + }, + { + "$id": "k5j6i7h8g9f0e1d2", + "$createdAt": "2022-12-27T20:06:38+00:00", + "$updatedAt": "2023-08-03T10:25:57+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Kathryn Henry", + "age": 38 + }, + { + "$id": "c3b4a5z6y7x8w9v0", + "$createdAt": "2020-04-20T04:41:24+00:00", + "$updatedAt": "2021-02-13T19:14:06+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Christopher Landry", + "age": 23 + }, + { + "$id": "u1t2s3r4q5p6o7n8", + "$createdAt": "2023-05-08T00:58:10+00:00", + "$updatedAt": "2024-07-05T03:36:48+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Jennifer Mcgee", + "age": 62 + }, + { + "$id": "m9l0k1j2i3h4g5f6", + "$createdAt": "2021-09-01T06:11:42+00:00", + "$updatedAt": "2022-01-26T16:59:23+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Cathy Church", + "age": 35 + }, + { + "$id": "e7d8c9b0a1z2y3x4", + "$createdAt": "2022-02-18T15:24:07+00:00", + "$updatedAt": "2023-04-12T00:40:31+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Jose Lopez", + "age": 41 + }, + { + "$id": "w5v6u7t8s9r0q1p2", + "$createdAt": "2020-12-13T09:03:55+00:00", + "$updatedAt": "2021-11-06T11:23:16+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "William Rose", + "age": 30 + }, + { + "$id": "o3n4m5l6k7j8i9h0", + "$createdAt": "2021-12-13T09:03:55+00:00", + "$updatedAt": "2022-11-06T11:23:16+00:00", + "$permissions": [ + "read(\"any\")" + ], + "name": "Charles Hammer", + "age": 61 + } +] \ No newline at end of file diff --git a/tests/resources/json/documents.json b/tests/resources/json/documents.json new file mode 100644 index 0000000000..1ce5533297 --- /dev/null +++ b/tests/resources/json/documents.json @@ -0,0 +1,502 @@ +[ + { + "$id": "hxfcwpcas5xokpwe", + "name": "Diamond Mendez", + "age": 56 + }, + { + "$id": "gw8nxwf6esn3tfwf", + "name": "Michael Huff", + "age": 20 + }, + { + "$id": "xb6bxg56lral1qy9", + "name": "Alyssa Rodriguez", + "age": 37 + }, + { + "$id": "imerjq5j36y3agh2", + "name": "Barbara Smith", + "age": 26 + }, + { + "$id": "07yq9qdlhmbzmr35", + "name": "Evelyn Edwards", + "age": 54 + }, + { + "$id": "ksqo631sbhwj5ltg", + "name": "Tina Richardson", + "age": 41 + }, + { + "$id": "j7zlndgu0gbshp15", + "name": "Joel Hernandez", + "age": 49 + }, + { + "$id": "mfntvnljrcmf7h6v", + "name": "Zachary Cooper", + "age": 59 + }, + { + "$id": "5f9b01nziqu2h8ed", + "name": "Brittany Spears", + "age": 20 + }, + { + "$id": "4vxzbnzraqznk5u8", + "name": "Holly White", + "age": 47 + }, + { + "$id": "d4ywy3mtphaatbpf", + "name": "Kimberly Barnes", + "age": 27 + }, + { + "$id": "88odnk6nthyyvbal", + "name": "Stephen Miller", + "age": 53 + }, + { + "$id": "08oekee3fn7mzaa5", + "name": "Yvonne Newman", + "age": 41 + }, + { + "$id": "quw55kn9895i5e4v", + "name": "Carol Kane", + "age": 38 + }, + { + "$id": "nge6bm8ykripei6f", + "name": "Doris Foster", + "age": 44 + }, + { + "$id": "4k16i33s0xl2ypx9", + "name": "Joseph Stokes", + "age": 28 + }, + { + "$id": "q0j5rxbgid66snyf", + "name": "Steve Williams", + "age": 31 + }, + { + "$id": "n1oxun7mqq3p103y", + "name": "James Carey", + "age": 29 + }, + { + "$id": "0dbvs840jkf8i0ye", + "name": "Kathryn Henry", + "age": 38 + }, + { + "$id": "5sfaidgs1h87v15v", + "name": "Christopher Landry", + "age": 23 + }, + { + "$id": "vg3punvfu5khmf41", + "name": "Jennifer Mcgee", + "age": 62 + }, + { + "$id": "f933qydr9u5b2r11", + "name": "Cathy Church", + "age": 35 + }, + { + "$id": "wjv87y1inf8yk32s", + "name": "Jose Lopez", + "age": 41 + }, + { + "$id": "uljysdvdlcyrbrwk", + "name": "William Rose", + "age": 30 + }, + { + "$id": "ot8xtzh77j55wq0s", + "name": "Sarah Ford", + "age": 26 + }, + { + "$id": "9t76vnsv2u36s43t", + "name": "Alisha Jones", + "age": 61 + }, + { + "$id": "66y4tnty62hw8c02", + "name": "Kristin Kelly", + "age": 61 + }, + { + "$id": "2punfblazi5v16ar", + "name": "Brendan Stout", + "age": 40 + }, + { + "$id": "sxhr4nf5w2gx4wbg", + "name": "Kelly Cruz", + "age": 18 + }, + { + "$id": "68dvrqfwqnkq5el9", + "name": "Samantha Martin", + "age": 50 + }, + { + "$id": "20192l6dbeinhkh0", + "name": "David Santos", + "age": 46 + }, + { + "$id": "si0l4dgay09ebfmf", + "name": "Elizabeth Carroll", + "age": 22 + }, + { + "$id": "lhse40vbldqb6ap1", + "name": "Corey Owens", + "age": 46 + }, + { + "$id": "h5t3pslykyx3kxfm", + "name": "Shelby Mueller", + "age": 65 + }, + { + "$id": "ldc0luydrw6jub0f", + "name": "Dr. Sylvia Myers", + "age": 29 + }, + { + "$id": "voc9628xg4dsgw2y", + "name": "Scott Freeman", + "age": 48 + }, + { + "$id": "o4y0gk3gqv1ax2fz", + "name": "Christopher Atkinson", + "age": 21 + }, + { + "$id": "u1n3x4e4u7e0vzj6", + "name": "Sean Diaz", + "age": 31 + }, + { + "$id": "s36eskwtm0w7lwr7", + "name": "Bobby Dyer", + "age": 57 + }, + { + "$id": "4hjnag1p5iwvtixd", + "name": "Daniel Hall", + "age": 62 + }, + { + "$id": "m91d80oxsa216zbh", + "name": "Jennifer Ramirez", + "age": 65 + }, + { + "$id": "5hj6858zo2g85n6v", + "name": "Angela Jackson", + "age": 57 + }, + { + "$id": "8m8oihv9a1e7nn92", + "name": "Kelly Lewis", + "age": 36 + }, + { + "$id": "7azy39la0no0mxi7", + "name": "Jessica Munoz", + "age": 55 + }, + { + "$id": "47pmjkhnnqhyit8c", + "name": "Kelly George", + "age": 65 + }, + { + "$id": "6j6cpy4kgneg1mmh", + "name": "Anthony Johnson", + "age": 65 + }, + { + "$id": "tnlmtvap1zz89km9", + "name": "Regina Fields", + "age": 61 + }, + { + "$id": "6cyuvnwwqdmrpfzh", + "name": "Sharon Schaefer", + "age": 30 + }, + { + "$id": "p1v4pyu2pqodc0ey", + "name": "Jacob French", + "age": 62 + }, + { + "$id": "6npynnhjt2jd05xo", + "name": "Jessica Costa", + "age": 23 + }, + { + "$id": "wcxedf13n2e9qi4l", + "name": "George Hardy", + "age": 53 + }, + { + "$id": "yf2xlcmszk2tqeig", + "name": "Andrea Allison", + "age": 20 + }, + { + "$id": "3bf2zzv7poststwa", + "name": "Kevin Ferguson", + "age": 32 + }, + { + "$id": "c2iataz0hhv39q63", + "name": "Joseph Johnson", + "age": 58 + }, + { + "$id": "3e8npxhov4a39pvq", + "name": "Ashley Martinez", + "age": 18 + }, + { + "$id": "t7dp41tysipytywq", + "name": "Charles Nixon", + "age": 23 + }, + { + "$id": "z8cztq7c47phyfhk", + "name": "Carol Dudley", + "age": 40 + }, + { + "$id": "2636f9d8r4ipm3h6", + "name": "David Weber", + "age": 51 + }, + { + "$id": "eh3f6wxtvkjq6ykq", + "name": "Scott Robinson", + "age": 32 + }, + { + "$id": "raskbwpsje69a59h", + "name": "Anthony Hardy", + "age": 38 + }, + { + "$id": "90hn1p0b4cs9e2og", + "name": "Mackenzie Owens", + "age": 52 + }, + { + "$id": "am3swwfbo076x0v1", + "name": "Brian Foster", + "age": 27 + }, + { + "$id": "5uw7utb9lq5cfncw", + "name": "Hannah Forbes", + "age": 56 + }, + { + "$id": "cs6mbfzkzifefx6r", + "name": "Lauren Reed", + "age": 26 + }, + { + "$id": "ftw3uvztziiz9x00", + "name": "Morgan Smith", + "age": 28 + }, + { + "$id": "uhrqseeo43mozpaq", + "name": "Samantha Alexander", + "age": 65 + }, + { + "$id": "pvvmzyfc1lxor11e", + "name": "Tiffany Roberts", + "age": 20 + }, + { + "$id": "jia7bdag4abz123s", + "name": "Emily Hayes", + "age": 34 + }, + { + "$id": "h6oozcngbz8o5x4y", + "name": "Rebecca Villegas", + "age": 52 + }, + { + "$id": "9v6z1pn2f9twcy12", + "name": "Donald Shah", + "age": 61 + }, + { + "$id": "wzz3jduioso77o7f", + "name": "Denise Cain", + "age": 59 + }, + { + "$id": "u51plhgvjodkswnr", + "name": "Kristine Ramirez", + "age": 53 + }, + { + "$id": "t1uhkmiytfyc13vc", + "name": "Stacey Adkins", + "age": 61 + }, + { + "$id": "iqaqnf0ybg2ct507", + "name": "Daniel Hunt", + "age": 20 + }, + { + "$id": "idwrwv2uu4hcpv2i", + "name": "Roberta Johnson", + "age": 48 + }, + { + "$id": "2yd2hd6auetjacyo", + "name": "Jason Williamson", + "age": 39 + }, + { + "$id": "egrmdbibnjhi914x", + "name": "Sandra Robinson", + "age": 50 + }, + { + "$id": "15m1pz2bb0ercgyk", + "name": "Steve Rice", + "age": 25 + }, + { + "$id": "0i21bhkxdagjurb7", + "name": "Kimberly Fritz", + "age": 53 + }, + { + "$id": "726ofi7h5snreq67", + "name": "Brianna Reynolds", + "age": 33 + }, + { + "$id": "csqxse3wym56eim6", + "name": "Alexander Williams", + "age": 50 + }, + { + "$id": "qeaoylnrsf8p3byg", + "name": "Andrew Thomas", + "age": 25 + }, + { + "$id": "edsswobumzyzbvhf", + "name": "Austin Williams", + "age": 57 + }, + { + "$id": "hdzhzpt0ahy5hkib", + "name": "Nicholas Williams", + "age": 24 + }, + { + "$id": "w1qmvmg4roa8xnwu", + "name": "Mrs. Michelle Cisneros", + "age": 48 + }, + { + "$id": "3z3o73x7adyuo6w0", + "name": "Stacey Smith", + "age": 39 + }, + { + "$id": "sse2u5zlgoqrgmcf", + "name": "Laura Beck", + "age": 20 + }, + { + "$id": "rvovijmvch58r4yx", + "name": "Molly Clark", + "age": 51 + }, + { + "$id": "doe06nrx8sg5mcuv", + "name": "Carmen Morris", + "age": 41 + }, + { + "$id": "jbjdwuvj5s4kw04y", + "name": "Amanda Munoz", + "age": 20 + }, + { + "$id": "6k2ewkla7js0yw23", + "name": "Rachel Collins", + "age": 44 + }, + { + "$id": "fcxuyr4kkhrnigu1", + "name": "John Alexander", + "age": 18 + }, + { + "$id": "d25fuwlos5mk07o0", + "name": "Stacy Hunter", + "age": 22 + }, + { + "$id": "1vdai2rxmwd57oet", + "name": "Eric Massey", + "age": 40 + }, + { + "$id": "pq4jnt9izu1wlrzd", + "name": "Scott Garcia", + "age": 20 + }, + { + "$id": "lz9kfc0lty5xcz14", + "name": "Cassandra Nelson", + "age": 35 + }, + { + "$id": "pu7w6tyab5jd4we9", + "name": "Aaron Johnson", + "age": 50 + }, + { + "$id": "8dupswd2kqwdyn8v", + "name": "Shannon Sherman", + "age": 45 + }, + { + "$id": "ye466l71jthiz2p6", + "name": "April Garcia", + "age": 60 + }, + { + "$id": "xogsmfwb73l16qdt", + "name": "Evan Lynn", + "age": 20 + } +] \ No newline at end of file diff --git a/tests/resources/json/documentsdb-documents.json b/tests/resources/json/documentsdb-documents.json new file mode 100644 index 0000000000..908c123e81 --- /dev/null +++ b/tests/resources/json/documentsdb-documents.json @@ -0,0 +1,162 @@ +[ + { + "$id": "doc01", + "name": "Alice", + "email": "alice@test.com", + "age": 35, + "active": false, + "tags": [ + "analyst", + "engineer" + ], + "address": { + "city": "New York", + "zip": "27436", + "country": "DE" + } + }, + { + "$id": "doc02", + "name": "Bob", + "email": "bob@test.com", + "age": 58, + "active": false, + "tags": [ + "engineer", + "manager" + ], + "address": { + "city": "London", + "zip": "49320", + "country": "JP" + } + }, + { + "$id": "doc03", + "name": "Charlie", + "email": "charlie@test.com", + "age": 23, + "active": true, + "tags": [ + "analyst", + "manager" + ], + "address": { + "city": "Tokyo", + "zip": "74758", + "country": "JP" + } + }, + { + "$id": "doc04", + "name": "Diana", + "email": "diana@test.com", + "age": 60, + "active": true, + "tags": [ + "analyst", + "manager" + ], + "address": { + "city": "Paris", + "zip": "50703", + "country": "JP" + } + }, + { + "$id": "doc05", + "name": "Eve", + "email": "eve@test.com", + "age": 59, + "active": false, + "tags": [ + "engineer", + "designer" + ], + "address": { + "city": "Berlin", + "zip": "98929", + "country": "UK" + } + }, + { + "$id": "doc06", + "name": "Frank", + "email": "frank@test.com", + "age": 23, + "active": true, + "tags": [ + "engineer", + "manager" + ], + "address": { + "city": "Sydney", + "zip": "82907", + "country": "US" + } + }, + { + "$id": "doc07", + "name": "Grace", + "email": "grace@test.com", + "age": 36, + "active": true, + "tags": [ + "analyst", + "developer" + ], + "address": { + "city": "Toronto", + "zip": "68710", + "country": "US" + } + }, + { + "$id": "doc08", + "name": "Hank", + "email": "hank@test.com", + "age": 28, + "active": false, + "tags": [ + "engineer", + "manager" + ], + "address": { + "city": "Mumbai", + "zip": "61026", + "country": "FR" + } + }, + { + "$id": "doc09", + "name": "Iris", + "email": "iris@test.com", + "age": 40, + "active": true, + "tags": [ + "developer", + "designer" + ], + "address": { + "city": "Seoul", + "zip": "48039", + "country": "FR" + } + }, + { + "$id": "doc10", + "name": "Jack", + "email": "jack@test.com", + "age": 45, + "active": false, + "tags": [ + "manager", + "developer" + ], + "address": { + "city": "Dubai", + "zip": "22733", + "country": "JP" + } + } +] \ No newline at end of file diff --git a/tests/resources/json/irrelevant-column.json b/tests/resources/json/irrelevant-column.json new file mode 100644 index 0000000000..d047620ab0 --- /dev/null +++ b/tests/resources/json/irrelevant-column.json @@ -0,0 +1,602 @@ +[ + { + "$id": "r5ctmrqwqn1m3rc0", + "name": "Diamond Mendez", + "age": 56, + "email": "diamond.mendez@example.com" + }, + { + "$id": "wxwp7e7q7nx3ltfx", + "name": "Michael Huff", + "age": 20, + "email": "michael.huff@example.com" + }, + { + "$id": "4ct0b38fwaojawlv", + "name": "Alyssa Rodriguez", + "age": 37, + "email": "alyssa.rodriguez@example.com" + }, + { + "$id": "o0jjcuygbta2zvga", + "name": "Barbara Smith", + "age": 26, + "email": "barbara.smith@example.com" + }, + { + "$id": "bdy6l2ofl8klb4pb", + "name": "Evelyn Edwards", + "age": 54, + "email": "evelyn.edwards@example.com" + }, + { + "$id": "rkccl72v7zwtbila", + "name": "Tina Richardson", + "age": 41, + "email": "tina.richardson@example.com" + }, + { + "$id": "cilw7um0cd927esj", + "name": "Joel Hernandez", + "age": 49, + "email": "joel.hernandez@example.com" + }, + { + "$id": "60povvz0votkve1j", + "name": "Zachary Cooper", + "age": 59, + "email": "zachary.cooper@example.com" + }, + { + "$id": "ayow5dzwktvbbtp2", + "name": "Brittany Spears", + "age": 20, + "email": "brittany.spears@example.com" + }, + { + "$id": "cfru98od0lab0b2n", + "name": "Holly White", + "age": 47, + "email": "holly.white@example.com" + }, + { + "$id": "vjxjldvu3r6uylq6", + "name": "Kimberly Barnes", + "age": 27, + "email": "kimberly.barnes@example.com" + }, + { + "$id": "d1p47hl97pw6xowb", + "name": "Stephen Miller", + "age": 53, + "email": "stephen.miller@example.com" + }, + { + "$id": "yxk6qaa5ryb3gqrb", + "name": "Yvonne Newman", + "age": 41, + "email": "yvonne.newman@example.com" + }, + { + "$id": "ifkeo7j8t7hfd7z8", + "name": "Carol Kane", + "age": 38, + "email": "carol.kane@example.com" + }, + { + "$id": "e1q4lq8vvpxp9ysb", + "name": "Doris Foster", + "age": 44, + "email": "doris.foster@example.com" + }, + { + "$id": "obec6d52dc6swzsf", + "name": "Joseph Stokes", + "age": 28, + "email": "joseph.stokes@example.com" + }, + { + "$id": "26v06la6ug8wkvim", + "name": "Steve Williams", + "age": 31, + "email": "steve.williams@example.com" + }, + { + "$id": "m4glre4ch12vkxp6", + "name": "James Carey", + "age": 29, + "email": "james.carey@example.com" + }, + { + "$id": "jee8fyfffnjugsd5", + "name": "Kathryn Henry", + "age": 38, + "email": "kathryn.henry@example.com" + }, + { + "$id": "miquc6ljb9l3a31r", + "name": "Christopher Landry", + "age": 23, + "email": "christopher.landry@example.com" + }, + { + "$id": "ghf7da7seeuj1zdl", + "name": "Jennifer Mcgee", + "age": 62, + "email": "jennifer.mcgee@example.com" + }, + { + "$id": "x7h11phjrz77w0q8", + "name": "Cathy Church", + "age": 35, + "email": "cathy.church@example.com" + }, + { + "$id": "dn8u5lsux4708z6j", + "name": "Jose Lopez", + "age": 41, + "email": "jose.lopez@example.com" + }, + { + "$id": "zb7fdlyohuyy5i9k", + "name": "William Rose", + "age": 30, + "email": "william.rose@example.com" + }, + { + "$id": "qyrj8m9krp4dt4wt", + "name": "Sarah Ford", + "age": 26, + "email": "sarah.ford@example.com" + }, + { + "$id": "t6t673zpfyhhz8pg", + "name": "Alisha Jones", + "age": 61, + "email": "alisha.jones@example.com" + }, + { + "$id": "0hfbo0iy1q9bwc2n", + "name": "Kristin Kelly", + "age": 61, + "email": "kristin.kelly@example.com" + }, + { + "$id": "8alv4e4xrpcj443z", + "name": "Brendan Stout", + "age": 40, + "email": "brendan.stout@example.com" + }, + { + "$id": "qxm7a0z32xdkzxdj", + "name": "Kelly Cruz", + "age": 18, + "email": "kelly.cruz@example.com" + }, + { + "$id": "885mti7j7oiz5p5g", + "name": "Samantha Martin", + "age": 50, + "email": "samantha.martin@example.com" + }, + { + "$id": "v8i7dvhby6711m66", + "name": "David Santos", + "age": 46, + "email": "david.santos@example.com" + }, + { + "$id": "rggc0ow8ccd2jgvp", + "name": "Elizabeth Carroll", + "age": 22, + "email": "elizabeth.carroll@example.com" + }, + { + "$id": "012472s64rvzq1c4", + "name": "Corey Owens", + "age": 46, + "email": "corey.owens@example.com" + }, + { + "$id": "0k2xrwj4g33ut14y", + "name": "Shelby Mueller", + "age": 65, + "email": "shelby.mueller@example.com" + }, + { + "$id": "s3y9rl4uzf3difiq", + "name": "Dr. Sylvia Myers", + "age": 29, + "email": "sylvia.myers@example.com" + }, + { + "$id": "ntpc2td892t7f6an", + "name": "Scott Freeman", + "age": 48, + "email": "scott.freeman@example.com" + }, + { + "$id": "7f703gibyr5ijdmt", + "name": "Christopher Atkinson", + "age": 21, + "email": "christopher.atkinson@example.com" + }, + { + "$id": "r2jdf2pivkxmqd0l", + "name": "Sean Diaz", + "age": 31, + "email": "sean.diaz@example.com" + }, + { + "$id": "fj98fji1lrxeigs9", + "name": "Bobby Dyer", + "age": 57, + "email": "bobby.dyer@example.com" + }, + { + "$id": "mehqmzp9u7xv1z3j", + "name": "Daniel Hall", + "age": 62, + "email": "daniel.hall@example.com" + }, + { + "$id": "4cd5ln65qjfv3h4j", + "name": "Jennifer Ramirez", + "age": 65, + "email": "jennifer.ramirez@example.com" + }, + { + "$id": "wdi6ap0oa7m1ab1d", + "name": "Angela Jackson", + "age": 57, + "email": "angela.jackson@example.com" + }, + { + "$id": "l2foqjhxvjhjzijb", + "name": "Kelly Lewis", + "age": 36, + "email": "kelly.lewis@example.com" + }, + { + "$id": "d963t5yu35uagwm4", + "name": "Jessica Munoz", + "age": 55, + "email": "jessica.munoz@example.com" + }, + { + "$id": "99ez9uxsim8zp64m", + "name": "Kelly George", + "age": 65, + "email": "kelly.george@example.com" + }, + { + "$id": "v7wl221gycftl63d", + "name": "Anthony Johnson", + "age": 65, + "email": "anthony.johnson@example.com" + }, + { + "$id": "p2zzj0lnmjvqzfc3", + "name": "Regina Fields", + "age": 61, + "email": "regina.fields@example.com" + }, + { + "$id": "fk655e243z2ivvx6", + "name": "Sharon Schaefer", + "age": 30, + "email": "sharon.schaefer@example.com" + }, + { + "$id": "4ywsv6fw8g2d8ncw", + "name": "Jacob French", + "age": 62, + "email": "jacob.french@example.com" + }, + { + "$id": "y61q9k6g4h0fxxz4", + "name": "Jessica Costa", + "age": 23, + "email": "jessica.costa@example.com" + }, + { + "$id": "knj4hfzsthk7vx5n", + "name": "George Hardy", + "age": 53, + "email": "george.hardy@example.com" + }, + { + "$id": "a88u9w2pct2nn8l6", + "name": "Andrea Allison", + "age": 20, + "email": "andrea.allison@example.com" + }, + { + "$id": "hw960v1ybycrwr5o", + "name": "Kevin Ferguson", + "age": 32, + "email": "kevin.ferguson@example.com" + }, + { + "$id": "j9garslpgx6jgzgb", + "name": "Joseph Johnson", + "age": 58, + "email": "joseph.johnson@example.com" + }, + { + "$id": "gv101bz36elm84cd", + "name": "Ashley Martinez", + "age": 18, + "email": "ashley.martinez@example.com" + }, + { + "$id": "xrvzgt3gc0c7g4cl", + "name": "Charles Nixon", + "age": 23, + "email": "charles.nixon@example.com" + }, + { + "$id": "awjlu7uk0eutcfpb", + "name": "Carol Dudley", + "age": 40, + "email": "carol.dudley@example.com" + }, + { + "$id": "95oi26p2zdudpime", + "name": "David Weber", + "age": 51, + "email": "david.weber@example.com" + }, + { + "$id": "h8x7pkhdvu5bcp89", + "name": "Scott Robinson", + "age": 32, + "email": "scott.robinson@example.com" + }, + { + "$id": "oj6cu4jm1z2afe7s", + "name": "Anthony Hardy", + "age": 38, + "email": "anthony.hardy@example.com" + }, + { + "$id": "hgsdi1g30poqqmf0", + "name": "Mackenzie Owens", + "age": 52, + "email": "mackenzie.owens@example.com" + }, + { + "$id": "8fzdz914bqlqk2tc", + "name": "Brian Foster", + "age": 27, + "email": "brian.foster@example.com" + }, + { + "$id": "fwlqoeiunjhczpl0", + "name": "Hannah Forbes", + "age": 56, + "email": "hannah.forbes@example.com" + }, + { + "$id": "rsv8156goe8z4j6j", + "name": "Lauren Reed", + "age": 26, + "email": "lauren.reed@example.com" + }, + { + "$id": "1fjqv3w7uwbswe2p", + "name": "Morgan Smith", + "age": 28, + "email": "morgan.smith@example.com" + }, + { + "$id": "soqrzmhhg05hhzn4", + "name": "Samantha Alexander", + "age": 65, + "email": "samantha.alexander@example.com" + }, + { + "$id": "8quy52cto9kjjokp", + "name": "Tiffany Roberts", + "age": 20, + "email": "tiffany.roberts@example.com" + }, + { + "$id": "e3i1g1lw04v7jd89", + "name": "Emily Hayes", + "age": 34, + "email": "emily.hayes@example.com" + }, + { + "$id": "s7n8lzb0sw7h93z1", + "name": "Rebecca Villegas", + "age": 52, + "email": "rebecca.villegas@example.com" + }, + { + "$id": "e2lc7i81tpkqs1rp", + "name": "Donald Shah", + "age": 61, + "email": "donald.shah@example.com" + }, + { + "$id": "3oe2mysup1xluiw0", + "name": "Denise Cain", + "age": 59, + "email": "denise.cain@example.com" + }, + { + "$id": "1vqypc37f85nuqz4", + "name": "Kristine Ramirez", + "age": 53, + "email": "kristine.ramirez@example.com" + }, + { + "$id": "m0uh7r3dc6z8ucb4", + "name": "Stacey Adkins", + "age": 61, + "email": "stacey.adkins@example.com" + }, + { + "$id": "jdofz6x1ahganmqf", + "name": "Daniel Hunt", + "age": 20, + "email": "daniel.hunt@example.com" + }, + { + "$id": "vbe903c2q4m4q97g", + "name": "Roberta Johnson", + "age": 48, + "email": "roberta.johnson@example.com" + }, + { + "$id": "sndngrxuwpd93pdb", + "name": "Jason Williamson", + "age": 39, + "email": "jason.williamson@example.com" + }, + { + "$id": "66hvaw2p5xwf07p8", + "name": "Sandra Robinson", + "age": 50, + "email": "sandra.robinson@example.com" + }, + { + "$id": "9pvingfsl8cmag5c", + "name": "Steve Rice", + "age": 25, + "email": "steve.rice@example.com" + }, + { + "$id": "qe154m5hh00u4iiz", + "name": "Kimberly Fritz", + "age": 53, + "email": "kimberly.fritz@example.com" + }, + { + "$id": "avqnbrco2f0tfupk", + "name": "Brianna Reynolds", + "age": 33, + "email": "brianna.reynolds@example.com" + }, + { + "$id": "cqs10gi2qu1r3ugb", + "name": "Alexander Williams", + "age": 50, + "email": "alexander.williams@example.com" + }, + { + "$id": "jrpmfi6hmm7pmegp", + "name": "Andrew Thomas", + "age": 25, + "email": "andrew.thomas@example.com" + }, + { + "$id": "heeab2qqf0zm446f", + "name": "Austin Williams", + "age": 57, + "email": "austin.williams@example.com" + }, + { + "$id": "bkhugvnil7kjchm6", + "name": "Nicholas Williams", + "age": 24, + "email": "nicholas.williams@example.com" + }, + { + "$id": "b045j302pvv8l1p4", + "name": "Mrs. Michelle Cisneros", + "age": 48, + "email": "michelle.cisneros@example.com" + }, + { + "$id": "aikhii5q210lrfpr", + "name": "Stacey Smith", + "age": 39, + "email": "stacey.smith@example.com" + }, + { + "$id": "x0zajitea1z2dfo0", + "name": "Laura Beck", + "age": 20, + "email": "laura.beck@example.com" + }, + { + "$id": "abeecki7mdff1tv0", + "name": "Molly Clark", + "age": 51, + "email": "molly.clark@example.com" + }, + { + "$id": "yizama8r3i1to548", + "name": "Carmen Morris", + "age": 41, + "email": "carmen.morris@example.com" + }, + { + "$id": "8690yh971g4rgspj", + "name": "Amanda Munoz", + "age": 20, + "email": "amanda.munoz@example.com" + }, + { + "$id": "cd9vk5v97t359ul2", + "name": "Rachel Collins", + "age": 44, + "email": "rachel.collins@example.com" + }, + { + "$id": "wrkgmx1v0w9ja4l8", + "name": "John Alexander", + "age": 18, + "email": "john.alexander@example.com" + }, + { + "$id": "kxp3ucqo6ped4ss7", + "name": "Stacy Hunter", + "age": 22, + "email": "stacy.hunter@example.com" + }, + { + "$id": "dbvv8okae2qgo0gm", + "name": "Eric Massey", + "age": 40, + "email": "eric.massey@example.com" + }, + { + "$id": "9tn3nm6ppnayisje", + "name": "Scott Garcia", + "age": 20, + "email": "scott.garcia@example.com" + }, + { + "$id": "1xuc5t60xpcvd4qi", + "name": "Cassandra Nelson", + "age": 35, + "email": "cassandra.nelson@example.com" + }, + { + "$id": "qao1nulwn0kqyfkc", + "name": "Aaron Johnson", + "age": 50, + "email": "aaron.johnson@example.com" + }, + { + "$id": "kd2q6owvuwsy5knx", + "name": "Shannon Sherman", + "age": 45, + "email": "shannon.sherman@example.com" + }, + { + "$id": "wsl37kjo0bib4wrc", + "name": "April Garcia", + "age": 60, + "email": "april.garcia@example.com" + }, + { + "$id": "ujlz7k84xzfx4khs", + "name": "Evan Lynn", + "age": 20, + "email": "evan.lynn@example.com" + } +] \ No newline at end of file diff --git a/tests/resources/json/missing-column.json b/tests/resources/json/missing-column.json new file mode 100644 index 0000000000..ac7a8e1a85 --- /dev/null +++ b/tests/resources/json/missing-column.json @@ -0,0 +1,402 @@ +[ + { + "$id": "hxfcwpcas5xokpwe", + "name": "Diamond Mendez" + }, + { + "$id": "gw8nxwf6esn3tfwf", + "name": "Michael Huff" + }, + { + "$id": "xb6bxg56lral1qy9", + "name": "Alyssa Rodriguez" + }, + { + "$id": "imerjq5j36y3agh2", + "name": "Barbara Smith" + }, + { + "$id": "07yq9qdlhmbzmr35", + "name": "Evelyn Edwards" + }, + { + "$id": "ksqo631sbhwj5ltg", + "name": "Tina Richardson" + }, + { + "$id": "j7zlndgu0gbshp15", + "name": "Joel Hernandez" + }, + { + "$id": "mfntvnljrcmf7h6v", + "name": "Zachary Cooper" + }, + { + "$id": "5f9b01nziqu2h8ed", + "name": "Brittany Spears" + }, + { + "$id": "4vxzbnzraqznk5u8", + "name": "Holly White" + }, + { + "$id": "d4ywy3mtphaatbpf", + "name": "Kimberly Barnes" + }, + { + "$id": "88odnk6nthyyvbal", + "name": "Stephen Miller" + }, + { + "$id": "08oekee3fn7mzaa5", + "name": "Yvonne Newman" + }, + { + "$id": "quw55kn9895i5e4v", + "name": "Carol Kane" + }, + { + "$id": "nge6bm8ykripei6f", + "name": "Doris Foster" + }, + { + "$id": "4k16i33s0xl2ypx9", + "name": "Joseph Stokes" + }, + { + "$id": "q0j5rxbgid66snyf", + "name": "Steve Williams" + }, + { + "$id": "n1oxun7mqq3p103y", + "name": "James Carey" + }, + { + "$id": "0dbvs840jkf8i0ye", + "name": "Kathryn Henry" + }, + { + "$id": "5sfaidgs1h87v15v", + "name": "Christopher Landry" + }, + { + "$id": "vg3punvfu5khmf41", + "name": "Jennifer Mcgee" + }, + { + "$id": "f933qydr9u5b2r11", + "name": "Cathy Church" + }, + { + "$id": "wjv87y1inf8yk32s", + "name": "Jose Lopez" + }, + { + "$id": "uljysdvdlcyrbrwk", + "name": "William Rose" + }, + { + "$id": "ot8xtzh77j55wq0s", + "name": "Sarah Ford" + }, + { + "$id": "9t76vnsv2u36s43t", + "name": "Alisha Jones" + }, + { + "$id": "66y4tnty62hw8c02", + "name": "Kristin Kelly" + }, + { + "$id": "2punfblazi5v16ar", + "name": "Brendan Stout" + }, + { + "$id": "sxhr4nf5w2gx4wbg", + "name": "Kelly Cruz" + }, + { + "$id": "68dvrqfwqnkq5el9", + "name": "Samantha Martin" + }, + { + "$id": "20192l6dbeinhkh0", + "name": "David Santos" + }, + { + "$id": "si0l4dgay09ebfmf", + "name": "Elizabeth Carroll" + }, + { + "$id": "lhse40vbldqb6ap1", + "name": "Corey Owens" + }, + { + "$id": "h5t3pslykyx3kxfm", + "name": "Shelby Mueller" + }, + { + "$id": "ldc0luydrw6jub0f", + "name": "Dr. Sylvia Myers" + }, + { + "$id": "voc9628xg4dsgw2y", + "name": "Scott Freeman" + }, + { + "$id": "o4y0gk3gqv1ax2fz", + "name": "Christopher Atkinson" + }, + { + "$id": "u1n3x4e4u7e0vzj6", + "name": "Sean Diaz" + }, + { + "$id": "s36eskwtm0w7lwr7", + "name": "Bobby Dyer" + }, + { + "$id": "4hjnag1p5iwvtixd", + "name": "Daniel Hall" + }, + { + "$id": "m91d80oxsa216zbh", + "name": "Jennifer Ramirez" + }, + { + "$id": "5hj6858zo2g85n6v", + "name": "Angela Jackson" + }, + { + "$id": "8m8oihv9a1e7nn92", + "name": "Kelly Lewis" + }, + { + "$id": "7azy39la0no0mxi7", + "name": "Jessica Munoz" + }, + { + "$id": "47pmjkhnnqhyit8c", + "name": "Kelly George" + }, + { + "$id": "6j6cpy4kgneg1mmh", + "name": "Anthony Johnson" + }, + { + "$id": "tnlmtvap1zz89km9", + "name": "Regina Fields" + }, + { + "$id": "6cyuvnwwqdmrpfzh", + "name": "Sharon Schaefer" + }, + { + "$id": "p1v4pyu2pqodc0ey", + "name": "Jacob French" + }, + { + "$id": "6npynnhjt2jd05xo", + "name": "Jessica Costa" + }, + { + "$id": "wcxedf13n2e9qi4l", + "name": "George Hardy" + }, + { + "$id": "yf2xlcmszk2tqeig", + "name": "Andrea Allison" + }, + { + "$id": "3bf2zzv7poststwa", + "name": "Kevin Ferguson" + }, + { + "$id": "c2iataz0hhv39q63", + "name": "Joseph Johnson" + }, + { + "$id": "3e8npxhov4a39pvq", + "name": "Ashley Martinez" + }, + { + "$id": "t7dp41tysipytywq", + "name": "Charles Nixon" + }, + { + "$id": "z8cztq7c47phyfhk", + "name": "Carol Dudley" + }, + { + "$id": "2636f9d8r4ipm3h6", + "name": "David Weber" + }, + { + "$id": "eh3f6wxtvkjq6ykq", + "name": "Scott Robinson" + }, + { + "$id": "raskbwpsje69a59h", + "name": "Anthony Hardy" + }, + { + "$id": "90hn1p0b4cs9e2og", + "name": "Mackenzie Owens" + }, + { + "$id": "am3swwfbo076x0v1", + "name": "Brian Foster" + }, + { + "$id": "5uw7utb9lq5cfncw", + "name": "Hannah Forbes" + }, + { + "$id": "cs6mbfzkzifefx6r", + "name": "Lauren Reed" + }, + { + "$id": "ftw3uvztziiz9x00", + "name": "Morgan Smith" + }, + { + "$id": "uhrqseeo43mozpaq", + "name": "Samantha Alexander" + }, + { + "$id": "pvvmzyfc1lxor11e", + "name": "Tiffany Roberts" + }, + { + "$id": "jia7bdag4abz123s", + "name": "Emily Hayes" + }, + { + "$id": "h6oozcngbz8o5x4y", + "name": "Rebecca Villegas" + }, + { + "$id": "9v6z1pn2f9twcy12", + "name": "Donald Shah" + }, + { + "$id": "wzz3jduioso77o7f", + "name": "Denise Cain" + }, + { + "$id": "u51plhgvjodkswnr", + "name": "Kristine Ramirez" + }, + { + "$id": "t1uhkmiytfyc13vc", + "name": "Stacey Adkins" + }, + { + "$id": "iqaqnf0ybg2ct507", + "name": "Daniel Hunt" + }, + { + "$id": "idwrwv2uu4hcpv2i", + "name": "Roberta Johnson" + }, + { + "$id": "2yd2hd6auetjacyo", + "name": "Jason Williamson" + }, + { + "$id": "egrmdbibnjhi914x", + "name": "Sandra Robinson" + }, + { + "$id": "15m1pz2bb0ercgyk", + "name": "Steve Rice" + }, + { + "$id": "0i21bhkxdagjurb7", + "name": "Kimberly Fritz" + }, + { + "$id": "726ofi7h5snreq67", + "name": "Brianna Reynolds" + }, + { + "$id": "csqxse3wym56eim6", + "name": "Alexander Williams" + }, + { + "$id": "qeaoylnrsf8p3byg", + "name": "Andrew Thomas" + }, + { + "$id": "edsswobumzyzbvhf", + "name": "Austin Williams" + }, + { + "$id": "hdzhzpt0ahy5hkib", + "name": "Nicholas Williams" + }, + { + "$id": "w1qmvmg4roa8xnwu", + "name": "Mrs. Michelle Cisneros" + }, + { + "$id": "3z3o73x7adyuo6w0", + "name": "Stacey Smith" + }, + { + "$id": "sse2u5zlgoqrgmcf", + "name": "Laura Beck" + }, + { + "$id": "rvovijmvch58r4yx", + "name": "Molly Clark" + }, + { + "$id": "doe06nrx8sg5mcuv", + "name": "Carmen Morris" + }, + { + "$id": "jbjdwuvj5s4kw04y", + "name": "Amanda Munoz" + }, + { + "$id": "6k2ewkla7js0yw23", + "name": "Rachel Collins" + }, + { + "$id": "fcxuyr4kkhrnigu1", + "name": "John Alexander" + }, + { + "$id": "d25fuwlos5mk07o0", + "name": "Stacy Hunter" + }, + { + "$id": "1vdai2rxmwd57oet", + "name": "Eric Massey" + }, + { + "$id": "pq4jnt9izu1wlrzd", + "name": "Scott Garcia" + }, + { + "$id": "lz9kfc0lty5xcz14", + "name": "Cassandra Nelson" + }, + { + "$id": "pu7w6tyab5jd4we9", + "name": "Aaron Johnson" + }, + { + "$id": "8dupswd2kqwdyn8v", + "name": "Shannon Sherman" + }, + { + "$id": "ye466l71jthiz2p6", + "name": "April Garcia" + }, + { + "$id": "xogsmfwb73l16qdt", + "name": "Evan Lynn" + } +] \ No newline at end of file diff --git a/tests/resources/json/vectorsdb-documents.json b/tests/resources/json/vectorsdb-documents.json new file mode 100644 index 0000000000..f1bada8a7e --- /dev/null +++ b/tests/resources/json/vectorsdb-documents.json @@ -0,0 +1,262 @@ +[ + { + "$id": "vec01", + "metadata": { + "title": "Vector Document 1", + "score": 0.643, + "category": "art" + }, + "embeddings": [ + -0.516361, + 0.261456, + -0.77323, + 0.909775, + -0.003065, + -0.626888, + 0.704171, + -0.366385, + 0.198681, + 0.854914, + -0.227548, + -0.14278, + -0.548405, + -0.822939, + 0.407267, + 0.570791 + ] + }, + { + "$id": "vec02", + "metadata": { + "title": "Vector Document 2", + "score": 0.322, + "category": "art" + }, + "embeddings": [ + 0.103211, + 0.028378, + 0.806001, + -0.412638, + -0.200009, + 0.448239, + -0.781144, + 0.621957, + -0.868352, + 0.217013, + 0.308217, + -0.851528, + -0.340766, + 0.805996, + -0.065638, + -0.363951 + ] + }, + { + "$id": "vec03", + "metadata": { + "title": "Vector Document 3", + "score": 0.503, + "category": "science" + }, + "embeddings": [ + 0.971901, + -0.475154, + 0.60526, + -0.151618, + -0.938199, + -0.139977, + 0.154581, + -0.868021, + 0.069567, + 0.233545, + -0.164826, + 0.239036, + 0.8875, + 0.488075, + 0.787231, + 0.090293 + ] + }, + { + "$id": "vec04", + "metadata": { + "title": "Vector Document 4", + "score": 0.943, + "category": "science" + }, + "embeddings": [ + 0.372035, + -0.868405, + -0.974987, + -0.836673, + 0.030057, + -0.667698, + 0.095189, + 0.518373, + -0.173732, + 0.966379, + 0.509861, + -0.172607, + 0.420855, + -0.534562, + 0.600872, + 0.194391 + ] + }, + { + "$id": "vec05", + "metadata": { + "title": "Vector Document 5", + "score": 0.924, + "category": "music" + }, + "embeddings": [ + -0.318783, + -0.876486, + 0.843971, + -0.472869, + -0.350164, + 0.936237, + -0.741591, + 0.298213, + -0.075429, + 0.243415, + 0.929625, + 0.96649, + 0.251843, + -0.371953, + 0.348079, + 0.090382 + ] + }, + { + "$id": "vec06", + "metadata": { + "title": "Vector Document 6", + "score": 0.385, + "category": "science" + }, + "embeddings": [ + -0.608111, + 0.549885, + 0.720346, + 0.260442, + -0.023344, + 0.800346, + -0.956586, + -0.407161, + 0.516883, + 0.230456, + 0.376704, + -0.347203, + 0.925657, + -0.486608, + 0.330032, + 0.323414 + ] + }, + { + "$id": "vec07", + "metadata": { + "title": "Vector Document 7", + "score": 0.61, + "category": "science" + }, + "embeddings": [ + 0.45451, + -0.251783, + -0.479459, + 0.167412, + 0.890343, + 0.415136, + -0.506263, + 0.318171, + -0.729294, + -0.907919, + 0.607033, + 0.652333, + 0.97129, + -0.118671, + 0.110045, + -0.514242 + ] + }, + { + "$id": "vec08", + "metadata": { + "title": "Vector Document 8", + "score": 0.107, + "category": "history" + }, + "embeddings": [ + -0.883255, + -0.294593, + -0.153187, + 0.595936, + 0.341818, + -0.410719, + 0.017348, + -0.350047, + -0.888906, + 0.344646, + 0.184335, + -0.429053, + 0.540542, + -0.408493, + -0.982748, + -0.255031 + ] + }, + { + "$id": "vec09", + "metadata": { + "title": "Vector Document 9", + "score": 0.914, + "category": "art" + }, + "embeddings": [ + 0.31891, + 0.502493, + -0.245712, + -0.325282, + -0.514772, + 0.097323, + 0.812412, + -0.762069, + 0.846688, + -0.391482, + 0.879953, + 0.986352, + -0.562588, + -0.566111, + 0.163109, + 0.119346 + ] + }, + { + "$id": "vec10", + "metadata": { + "title": "Vector Document 10", + "score": 0.169, + "category": "science" + }, + "embeddings": [ + -0.563645, + -0.19119, + -0.853168, + -0.526952, + -0.459635, + 0.850766, + -0.345261, + 0.547467, + 0.455409, + -0.507607, + -0.228436, + -0.587395, + 0.109146, + 0.190046, + 0.861559, + -0.724741 + ] + } +] \ No newline at end of file diff --git a/tests/unit/Platform/Modules/Installer/ModuleTest.php b/tests/unit/Platform/Modules/Installer/ModuleTest.php index 0b7e7effcb..507a4e25f6 100644 --- a/tests/unit/Platform/Modules/Installer/ModuleTest.php +++ b/tests/unit/Platform/Modules/Installer/ModuleTest.php @@ -134,7 +134,7 @@ class ModuleTest extends TestCase $this->assertActionParams($action, [ 'appDomain', 'httpPort', 'httpsPort', 'emailCertificates', 'opensslKey', 'assistantOpenAIKey', 'accountEmail', 'accountPassword', 'database', - 'installId', 'retryStep', + 'installId', 'retryStep', 'migrate', ]); $this->assertActionInjects($action, ['request', 'response', 'swooleResponse', 'installerState', 'installerConfig', 'installerPaths']); } diff --git a/tests/unit/Utopia/Response/Filters/V16Test.php b/tests/unit/Utopia/Response/Filters/V16Test.php index e771146e3a..2ba60d35e9 100644 --- a/tests/unit/Utopia/Response/Filters/V16Test.php +++ b/tests/unit/Utopia/Response/Filters/V16Test.php @@ -3,6 +3,7 @@ namespace Tests\Unit\Utopia\Response\Filters; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Filters\V16; use Cron\CronExpression; use PHPUnit\Framework\Attributes\DataProvider; @@ -11,10 +12,7 @@ use Utopia\Database\DateTime; class V16Test extends TestCase { - /** - * @var Filter - */ - protected $filter = null; + protected Filter $filter; public function setUp(): void { diff --git a/tests/unit/Utopia/Response/Filters/V17Test.php b/tests/unit/Utopia/Response/Filters/V17Test.php index 21d91e1314..0bdc4de53e 100644 --- a/tests/unit/Utopia/Response/Filters/V17Test.php +++ b/tests/unit/Utopia/Response/Filters/V17Test.php @@ -3,16 +3,14 @@ namespace Tests\Unit\Utopia\Response\Filters; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Filters\V17; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class V17Test extends TestCase { - /** - * @var Filter - */ - protected $filter = null; + protected Filter $filter; public function setUp(): void { diff --git a/tests/unit/Utopia/Response/Filters/V18Test.php b/tests/unit/Utopia/Response/Filters/V18Test.php index da169a7d0e..2e09b34515 100644 --- a/tests/unit/Utopia/Response/Filters/V18Test.php +++ b/tests/unit/Utopia/Response/Filters/V18Test.php @@ -3,16 +3,14 @@ namespace Tests\Unit\Utopia\Response\Filters; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Filters\V18; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class V18Test extends TestCase { - /** - * @var Filter - */ - protected $filter = null; + protected Filter $filter; public function setUp(): void { diff --git a/tests/unit/Utopia/Response/Filters/V19Test.php b/tests/unit/Utopia/Response/Filters/V19Test.php index eaeefba2fc..a53dbfe355 100644 --- a/tests/unit/Utopia/Response/Filters/V19Test.php +++ b/tests/unit/Utopia/Response/Filters/V19Test.php @@ -3,16 +3,14 @@ namespace Tests\Unit\Utopia\Response\Filters; use Appwrite\Utopia\Response; +use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Filters\V19; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class V19Test extends TestCase { - /** - * @var Filter - */ - protected $filter = null; + protected Filter $filter; public function setUp(): void {