Compare commits

...
Author SHA1 Message Date
ArnabChatterjee20k 9d0950b8bc Merge remote-tracking branch 'origin/bump-database-version-bkp' into test-shared 2026-04-01 13:40:37 +05:30
ArnabChatterjee20k 647b4bc632 removed adapters for checking 2026-04-01 12:48:35 +05:30
premtsd-codeandGitHub d38c921781 Merge branch '1.9.x' into bump-database-version 2026-04-01 05:23:37 +01:00
Jake BarnbyandGitHub 44610462b3 Merge pull request #11735 from appwrite/lohanidamodar-patch-2 2026-04-01 03:57:20 +00:00
Damodar LohaniandGitHub 28ece7de02 Change Usage class from final to non-final 2026-04-01 09:36:26 +05:45
Jake BarnbyandGitHub 8c6d4d8b36 Merge pull request #11734 from appwrite/fix-defaults 2026-04-01 02:50:09 +00:00
Jake Barnby 2ebc6f70ef (fix): param default 2026-04-01 15:49:40 +13:00
Damodar LohaniandGitHub 2b7690d6db Merge pull request #11732 from appwrite/claude/add-deployment-hook-method-prkpM
Add beforeCreateGitDeployment hook for deployment validation
2026-04-01 08:27:50 +05:45
Damodar LohaniandGitHub d9af799cc7 Merge branch '1.9.x' into claude/add-deployment-hook-method-prkpM 2026-04-01 08:09:46 +05:45
Damodar LohaniandGitHub 3ed1ca736d Merge pull request #11731 from appwrite/claude/update-php-runtimes-hNh1r
Update dependencies
2026-04-01 07:57:25 +05:45
Jake BarnbyandGitHub 9fa35db838 Merge pull request #11733 from appwrite/fix-defaults 2026-04-01 02:08:45 +00:00
Claude b6e020389b Remove docblock from beforeCreateGitDeployment hook
https://claude.ai/code/session_01HP1N9hHbqMzxm5QmaoGhyZ
2026-04-01 02:07:36 +00:00
Jake Barnby ccc0cfbfdc (fix): migrate default 2026-04-01 15:04:52 +13:00
Claude b91506fc2d Rename hook to beforeCreateGitDeployment
https://claude.ai/code/session_01HP1N9hHbqMzxm5QmaoGhyZ
2026-04-01 02:03:59 +00:00
Claude 9ffc23946c Add validateGitDeployment hook method to Deployment trait
Add a no-op protected method that Cloud can override to enforce
billing/block checks before processing git deployments. The hook
is called inside the foreach loop after project validation, so any
exception it throws is caught and logged as an error.

https://claude.ai/code/session_01HP1N9hHbqMzxm5QmaoGhyZ
2026-04-01 02:02:18 +00:00
Jake BarnbyandGitHub 8bb6b5cd2a Merge pull request #11646 from appwrite/feat/import-export-json 2026-04-01 01:56:47 +00:00
Claude afea4ca57b Update appwrite/php-runtimes to 0.19.5
https://claude.ai/code/session_01KXrbPzuXNzRhn38xm9zqwJ
2026-04-01 01:44:07 +00:00
Jake BarnbyandGitHub b30b89ade9 Merge pull request #11730 from appwrite/fix-v24 2026-04-01 01:11:54 +00:00
Jake Barnby 829cf887dc (fix): missing users case 2026-04-01 14:07:59 +13:00
Prem Palanisamy 32728ae4a0 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version 2026-04-01 02:16:20 +02:00
premtsd-codeandGitHub d862a64874 Merge branch '1.9.x' into feat/import-export-json 2026-03-31 22:54:12 +01:00
Prem Palanisamy 168166b9c1 bump utopia-php/database to 5.3.19 and fix shared-mode CI failures
- Bump utopia-php/database from 5.3.17 to 5.3.19

- Remove invalid (int) cast on tenant sequence in shared tables mode

- Fix DSN construction for documentsdb/vectorsdb: filter empty strings
  from explode(), skip pool filtering when shared tables env vars unset,
  fail fast when no pool found

- Use dedicated mode for separate database pools in getDatabasesDB
  since shared tables can't work across engines (PostgreSQL integer
  _tenant vs MongoDB UUID tenant). Auto-init schema on first use.

- Add documentsdb/vectorsdb shared tables env vars to CI workflow

- Fix testChannelTablesDBRowUpdate race condition with deterministic
  event drain loop
2026-03-31 21:24:22 +02:00
Luke B. SilverandGitHub 142e671c7d Merge pull request #11720 from appwrite/codex/phpstan-core-type-docs-1
[codex] Fix PHPStan core type and PHPDoc issues (part 1)
2026-03-31 19:42:26 +01:00
Chirag Aggarwal b4085d1083 Fix token trait PHPStan static access 2026-03-31 22:23:37 +05:30
Chirag Aggarwal 18ed6a9c59 Fix more PHPStan static access issues 2026-03-31 22:04:37 +05:30
Chirag Aggarwal 3d64ccd056 Fix more PHPStan docblock issues 2026-03-31 21:48:14 +05:30
Chirag Aggarwal 4f73eb021f Fix PHPStan core type and PHPDoc issues (part 1) 2026-03-31 21:44:20 +05:30
premtsd-codeandGitHub 7dcfd3ff3d Update description for get-queue-audits endpoint 2026-03-31 12:38:48 +01:00
Prem Palanisamy 4dfdfb5e59 Merge remote-tracking branch 'origin/1.9.x' into feat/import-export-json 2026-03-31 12:37:17 +01:00
Prem Palanisamy 5d1009b324 fix: correct resourceType routing, schemaless validation, and E2E tests for migrations
- Add getDatabaseResourceType() helper to map database types to resource constants
- Use database-specific resourceType for CSV/JSON import/export instead of hardcoded TYPE_DATABASE
- Skip attribute validation for schemaless databases (DocumentsDB/VectorsDB) in exports
- Parse JSON export queries in migration worker
- Restore MigrationsBase from 1.9.x and append VectorsDB/DocumentsDB E2E tests
2026-03-31 12:35:18 +01:00
Chirag AggarwalandGitHub 4568316345 Merge pull request #11715 from appwrite/fix-specs-defaults
Fix spec generation defaults for _APP_HOME and _APP_SYSTEM_TEAM_EMAIL
2026-03-31 15:52:14 +05:30
Jake BarnbyandGitHub c444bccacc Merge pull request #11716 from appwrite/fix-installer 2026-03-31 10:09:42 +00:00
Jake Barnby f9aee4de5d (fix): clear stale install data before starting new installation 2026-03-31 23:08:31 +13:00
Chirag Aggarwal 037878cc75 fix: use correct defaults for spec generation
Use https://appwrite.io and team@appwrite.io as defaults for _APP_HOME
and _APP_SYSTEM_TEAM_EMAIL in spec generation, instead of [HOSTNAME]
and team@localhost.test placeholders.
2026-03-31 15:11:11 +05:30
Jake BarnbyandGitHub 416b26ac7d Merge pull request #11710 from appwrite/fix-installer
(fix): guard against missing Host header in dispatch
2026-03-31 08:30:23 +00:00
Jake Barnby b47ac00ca8 (refactor): rename migrate param and add --migrate flag to upgrade task 2026-03-31 21:08:29 +13:00
Jake Barnby 2f53d09c5b (feat): add database migration step to upgrade installer 2026-03-31 20:58:33 +13:00
Jake Barnby 1fa1aa8621 (fix): guard against missing Host header in dispatch 2026-03-31 20:58:22 +13:00
Jake BarnbyandGitHub ec20fb59e2 Merge pull request #11689 from appwrite/fix-installer 2026-03-31 03:51:20 +00:00
Jake Barnby 7e49cf0bed Revert "(fix): increase GraphQL schema polling timeouts to match CI expectations"
This reverts commit 4e32497be1.
2026-03-31 16:48:32 +13:00
Jake BarnbyandClaude Opus 4.6 4e32497be1 (fix): increase GraphQL schema polling timeouts to match CI expectations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:26:20 +13:00
Jake Barnby fe6f79c9c6 (fix): restore ip key in tracking payload 2026-03-31 16:02:22 +13:00
Jake Barnby 5c84efdd7c (fix): upgrade UI — remove min-height, hide secret key row, fix copy 2026-03-31 15:58:45 +13:00
Jake Barnby 02d234ad3a (fix): detect existing installation as upgrade for web installer 2026-03-31 15:58:41 +13:00
Jake Barnby 8b7459f634 (fix): address review comments — coroutine guard and hostIp key 2026-03-31 15:15:51 +13:00
Jake Barnby 100cbf50f0 Merge remote-tracking branch 'origin/feat-auto-detect-cli' into fix-installer
# Conflicts:
#	src/Appwrite/Platform/Tasks/Install.php
2026-03-31 15:04:56 +13:00
Jake Barnby c7c59f4e7b Fix key 2026-03-31 15:04:04 +13:00
Jake Barnby a955dff55a (fix): run install tracking in coroutine to avoid blocking worker 2026-03-31 14:59:31 +13:00
Jake Barnby 7bcd5b4ebc (fix): remove swallowed exception around tracking call 2026-03-31 14:46:04 +13:00
Jake Barnby dfb23612b0 (docs): rewrite AGENTS.md with module structure and action patterns 2026-03-31 14:44:24 +13:00
Jake Barnby 4e71a54fae (fix): send SSE done event before tracking to prevent installer hang 2026-03-31 14:44:20 +13:00
Jake BarnbyandGitHub ea032c2804 Merge pull request #11679 from appwrite/fix/remove-documentsdb-spatial 2026-03-30 23:40:03 +00:00
Prem Palanisamy a80ecd0cb6 fix: alphabetize imports, update phpstan baseline count for migrations tests 2026-03-30 17:24:58 +01:00
Prem Palanisamy 8be7c6182e fix: update composer.lock for utopia-php/database branch 2026-03-30 17:16:05 +01:00
premtsd-codeandGitHub 3bb6a8bcc8 Merge branch '1.9.x' into feat/import-export-json 2026-03-30 16:15:16 +01:00
Prem Palanisamy c72d438a10 fix: remove INDEX_SPATIAL from DocumentsDB index creation 2026-03-30 15:55:37 +01:00
Prem Palanisamy 0085d93aeb fix: add version alias for database branch 2026-03-30 15:48:05 +01:00
Prem Palanisamy 3b9b0c96c4 use utopia-php/database fix/mongo-order-case branch for order case normalization 2026-03-30 15:46:20 +01:00
Prem Palanisamy 2611bf4af1 fix: add setPlatform to JSON import trigger for consistency 2026-03-30 15:43:21 +01:00
Prem Palanisamy de219de31d fix: restore original databasetype block in CSV export 2026-03-30 15:26:09 +01:00
Prem Palanisamy d8bbd82556 fix: remove duplicate database fetch, add null-safe queries fallback, add schemaless comment 2026-03-30 14:55:05 +01:00
Harsh MahajanandGitHub 5d962e21b0 Merge pull request #11674 from appwrite/fix-vcs-sdk-models
fix: merge duplicate SDK responses in VCS repository list and detections
2026-03-30 16:49:56 +05:30
Harsh Mahajan 551c83dd2c fix: merge duplicate SDK responses in VCS repository list and detections 2026-03-30 16:34:26 +05:30
Chirag AggarwalandGitHub d964c5a439 Merge pull request #11671 from appwrite/codex/reduce-specs-memory-retention
[codex] Reduce specs task memory retention
2026-03-30 14:48:27 +05:30
Chirag Aggarwal 4850fb54ed Disallow --release=yes with --mode=examples 2026-03-30 14:19:02 +05:30
Chirag Aggarwal 796bc13c19 Skip spec version prompt when creating SDK releases
When --release=yes, the Appwrite spec version is not needed since
the release flow uses the SDK version from config. This moves the
version prompt and validation behind a !$createRelease guard and
hoists the release block before the spec/SDK generation setup.
2026-03-30 14:11:36 +05:30
Chirag Aggarwal 30e080c4d3 Address review: race-safe mkdir and separate encode/write errors 2026-03-30 14:04:20 +05:30
Prem Palanisamy aaebeec61e fix: remove spatial from documentsdb indexes, parse JSON export queries, skip schema validation for schemaless exports 2026-03-30 09:09:50 +01:00
ArnabChatterjee20kandGitHub 1789d2ce89 Merge pull request #11658 from appwrite/update-db-size
updated db size
2026-03-30 13:37:18 +05:30
Chirag Aggarwal 239a9c5457 Reduce specs task memory retention 2026-03-30 13:32:57 +05:30
premtsd-codeandGitHub 53d0e14f97 Merge branch '1.9.x' into feat/import-export-json 2026-03-28 04:23:07 +00:00
Prem Palanisamy 741267c6c5 fix: rename locale keys to dataExport, use {{type}} placeholder for CSV/JSON 2026-03-26 12:09:10 +00:00
Prem Palanisamy e85080499a fix: generalize export handler for CSV and JSON — download URL, email, dynamic file extension 2026-03-26 11:54:54 +00:00
Prem Palanisamy ee1ca5ace6 fix: remove email verification from vectorsdb export test (tested separately) 2026-03-26 11:36:50 +00:00
Prem Palanisamy b36472f0da add E2E tests for vectorsdb and documentsdb JSON import/export 2026-03-26 11:24:14 +00:00
Prem Palanisamy 52ae8b3880 fix: use type-specific resources for JSON endpoints, add JSON source/destination to worker 2026-03-26 09:23:45 +00:00
Prem Palanisamy 8f09e74462 fix: bump migration to 1.9.*, fix dataExportType property 2026-03-26 07:41:23 +00:00
Prem Palanisamy 30907d716f cleanup: remove duplicate setProject, remove stale spec files 2026-03-26 06:55:28 +00:00
DarshanandPrem Palanisamy eb46855e72 regen: specs. 2026-03-26 06:44:53 +00:00
DarshanandPrem Palanisamy 45557e5929 add: missing doc. 2026-03-26 06:44:36 +00:00
DarshanandPrem Palanisamy 6ad6a5dea3 specs. 2026-03-26 06:44:22 +00:00
DarshanandPrem Palanisamy 098f7aa3e3 update: comment. 2026-03-26 06:43:49 +00:00
DarshanandPrem Palanisamy f8c8c17757 add: tests;
fix: tests.
2026-03-26 06:43:49 +00:00
DarshanandPrem Palanisamy 5b1ee93927 fix: endpoint. 2026-03-26 06:43:04 +00:00
Jake Barnby dc6b2ce3aa (feat): auto-detect cli params to force non-interactive installer 2026-03-25 13:52:58 +13:00
78 changed files with 4315 additions and 1216 deletions
+16 -31
View File
@@ -180,38 +180,11 @@ jobs:
uses: actions/github-script@v8
with:
script: |
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
const allDatabases = ['MongoDB'];
const allModes = ['shared_v1', 'shared_v2'];
const defaultDatabases = ['MongoDB'];
const defaultModes = ['dedicated'];
const pr = context.payload.pull_request;
if (!pr) {
core.setOutput('databases', JSON.stringify(allDatabases));
core.setOutput('modes', JSON.stringify(allModes));
return;
}
const getContent = (ref) => github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: 'composer.lock',
ref,
});
const getDbVersion = (lock) => lock.packages?.find(p => p.name === 'utopia-php/database')?.version;
const [{ data: base }, { data: head }] = await Promise.all([
getContent(pr.base.sha),
getContent(pr.head.sha),
]);
const decode = (content) => JSON.parse(Buffer.from(content, 'base64').toString());
const databaseChanged = getDbVersion(decode(base.content)) !== getDbVersion(decode(head.content));
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
core.setOutput('databases', JSON.stringify(allDatabases));
core.setOutput('modes', JSON.stringify(allModes));
build:
name: Build
@@ -451,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
@@ -525,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
@@ -582,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
+99 -86
View File
@@ -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 <file>` | Format a specific file |
| `composer lint <file>` | 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.
+15 -15
View File
@@ -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}},",
+314 -12
View File
@@ -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')
+1 -1
View File
@@ -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
+27 -5
View File
@@ -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
+1 -1
View File
@@ -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";
@@ -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;
}
+13 -6
View File
@@ -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);
@@ -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({
@@ -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 {
+25
View File
@@ -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 = {
@@ -62,12 +62,14 @@ $badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
<span class="badge badge-neutral typography-text-xs-400" data-review-assistant-badge>Disabled</span>
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Appwrite Assistant</div>
</div>
<?php if (!$isUpgrade) { ?>
<div class="review-row">
<span class="badge <?php echo $badgeClass; ?> typography-text-xs-400" data-review-badge>
<?php echo htmlspecialchars((string) $badgeLabel, ENT_QUOTES, 'UTF-8'); ?>
</span>
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Secret API key</div>
</div>
<?php } ?>
</div>
</div>
</div>
@@ -6,7 +6,7 @@ $isUpgrade = $isUpgrade ?? false;
<div class="install-panel">
<div class="install-header">
<div class="typography-text-m-400 text-neutral-primary">
<?php echo $isUpgrade ? 'Updating your app…' : 'Installing your app…'; ?>
<?php echo $isUpgrade ? 'Updating Appwrite…' : 'Installing Appwrite…'; ?>
</div>
</div>
<div class="install-list" data-install-list></div>
@@ -0,0 +1,37 @@
<?php
$isUpgrade = $isUpgrade ?? false;
?>
<div class="step-layout" data-step="6">
<div class="stack-xl">
<div class="stack-xxxs">
<h1 class="typography-title-s text-neutral-primary">Database migration</h1>
<p class="typography-text-m-400 text-neutral-secondary">
Run database migration after the update to apply schema changes.
</p>
</div>
<div class="stack-xl">
<label class="migration-option" for="run-migration">
<span class="migration-option-content">
<span class="typography-text-m-500 text-neutral-primary">Run migration automatically</span>
<span class="typography-text-xs-400 text-neutral-tertiary">Recommended when upgrading to a new version</span>
</span>
<span class="migration-switch">
<input type="checkbox" id="run-migration" name="migrate" class="sr-only" checked>
<span class="migration-switch-track" aria-hidden="true">
<span class="migration-switch-thumb"></span>
</span>
</span>
</label>
<div class="migration-hint">
<span class="migration-hint-icon">
<?php include __DIR__ . '/../../icons/info.svg'; ?>
</span>
<span class="typography-text-xs-400 text-neutral-tertiary">
To run manually later: <code class="migration-code">docker compose exec appwrite migrate</code>
</span>
</div>
</div>
</div>
</div>
+26 -5
View File
@@ -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
+1 -1
View File
@@ -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.*",
Generated
+19 -19
View File
@@ -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",
+1 -1
View File
@@ -1 +1 @@
Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server.
Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server.
+2 -523
View File
@@ -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\<string, bool\> but returns array\<int\<0, max\>\>\.$#'
identifier: return.type
count: 1
path: src/Appwrite/Functions/EventProcessor.php
-
message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getWebhooksEvents\(\) should return array\<string, bool\> but returns array\<int\<0, max\>\>\.$#'
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
+1 -1
View File
@@ -155,7 +155,7 @@ abstract class OAuth2
/**
* @param string $code
*
* @return string
* @return int
*/
public function getAccessTokenExpiry(string $code): int
{
+1 -1
View File
@@ -108,7 +108,7 @@ class Disqus extends OAuth2
}
/**
* @param string $token
* @param string $accessToken
*
* @return string
*/
+1 -1
View File
@@ -33,7 +33,7 @@ class PersonalData extends Password
/**
* Is valid.
*
* @param mixed $value
* @param mixed $password
*
* @return bool
*/
-6
View File
@@ -6,14 +6,8 @@ use DeviceDetector\DeviceDetector;
class Detector
{
/**
* @param string
*/
protected $userAgent = '';
/**
* @param DeviceDetector
*/
protected $detctor;
/**
-3
View File
@@ -12,9 +12,6 @@ class Compose
*/
protected $compose = [];
/**
* @var string $data
*/
public function __construct(string $data)
{
$this->compose = yaml_parse($data);
-3
View File
@@ -11,9 +11,6 @@ class Service
*/
protected $service = [];
/**
* @var string $path
*/
public function __construct(array $service)
{
$this->service = $service;
-3
View File
@@ -9,9 +9,6 @@ class Env
*/
protected $vars = [];
/**
* @var string $data
*/
public function __construct(string $data)
{
$data = explode("\n", $data);
+5 -4
View File
@@ -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
+1 -1
View File
@@ -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'] ?? []),
+2 -2
View File
@@ -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
+18 -4
View File
@@ -8,6 +8,18 @@ use Utopia\Database\Query;
class EventProcessor
{
/**
* @param array<mixed> $events
* @return array<string, bool>
*/
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);
}
}
+7 -7
View File
@@ -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);
+14
View File
@@ -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');
+1 -1
View File
@@ -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
@@ -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);
}
@@ -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)) {
@@ -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
@@ -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)) {
@@ -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)
@@ -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);
@@ -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')
@@ -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')
+97 -8
View File
@@ -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.
+150 -138
View File
@@ -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...');
+24 -25
View File
@@ -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') {
+4 -1
View File
@@ -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);
+40 -18
View File
@@ -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
+2 -2
View File
@@ -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
*/
+5 -5
View File
@@ -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,
+3 -3
View File
@@ -629,9 +629,9 @@ class Response extends SwooleResponse
}
/**
* Return the currently set filter
* Return the currently set filters
*
* @return Filter
* @return array<Filter>
*/
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
+18 -4
View File
@@ -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);
+2 -2
View File
@@ -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
{
@@ -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
@@ -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] = [];
}
}
@@ -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];
}
/**
+34 -34
View File
@@ -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
@@ -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] = [];
}
}
@@ -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] = [];
}
}
@@ -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,
@@ -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] = [];
}
}
+10 -10
View File
@@ -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')]
+8 -8
View File
@@ -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()
@@ -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);
}
}
@@ -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']);
+4 -4
View File
@@ -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
@@ -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
@@ -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
@@ -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
}
]
+502
View File
@@ -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
}
]
@@ -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"
}
}
]
+602
View File
@@ -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"
}
]
+402
View File
@@ -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"
}
]
@@ -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
]
}
]
@@ -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']);
}
@@ -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
{
@@ -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
{
@@ -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
{
@@ -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
{