Compare commits

..
Author SHA1 Message Date
Prem Palanisamy 995b1c76ca fix: restore (int) cast on getDatabasesDB tenant to match cloud 2026-04-02 12:11:24 +02:00
Prem Palanisamy a071c715bc Merge remote-tracking branch 'origin/1.9.x' into bump-database-version2 2026-04-02 10:10:03 +02:00
Prem Palanisamy 52981e0164 fix: restore full CI matrix and revert test/endpoint changes to 1.9.x 2026-04-02 10:09:36 +02:00
Chirag AggarwalandGitHub 387668bb47 Merge pull request #11748 from appwrite/feat-smtp-messaging-adapter
Replace PHPMailer with utopia-php/messaging SMTP adapter
2026-04-02 12:11:58 +05:30
Chirag Aggarwal b6e95f4502 test: remove redundant headers empty-key case 2026-04-02 11:05:03 +05:30
Chirag Aggarwal 1900492aad Merge branch '1.9.x' into feat-smtp-messaging-adapter 2026-04-02 10:58:09 +05:30
Prem Palanisamy cb9d3869f0 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version2
# Conflicts:
#	src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php
2026-04-02 06:53:05 +02:00
Chirag AggarwalandGitHub 96f4ef7d22 Merge pull request #11751 from appwrite/chore/remove-phpstan-baseline
chore: remove phpstan baseline
2026-04-02 10:22:21 +05:30
Chirag AggarwalandGitHub 8e02a5a227 Merge pull request #11757 from appwrite/air/fix-flaky-migration-test-580aa1bc-d
Fix flaky vectordb migration test
2026-04-02 10:19:15 +05:30
Prem Palanisamy d17ae517d3 fix: replace exists+create with try/catch for vectorsdb metadata init 2026-04-02 06:26:55 +02:00
Chirag Aggarwal c2dd8aceda use stable 2026-04-02 09:42:59 +05:30
Chirag Aggarwal 24fcf4247c Fix flaky vectordb migration test 2026-04-02 09:41:20 +05:30
Chirag Aggarwal 2dc20ef0eb Update utopia-php/messaging lock to latest 2026-04-02 09:31:48 +05:30
Chirag Aggarwal 04943b6313 Pass recipient display name in EmailMessage to field
Use associative array format ['email' => ..., 'name' => ...] for
the to field so the recipient display name appears in the To header.
2026-04-02 09:29:57 +05:30
Chirag Aggarwal eb366cf94b fix: preserve cors max age type 2026-04-02 08:25:59 +05:30
Chirag Aggarwal 77b4f8b7a0 style: apply formatter 2026-04-02 08:23:51 +05:30
Chirag Aggarwal 24bd4c3d7b fix: preserve migration resource stats 2026-04-02 08:11:57 +05:30
Prem Palanisamy 25d9891f2b fix: use email probe in JSON export test, format fixes, add requireAdapter skips to Databases VectorsDB/DocumentsDB tests 2026-04-02 04:39:56 +02:00
Prem Palanisamy 8c9f9c6f58 fix: inherit project shared tables for separate pools, skip cross-engine migration tests 2026-04-02 02:05:10 +02:00
Chirag Aggarwal 33f8e35b62 chore: remove phpstan baseline 2026-04-01 23:01:11 +05:30
Chirag Aggarwal 2c1060f57a Fix null array key in HeadersTest
Cast null to string to avoid invalid array key type error
while preserving the test's intent of validating empty keys.
2026-04-01 21:31:08 +05:30
Chirag Aggarwal 30e0ca81bd Fix Usage::fromArray() to use static return type with new static()
Keep covariant return type with parent Base::fromArray(). The
new static() is safe here because the cloud subclass constructor
is backwards-compatible via optional params.
2026-04-01 21:30:36 +05:30
Chirag Aggarwal 92cc382a6b Change Usage::fromArray() return type from static to self
The cloud subclass has a different constructor signature so
new static() is unsafe. Use self since subclasses that need
deserialization should override fromArray() themselves.
2026-04-01 21:25:22 +05:30
Prem Palanisamy c322cc3ffe Merge remote-tracking branch 'origin/1.9.x' into bump-database-version
# Conflicts:
#	src/Appwrite/Event/Message/Usage.php
2026-04-01 17:34:13 +02:00
Chirag Aggarwal 7d428ffe83 trigger ci 2026-04-01 21:03:07 +05:30
Chirag Aggarwal fb96aecbef Use new static() in Usage::fromArray() for late static binding
Required since the class is no longer final and the return type
is static, so subclasses get the correct type.
2026-04-01 19:52:16 +05:30
Chirag Aggarwal d531c29fc8 Remove final from Usage class to allow cloud override 2026-04-01 17:20:34 +05:30
Chirag Aggarwal 7044601603 lock file 2026-04-01 17:05:14 +05:30
Chirag Aggarwal 53bc126015 Remove stale PHPMailer baseline entry from PHPStan
The PHPMailer $Port type error no longer occurs since registers.php
now uses the SMTP adapter instead of PHPMailer directly.
2026-04-01 17:01:15 +05:30
Chirag Aggarwal f1dc468e50 Restore replyTo elseif logic to preserve old behavior
customMailOptions replyTo and smtp replyTo are mutually exclusive,
matching the original PHPMailer implementation.
2026-04-01 16:37:08 +05:30
Chirag Aggarwal ce63b00cf4 lock file 2026-04-01 16:29:06 +05:30
Chirag Aggarwal b9aaecba25 Replace PHPMailer with utopia-php/messaging SMTP adapter
Use Utopia\Messaging\Adapter\Email\SMTP instead of raw PHPMailer
for the smtp register, Mails worker, and Doctor task. This enables
swapping email adapters (e.g. Resend) via DI override in downstream
repos by type-hinting against the EmailAdapter base class.
2026-04-01 16:25:33 +05:30
Prem Palanisamy d9eb69aa47 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version 2026-04-01 10:41:39 +02:00
Chirag AggarwalandGitHub a5b0378138 Merge pull request #11737 from appwrite/codex/phpstan-baseline-part-2
[codex] Fix PHPStan baseline cleanup issues (part 2)
2026-04-01 13:23:15 +05:30
Chirag AggarwalandGitHub 320068a576 Merge pull request #11740 from appwrite/speed-up-phpstan
Speed up PHPStan analysis with result caching
2026-04-01 13:01:07 +05:30
Chirag Aggarwal 3cd90ae629 fix analyze 2026-04-01 12:59:51 +05:30
Chirag Aggarwal 44f33473bd Use composer analyze in CI to stay in sync with local workflow 2026-04-01 12:04:31 +05:30
Chirag Aggarwal 358f1b78a8 Speed up PHPStan analysis with result caching
Configure a project-local result cache directory so PHPStan only
re-analyses files that changed. In CI, persist the cache across
runs with actions/cache and suppress progress output.
2026-04-01 12:00:32 +05:30
Jake BarnbyandGitHub 1f6b9d94bf Merge pull request #11739 from appwrite/readme-update-1.9.0-installation 2026-04-01 05:53:38 +00:00
Aditya OberaiandGitHub a734c8cd46 Update installation commands in readme for 1.9.0 to include self-hosted wizard 2026-04-01 11:20:45 +05:30
Chirag Aggarwal 1788e1bd6c Address PR review feedback 2026-04-01 11:15:59 +05:30
Prem Palanisamy 7f9ce1ca85 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version 2026-04-01 07:37:57 +02:00
Chirag Aggarwal 983adf3ffd Fix analyze regressions in PHPStan cleanup 2026-04-01 11:00:26 +05:30
Chirag Aggarwal f2ea0b9b48 Fix PHPStan baseline cleanup issues (part 2) 2026-04-01 10:20:20 +05:30
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
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
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
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 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
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
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
124 changed files with 4152 additions and 2136 deletions
+1
View File
@@ -39,6 +39,7 @@ _APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379
_APP_REDIS_PASS=
_APP_REDIS_USER=
COMPOSE_PROFILES=mariadb,mongodb,postgresql
_APP_DB_ADAPTER=mongodb
_APP_DB_HOST=mongodb
_APP_DB_PORT=27017
+160 -79
View File
@@ -150,8 +150,16 @@ jobs:
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Cache PHPStan result cache
uses: actions/cache@v4
with:
path: .phpstan-cache
key: phpstan-${{ github.sha }}
restore-keys: |
phpstan-
- name: Run PHPStan
run: composer analyze
run: composer analyze -- --no-progress
locale:
name: Checks / Locale
@@ -174,46 +182,22 @@ jobs:
outputs:
databases: ${{ steps.generate.outputs.databases }}
modes: ${{ steps.generate.outputs.modes }}
services: ${{ steps.generate.outputs.services }}
steps:
- name: Generate matrix
id: generate
uses: actions/github-script@v8
with:
script: |
const databases = [
{ name: 'MariaDB', env: '.env,compose/mariadb.env', compose: 'docker-compose.yml:compose/mariadb.yml' },
{ name: 'PostgreSQL', env: '.env,compose/postgresql.env', compose: 'docker-compose.yml:compose/postgresql.yml' },
{ name: 'MongoDB', env: '.env', compose: 'docker-compose.yml' },
];
const modes = ['dedicated', 'shared_v1', 'shared_v2'];
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
const services = [
{ name: 'Account', parallel: true },
{ name: 'Avatars', parallel: true, runner: 'blacksmith-4vcpu-ubuntu-2404' },
{ name: 'Databases', runner: 'blacksmith-4vcpu-ubuntu-2404' },
{ name: 'TablesDB', runner: 'blacksmith-4vcpu-ubuntu-2404' },
{ name: 'Functions', runner: 'blacksmith-4vcpu-ubuntu-2404' },
{ name: 'FunctionsSchedule', parallel: true },
{ name: 'GraphQL', parallel: true },
{ name: 'Projects', parallel: true },
{ name: 'Realtime', runner: 'blacksmith-4vcpu-ubuntu-2404' },
{ name: 'Sites', parallel: true, runner: 'blacksmith-4vcpu-ubuntu-2404' },
{ name: 'Teams', parallel: true },
{ name: 'Users', parallel: true },
{ name: 'ProjectWebhooks', parallel: true },
{ name: 'Migrations', parallel: true },
{ name: 'Project', parallel: true },
];
core.setOutput('services', JSON.stringify(services));
const defaultDatabases = [databases.find(d => d.name === 'MongoDB')];
const defaultDatabases = ['MongoDB'];
const defaultModes = ['dedicated'];
const pr = context.payload.pull_request;
if (!pr) {
core.setOutput('databases', JSON.stringify(databases));
core.setOutput('modes', JSON.stringify(modes));
core.setOutput('databases', JSON.stringify(allDatabases));
core.setOutput('modes', JSON.stringify(allModes));
return;
}
@@ -234,8 +218,8 @@ jobs:
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 ? databases : defaultDatabases));
core.setOutput('modes', JSON.stringify(databaseChanged ? modes : defaultModes));
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
build:
name: Build
@@ -312,29 +296,28 @@ jobs:
run: docker compose exec -T appwrite vars
- name: Run Unit Tests
timeout-minutes: 15
run: >-
docker compose exec
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite test /usr/src/code/tests/unit
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/unit
command: >-
docker compose exec
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite test /usr/src/code/tests/unit
e2e_general:
name: Tests / E2E / ${{ matrix.database.name }} (${{ matrix.mode }}) / General
name: Tests / E2E / General
runs-on: ubuntu-latest
needs: [build, matrix]
env:
COMPOSE_FILE: ${{ matrix.database.compose }}
COMPOSE_ENV_FILES: ${{ matrix.database.env }}
needs: build
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
database: ${{ fromJSON(needs.matrix.outputs.databases) }}
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
steps:
- name: Checkout repository
- name: checkout
uses: actions/checkout@v6
- name: Download Docker Image
@@ -351,10 +334,6 @@ jobs:
- name: Load and Start Appwrite
timeout-minutes: 5
env:
_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' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
@@ -368,12 +347,19 @@ jobs:
sleep 1
done
- name: Run tests
timeout-minutes: 20
run: >-
docker compose exec -T
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite vendor/bin/paratest --processes $(nproc) --functional --testsuite GeneralGroup --exclude-group abuseEnabled --exclude-group screenshots
- name: Run General Tests
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/General
command: >-
docker compose exec -T
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite test /usr/src/code/tests/e2e/General
- name: Failure Logs
if: failure()
@@ -382,12 +368,9 @@ jobs:
docker compose logs
e2e_service:
name: Tests / E2E / ${{ matrix.database.name }} (${{ matrix.mode }}) / ${{ matrix.service.name }}
runs-on: ${{ matrix.service.runner || 'ubuntu-latest' }}
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
needs: [build, matrix]
env:
COMPOSE_FILE: ${{ matrix.database.compose }}
COMPOSE_ENV_FILES: ${{ matrix.database.env }}
permissions:
contents: read
pull-requests: write
@@ -396,7 +379,45 @@ jobs:
matrix:
database: ${{ fromJSON(needs.matrix.outputs.databases) }}
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
service: ${{ fromJSON(needs.matrix.outputs.services) }}
service: [
Account,
Avatars,
Console,
Databases,
TablesDB,
Functions,
FunctionsSchedule,
GraphQL,
Health,
Locale,
Projects,
Realtime,
Sites,
Proxy,
Storage,
Tokens,
Teams,
Users,
ProjectWebhooks,
Webhooks,
VCS,
Messaging,
Migrations,
Project
]
include:
- service: Databases
runner: blacksmith-4vcpu-ubuntu-2404
- service: Sites
runner: blacksmith-4vcpu-ubuntu-2404
- service: Functions
runner: blacksmith-4vcpu-ubuntu-2404
- service: Avatars
runner: blacksmith-4vcpu-ubuntu-2404
- service: Realtime
runner: blacksmith-4vcpu-ubuntu-2404
- service: TablesDB
runner: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v6
@@ -407,6 +428,25 @@ jobs:
name: ${{ env.IMAGE }}
path: /tmp
- name: Set database environment
run: |
if [ "${{ matrix.database }}" = "MariaDB" ]; then
echo "COMPOSE_PROFILES=mariadb" >> $GITHUB_ENV
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
echo "_APP_DB_HOST=mariadb" >> $GITHUB_ENV
echo "_APP_DB_PORT=3306" >> $GITHUB_ENV
elif [ "${{ matrix.database }}" = "MongoDB" ]; then
echo "COMPOSE_PROFILES=mongodb" >> $GITHUB_ENV
echo "_APP_DB_ADAPTER=mongodb" >> $GITHUB_ENV
echo "_APP_DB_HOST=mongodb" >> $GITHUB_ENV
echo "_APP_DB_PORT=27017" >> $GITHUB_ENV
elif [ "${{ matrix.database }}" = "PostgreSQL" ]; then
echo "COMPOSE_PROFILES=postgresql" >> $GITHUB_ENV
echo "_APP_DB_ADAPTER=postgresql" >> $GITHUB_ENV
echo "_APP_DB_HOST=postgresql" >> $GITHUB_ENV
echo "_APP_DB_PORT=5432" >> $GITHUB_ENV
fi
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
@@ -419,6 +459,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
@@ -433,11 +477,26 @@ jobs:
done
- name: Run tests
timeout-minutes: 20
run: |
docker compose exec -T \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) ${{ matrix.service.parallel && '--functional' || '' }} --testsuite ${{ matrix.service.name }} --exclude-group abuseEnabled --exclude-group screenshots
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
timeout_minutes: 20
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/${{ matrix.service }}
command: |
SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}"
# Services that rely on sequential test method execution (shared static state)
FUNCTIONAL_FLAG="--functional"
case "${{ matrix.service }}" in
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
esac
docker compose exec -T \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
- name: Failure Logs
if: failure()
@@ -478,17 +537,28 @@ 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
docker compose up -d --quiet-pull --wait
- name: Run tests
timeout-minutes: 15
run: >-
docker compose exec -T
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite test /usr/src/code/tests/e2e --group=abuseEnabled
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e
command: >-
docker compose exec -T
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite test /usr/src/code/tests/e2e --group=abuseEnabled
- name: Failure Logs
if: failure()
@@ -528,6 +598,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
@@ -542,11 +616,18 @@ jobs:
done
- name: Run tests
timeout-minutes: 15
run: >-
docker compose exec -T
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite test /usr/src/code/tests/e2e/Services/Sites --group=screenshots
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 60
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/Sites
command: >-
docker compose exec -T
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite test /usr/src/code/tests/e2e/Services/Sites --group=screenshots
- name: Failure Logs
if: failure()
+1
View File
@@ -21,6 +21,7 @@ appwrite.config.json
/app/config/specs/
/docs/examples/
.phpunit.cache
.phpstan-cache
playwright-report
test-results
docker-compose.web-installer.yml
+1 -1
View File
@@ -20,7 +20,7 @@ Self-hosted Backend-as-a-Service platform. Hybrid monolithic-microservice archit
- PHP 8.3+, Swoole 6.x (async runtime, replaces PHP-FPM)
- Utopia PHP framework (HTTP routing, CLI, DI, queue)
- MongoDB (default), MariaDB, PostgreSQL (adapters via utopia-php/database; use `compose/mariadb.yml` or `compose/postgresql.yml` overlays to switch)
- 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
+1 -1
View File
@@ -296,7 +296,7 @@ Appwrite stack is a combination of a variety of open-source technologies and too
### Other Technologies
- Redis - for managing cache and in-memory data (currently, we do not use Redis for persistent data).
- MongoDB - default database for storage and queries (MariaDB and PostgreSQL also supported via compose overlays).
- MariaDB - for database storage and queries.
- InfluxDB - for managing stats and time-series based data
- Statsd - for sending data over UDP protocol (using Telegraf)
- ClamAV - for validating and scanning storage files.
+3
View File
@@ -72,6 +72,7 @@ Before running the installation command, make sure you have [Docker](https://www
```bash
docker run -it --rm \
--publish 20080:20080 \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
@@ -84,6 +85,7 @@ docker run -it --rm \
```cmd
docker run -it --rm ^
--publish 20080:20080 ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
@@ -94,6 +96,7 @@ docker run -it --rm ^
```powershell
docker run -it --rm `
--publish 20080:20080 `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
+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}},",
-3
View File
@@ -31,9 +31,6 @@ class FunctionUseCases
public const DEV_TOOLS = 'dev-tools';
public const AUTH = 'auth';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
+3 -6
View File
@@ -25,9 +25,6 @@ class SiteUseCases
public const FORMS = 'forms';
public const DASHBOARD = 'dashboard';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
@@ -252,7 +249,7 @@ return [
'frameworks' => [
getFramework('VITE', [
'providerRootDirectory' => './vite/vitepress',
'outputDirectory' => '404.html',
'fallbackFile' => '404.html',
'installCommand' => 'npm i vitepress && npm install',
'buildCommand' => 'npm run docs:build',
'outputDirectory' => './.vitepress/dist',
@@ -275,7 +272,7 @@ return [
'frameworks' => [
getFramework('VUE', [
'providerRootDirectory' => './vue/vuepress',
'outputDirectory' => '404.html',
'fallbackFile' => '404.html',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './src/.vuepress/dist',
@@ -298,7 +295,7 @@ return [
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/docusaurus',
'outputDirectory' => '404.html',
'fallbackFile' => '404.html',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './build',
+12 -7
View File
@@ -695,6 +695,7 @@ Http::delete('/v1/account/sessions')
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
$currentSession = null;
foreach ($sessions as $session) {/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
@@ -716,6 +717,7 @@ Http::delete('/v1/account/sessions')
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
// Use current session for events.
$currentSession = $session;
$queueForEvents
->setPayload($response->output($session, Response::MODEL_SESSION));
@@ -728,9 +730,11 @@ Http::delete('/v1/account/sessions')
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId());
if ($currentSession instanceof Document) {
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $currentSession->getId());
}
$response->noContent();
});
@@ -776,7 +780,8 @@ Http::get('/v1/account/sessions/:sessionId')
->setAttribute('secret', $session->getAttribute('secret', ''))
;
return $response->dynamic($session, Response::MODEL_SESSION);
$response->dynamic($session, Response::MODEL_SESSION);
return;
}
}
@@ -956,7 +961,7 @@ Http::patch('/v1/account/sessions/:sessionId')
->setPayload($response->output($session, Response::MODEL_SESSION))
;
return $response->dynamic($session, Response::MODEL_SESSION);
$response->dynamic($session, Response::MODEL_SESSION);
});
Http::post('/v1/account/sessions/email')
@@ -1988,7 +1993,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
if (isset($sessionUpgrade) && $sessionUpgrade) {
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
foreach ($user->getAttribute('targets', []) as $target) {
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
continue;
@@ -4715,5 +4720,5 @@ Http::delete('/v1/account/identities/:identityId')
->setParam('identityId', $identity->getId())
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
return $response->noContent();
$response->noContent();
});
+1
View File
@@ -231,6 +231,7 @@ Http::get('/v1/locale/continents')
->inject('locale')
->action(function (Response $response, Locale $locale) {
$list = array_keys(Config::getParam('locale-continents'));
$output = [];
foreach ($list as $value) {
$output[] = new Document([
+1 -1
View File
@@ -3566,7 +3566,7 @@ Http::post('/v1/messaging/messages/push')
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
$scheduleTime = $scheduledAt;
if (!\is_null($scheduleTime)) {
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
} else {
+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
@@ -1570,7 +1570,7 @@ Http::post('/v1/projects/:projectId/smtp/tests')
->trigger();
}
return $response->noContent();
$response->noContent();
});
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
+5 -7
View File
@@ -2337,9 +2337,8 @@ Http::post('/v1/users/:userId/sessions')
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($session, Response::MODEL_SESSION);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($session, Response::MODEL_SESSION);
});
Http::post('/v1/users/:userId/tokens')
@@ -2402,9 +2401,8 @@ Http::post('/v1/users/:userId/tokens')
->setParam('tokenId', $token->getId())
->setPayload($response->output($token, Response::MODEL_TOKEN));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($token, Response::MODEL_TOKEN);
});
Http::delete('/v1/users/:userId/sessions/:sessionId')
@@ -2658,7 +2656,7 @@ Http::delete('/v1/users/identities/:identityId')
->setParam('identityId', $identity->getId())
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
return $response->noContent();
$response->noContent();
});
Http::post('/v1/users/:userId/jwts')
+17 -10
View File
@@ -166,14 +166,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
$response->redirect('https://' . $request->getHostname() . $request->getURI());
return false;
}
}
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
/** @var Document $deployment */
if (!empty($rule->getAttribute('deploymentId', ''))) {
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
} else {
@@ -244,6 +244,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($isPreview && $requirePreview) {
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
$authorized = false;
$user = new Document();
// Security checks to mark authorized true
if (!empty($cookie)) {
@@ -273,7 +274,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$membershipExists = false;
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if (!$project->isEmpty() && isset($user)) {
if (!$project->isEmpty() && !$user->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$membership = $user->find('teamId', $teamId, 'memberships');
if (!empty($membership)) {
@@ -379,7 +380,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$executionId = ID::unique();
$headers = \array_merge([], $requestHeaders);
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-user-id'] = '';
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
@@ -459,7 +460,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
]);
@@ -529,6 +530,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
/** Execute function */
$executionResponse = [
'headers' => [],
'body' => '',
];
try {
$version = match ($type) {
'function' => $resource->getAttribute('version', 'v2'),
@@ -734,7 +740,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
$execution->setAttribute('responseHeaders', $headers);
$body = $execution['responseBody'] ?? '';
$body = $execution['responseBody'];
$contentType = 'text/plain';
foreach ($executionResponse['headers'] as $name => $values) {
@@ -865,9 +871,9 @@ Http::init()
Request::setRoute($route);
if ($route === null) {
return $response
->setStatusCode(404)
->send('Not Found');
$response->setStatusCode(404);
$response->send('Not Found');
return;
}
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
@@ -973,7 +979,8 @@ Http::init()
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
$response->redirect('https://' . $request->getHostname() . $request->getURI());
return;
}
}
});
+30 -28
View File
@@ -244,36 +244,38 @@ Http::get('/v1/mock/github/callback')
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
}
if (!empty($providerInstallationId)) {
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => false
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
if (empty($providerInstallationId)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing provider installation ID');
}
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => false
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
$response->json([
'installationId' => $installation->getId(),
]);
+4 -3
View File
@@ -3,6 +3,8 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/init/span.php';
global $register;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Constant;
@@ -31,7 +33,6 @@ use Utopia\Http\Files;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
use Utopia\Pools\Group;
use Utopia\Span\Span;
use Utopia\System\System;
@@ -293,7 +294,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
go(function () use ($register, $app) {
$pools = $register->get('pools');
/** @var Group $pools */
/** @var \Utopia\Pools\Group $pools */
Http::setResource('pools', fn () => $pools);
/** @var array $collections */
@@ -655,7 +656,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
/** @var Table $riskyDomains */
/** @var \Swoole\Table $riskyDomains */
$riskyDomains = $app->getResource('riskyDomains');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
+21 -36
View File
@@ -6,7 +6,6 @@ use Appwrite\Hooks\Hooks;
use Appwrite\PubSub\Adapter\Redis as PubSub;
use Appwrite\URL\URL as AppwriteURL;
use MaxMind\Db\Reader;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Database\PDOProxy;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Config\Config;
@@ -25,6 +24,7 @@ use Utopia\Logger\Adapter\LogOwl;
use Utopia\Logger\Adapter\Raygun;
use Utopia\Logger\Adapter\Sentry;
use Utopia\Logger\Logger;
use Utopia\Messaging\Adapter\Email\SMTP;
use Utopia\Mongo\Client as MongoClient;
use Utopia\Pools\Adapter\Stack as StackPool;
use Utopia\Pools\Adapter\Swoole as SwoolePool;
@@ -56,7 +56,7 @@ $register->set('logger', function () {
}
try {
$loggingProvider = new DSN($providerConfig ?? '');
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
$providerConfig = match ($providerName) {
@@ -76,7 +76,7 @@ $register->set('logger', function () {
};
}
if (empty($providerName) || empty($providerConfig)) {
if (empty($providerName)) {
return;
}
@@ -121,7 +121,7 @@ $register->set('realtimeLogger', function () {
default => ['key' => $loggingProvider->getHost()],
};
if (empty($providerName) || empty($providerConfig)) {
if (empty($providerName)) {
return;
}
@@ -242,8 +242,8 @@ $register->set('pools', function () {
],
];
$maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14);
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
@@ -308,7 +308,7 @@ $register->set('pools', function () {
]);
});
},
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase, $dsn) {
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
try {
$mongo = new MongoClient($dsnDatabase, $dsnHost, (int)$dsnPort, $dsnUser, $dsnPass, false);
@$mongo->connect();
@@ -433,35 +433,20 @@ $register->set('db', function () {
});
$register->set('smtp', function () {
$mail = new PHPMailer(true);
$mail->isSMTP();
$username = System::getEnv('_APP_SMTP_USERNAME');
$password = System::getEnv('_APP_SMTP_PASSWORD');
$mail->XMailer = 'Appwrite Mailer';
$mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp');
$mail->Port = System::getEnv('_APP_SMTP_PORT', 25);
$mail->SMTPAuth = !empty($username) && !empty($password);
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
$mail->SMTPAutoTLS = false;
$mail->SMTPKeepAlive = true;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
$from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$mail->setFrom($email, $from);
$mail->addReplyTo($email, $from);
$mail->isHTML(true);
return $mail;
$username = System::getEnv('_APP_SMTP_USERNAME', '');
$password = System::getEnv('_APP_SMTP_PASSWORD', '');
return new SMTP(
host: System::getEnv('_APP_SMTP_HOST', 'smtp'),
port: (int) System::getEnv('_APP_SMTP_PORT', 25),
username: $username,
password: $password,
smtpSecure: System::getEnv('_APP_SMTP_SECURE', ''),
smtpAutoTLS: false,
xMailer: 'Appwrite Mailer',
timeout: 10,
keepAlive: true,
timelimit: 30,
);
});
$register->set('geodb', function () {
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
+26 -5
View File
@@ -696,7 +696,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$cacheKey = \sprintf(
'%s-cache-%s:%s:%s:project:%s:functions:events',
$dbForProject->getCacheName(),
$hostname ?? '',
$hostname,
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$project->getId()
@@ -888,7 +888,6 @@ 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) {
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
@@ -907,11 +906,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,7 +922,28 @@ 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)) {
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
if ($databaseHost !== $dsn->getHost()) {
$dbTypeSharedTables = match ($databaseType) {
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
default => [],
};
if (\in_array($databaseHost, $dbTypeSharedTables)) {
$database
->setSharedTables(true)
->setTenant((int)$project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
} elseif (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int)$project->getSequence())
+6 -7
View File
@@ -237,7 +237,6 @@ if (!function_exists('getTelemetry')) {
if (!function_exists('triggerStats')) {
function triggerStats(array $event, string $projectId): void
{
return;
}
}
@@ -320,14 +319,14 @@ if (!function_exists('logError')) {
$server->error(logError(...));
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
$server->onStart(function () use ($stats, $containerId, &$statsDocument) {
sleep(5); // wait for the initial database schema to be ready
Console::success('Server started successfully');
/**
* Create document for this worker to share stats across Containers.
*/
go(function () use ($register, $containerId, &$statsDocument) {
go(function () use ($containerId, &$statsDocument) {
$attempts = 0;
$database = getConsoleDB();
@@ -357,7 +356,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
*/
// TODO: Remove this if check once it doesn't cause issues for cloud
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
Timer::tick(5000, function () use ($stats, &$statsDocument) {
$payload = [];
foreach ($stats as $projectId => $value) {
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
@@ -396,7 +395,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$attempts = 0;
$start = time();
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
Timer::tick(5000, function () use ($server, $realtime, $stats) {
/**
* Sending current connections to project channels on the console project every 5 seconds.
*/
@@ -798,7 +797,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
}
});
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
$project = null;
$authorization = null;
@@ -810,7 +809,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
// Get authorization from connection (stored during onOpen)
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
if ($authorization === null) {
$authorization = new Authorization('');
$authorization = new Authorization();
}
$database = getConsoleDB();
+27 -6
View File
@@ -246,7 +246,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,9 +256,29 @@ 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)) {
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
if ($databaseHost !== $dsn->getHost()) {
$dbTypeSharedTables = match ($databaseType) {
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
default => [],
};
if (\in_array($databaseHost, $dbTypeSharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant((int) $projectDocument->getSequence())
@@ -280,14 +301,14 @@ Server::setResource('abuseRetention', function () {
Server::setResource('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
Server::setResource('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
});
Server::setResource('cache', function (Registry $register) {
-3
View File
@@ -1,3 +0,0 @@
_APP_DB_ADAPTER=mariadb
_APP_DB_HOST=mariadb
_APP_DB_PORT=3306
-36
View File
@@ -1,36 +0,0 @@
x-logging: &x-logging
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
services:
mongodb:
scale: 0
mariadb:
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
networks:
- appwrite
volumes:
- appwrite-mariadb:/var/lib/mysql:rw
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
- MYSQL_USER=${_APP_DB_USER}
- MYSQL_PASSWORD=${_APP_DB_PASS}
- MARIADB_AUTO_UPGRADE=1
command: "mysqld --innodb-flush-method=fsync"
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 12
volumes:
appwrite-mariadb:
-3
View File
@@ -1,3 +0,0 @@
_APP_DB_ADAPTER=postgresql
_APP_DB_HOST=postgresql
_APP_DB_PORT=5432
-3
View File
@@ -1,3 +0,0 @@
services:
mongodb:
scale: 0
+2 -2
View File
@@ -72,8 +72,8 @@
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.8.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
Generated
+26 -26
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": "4fe91e67f343fbe6deac1fdc7eda949f",
"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",
@@ -4467,23 +4467,23 @@
},
{
"name": "utopia-php/messaging",
"version": "0.20.1",
"version": "0.22.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0"
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0",
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-openssl": "*",
"giggsey/libphonenumber-for-php-lite": "9.0.23",
"php": ">=8.0.0",
"php": ">=8.1.0",
"phpmailer/phpmailer": "6.9.1"
},
"require-dev": {
@@ -4512,22 +4512,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
"source": "https://github.com/utopia-php/messaging/tree/0.20.1"
"source": "https://github.com/utopia-php/messaging/tree/0.22.0"
},
"time": "2026-02-06T09:56:06+00:00"
"time": "2026-04-02T04:09:19+00:00"
},
{
"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
View File
@@ -4,6 +4,7 @@
services:
appwrite-mongo-express:
profiles: ["mongodb"]
image: mongo-express
container_name: appwrite-mongo-express
networks:
+37 -13
View File
@@ -1261,27 +1261,28 @@ services:
retries: 20
start_period: 5s
postgresql:
image: appwrite/postgres:0.1.0
container_name: appwrite-postgresql
mariadb:
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
networks:
- appwrite
volumes:
- appwrite-postgresql:/var/lib/postgresql/18/data:rw
- appwrite-mariadb:/var/lib/mysql:rw
ports:
- "5432:5432"
- "3306:3306"
environment:
- POSTGRES_DB=${_APP_DB_SCHEMA}
- POSTGRES_USER=${_APP_DB_USER}
- POSTGRES_PASSWORD=${_APP_DB_PASS}
- MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
- MYSQL_USER=${_APP_DB_USER}
- MYSQL_PASSWORD=${_APP_DB_PASS}
- MARIADB_AUTO_UPGRADE=1
command: "mysqld --innodb-flush-method=fsync"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
command: "postgres"
retries: 12
mongodb:
image: mongo:8.2.5
@@ -1319,6 +1320,28 @@ services:
retries: 10
start_period: 30s
postgresql:
image: appwrite/postgres:0.1.0
container_name: appwrite-postgresql
<<: *x-logging
networks:
- appwrite
volumes:
- appwrite-postgresql:/var/lib/postgresql/18/data:rw
ports:
- "5432:5432"
environment:
- POSTGRES_DB=${_APP_DB_SCHEMA}
- POSTGRES_USER=${_APP_DB_USER}
- POSTGRES_PASSWORD=${_APP_DB_PASS}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
command: "postgres"
ollama:
image: appwrite/ollama:0.1.1
container_name: ollama
@@ -1441,9 +1464,10 @@ networks:
volumes:
appwrite-postgresql:
appwrite-mariadb:
appwrite-mongodb:
appwrite-mongodb-keyfile:
appwrite-postgresql:
appwrite-redis:
appwrite-cache:
appwrite-uploads:
+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.
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -1,8 +1,6 @@
includes:
- phpstan-baseline.neon
parameters:
level: 3
tmpDir: .phpstan-cache
paths:
- src
- app
@@ -14,4 +12,3 @@ parameters:
- vendor/swoole/ide-helper
excludePaths:
- tests/resources
+10 -105
View File
@@ -16,123 +16,28 @@
<testsuite name="unit">
<directory>./tests/unit</directory>
</testsuite>
<testsuite name="General">
<directory>./tests/e2e/General</directory>
<directory>./tests/e2e/Scopes</directory>
</testsuite>
<testsuite name="Console">
<directory>./tests/e2e/Services/Console</directory>
</testsuite>
<testsuite name="Health">
<directory>./tests/e2e/Services/Health</directory>
</testsuite>
<testsuite name="Locale">
<directory>./tests/e2e/Services/Locale</directory>
</testsuite>
<testsuite name="Tokens">
<directory>./tests/e2e/Services/Tokens</directory>
</testsuite>
<testsuite name="VCS">
<directory>./tests/e2e/Services/VCS</directory>
</testsuite>
<testsuite name="Webhooks">
<directory>./tests/e2e/Services/Webhooks</directory>
</testsuite>
<testsuite name="GeneralGroup">
<directory>./tests/e2e/General</directory>
<directory>./tests/e2e/Scopes</directory>
<directory>./tests/e2e/Services/Console</directory>
<directory>./tests/e2e/Services/Health</directory>
<directory>./tests/e2e/Services/Locale</directory>
<directory>./tests/e2e/Services/Messaging</directory>
<directory>./tests/e2e/Services/Proxy</directory>
<directory>./tests/e2e/Services/Storage</directory>
<directory>./tests/e2e/Services/Tokens</directory>
<directory>./tests/e2e/Services/VCS</directory>
<directory>./tests/e2e/Services/Webhooks</directory>
</testsuite>
<testsuite name="Account">
<directory>./tests/e2e/Services/Account</directory>
</testsuite>
<testsuite name="Avatars">
<directory>./tests/e2e/Services/Avatars</directory>
</testsuite>
<testsuite name="Databases">
<directory>./tests/e2e/Services/Databases</directory>
</testsuite>
<testsuite name="Functions">
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
</testsuite>
<testsuite name="FunctionsSchedule">
<directory>./tests/e2e/Services/FunctionsSchedule</directory>
</testsuite>
<testsuite name="GraphQL">
<directory>./tests/e2e/Services/GraphQL</directory>
</testsuite>
<testsuite name="Messaging">
<directory>./tests/e2e/Services/Messaging</directory>
</testsuite>
<testsuite name="Migrations">
<directory>./tests/e2e/Services/Migrations</directory>
</testsuite>
<testsuite name="Project">
<directory>./tests/e2e/Services/Project</directory>
</testsuite>
<testsuite name="ProjectWebhooks">
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
</testsuite>
<testsuite name="Projects">
<directory>./tests/e2e/Services/Projects</directory>
</testsuite>
<testsuite name="Proxy">
<directory>./tests/e2e/Services/Proxy</directory>
</testsuite>
<testsuite name="Realtime">
<directory>./tests/e2e/Services/Realtime</directory>
</testsuite>
<testsuite name="Sites">
<directory>./tests/e2e/Services/Sites</directory>
</testsuite>
<testsuite name="Storage">
<directory>./tests/e2e/Services/Storage</directory>
</testsuite>
<testsuite name="TablesDB">
<directory>./tests/e2e/Services/TablesDB</directory>
</testsuite>
<testsuite name="Teams">
<directory>./tests/e2e/Services/Teams</directory>
</testsuite>
<testsuite name="Users">
<directory>./tests/e2e/Services/Users</directory>
</testsuite>
<testsuite name="e2e">
<file>./tests/e2e/Client.php</file>
<directory>./tests/e2e/General</directory>
<directory>./tests/e2e/Scopes</directory>
<directory>./tests/e2e/Services/Teams</directory>
<directory>./tests/e2e/Services/Realtime</directory>
<directory>./tests/e2e/Services/Account</directory>
<directory>./tests/e2e/Services/Avatars</directory>
<directory>./tests/e2e/Services/Users</directory>
<directory>./tests/e2e/Services/Console</directory>
<directory>./tests/e2e/Services/Avatars</directory>
<directory>./tests/e2e/Services/Databases</directory>
<directory>./tests/e2e/Services/FunctionsSchedule</directory>
<directory>./tests/e2e/Services/GraphQL</directory>
<directory>./tests/e2e/Services/Health</directory>
<directory>./tests/e2e/Services/Locale</directory>
<directory>./tests/e2e/Services/Projects</directory>
<directory>./tests/e2e/Services/Storage</directory>
<directory>./tests/e2e/Services/Tokens</directory>
<directory>./tests/e2e/Services/Webhooks</directory>
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
<directory>./tests/e2e/Services/Messaging</directory>
<directory>./tests/e2e/Services/Migrations</directory>
<directory>./tests/e2e/Services/Project</directory>
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
<directory>./tests/e2e/Services/Projects</directory>
<directory>./tests/e2e/Services/Proxy</directory>
<directory>./tests/e2e/Services/Realtime</directory>
<directory>./tests/e2e/Services/Sites</directory>
<directory>./tests/e2e/Services/Storage</directory>
<directory>./tests/e2e/Services/TablesDB</directory>
<directory>./tests/e2e/Services/Teams</directory>
<directory>./tests/e2e/Services/Tokens</directory>
<directory>./tests/e2e/Services/Users</directory>
<directory>./tests/e2e/Services/VCS</directory>
<directory>./tests/e2e/Services/Webhooks</directory>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
+3 -5
View File
@@ -285,7 +285,7 @@ class Event
*
* @param string $key
* @param Document $context
* @return self
* @return static
*/
public function setContext(string $key, Document $context): self
{
@@ -309,7 +309,7 @@ class Event
/**
* Set class used for this event.
* @param string $class
* @return self
* @return static
*/
public function setClass(string $class): self
{
@@ -648,10 +648,8 @@ class Event
*
* @param Event $event
*
* @return self
*
*/
public function from(Event $event): self
public function from(Event $event): static
{
$this->project = $event->getProject();
$this->user = $event->getUser();
+2 -1
View File
@@ -4,7 +4,7 @@ namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Usage extends Base
class Usage extends Base
{
/**
* @param Document $project
@@ -40,6 +40,7 @@ final class Usage extends Base
*/
public static function fromArray(array $data): static
{
/** @phpstan-ignore new.static (subclass constructors are backwards-compatible via optional params) */
return new static(
project: new Document($data['project'] ?? []),
metrics: $data['metrics'] ?? [],
+6 -10
View File
@@ -25,11 +25,7 @@ class Resolvers
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
/** @var Http $utopia */
/** @var Response $response */
/** @var Request $request */
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -96,7 +92,7 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -127,7 +123,7 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -163,7 +159,7 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -195,7 +191,7 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
@@ -225,7 +221,7 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
+76 -75
View File
@@ -98,10 +98,9 @@ class Schema
foreach ($routes as $route) {
/** @var Route $route */
/** @var \Appwrite\SDK\Method $sdk */
$sdk = $route->getLabel('sdk', false);
if (empty($sdk)) {
if ($sdk === false) {
continue;
}
@@ -177,7 +176,7 @@ class Schema
$required = $attr['required'];
$default = $attr['default'];
$escapedKey = str_replace('$', '', $key);
$collections[$collectionId][$escapedKey] = [
$collections[$databaseId][$collectionId][$escapedKey] = [
'type' => Mapper::attribute(
$type,
$array,
@@ -187,80 +186,82 @@ class Schema
];
}
foreach ($collections as $collectionId => $attributes) {
$objectType = new ObjectType([
'name' => $collectionId,
'fields' => \array_merge(
["_id" => ['type' => Type::string()]],
$attributes
),
]);
$attributes = \array_merge(
$attributes,
Mapper::args('mutate')
);
$queryFields[$collectionId . 'Get'] = [
'type' => $objectType,
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentGet(
$utopia,
$databaseId,
$collectionId,
$urls['get'],
)
];
$queryFields[$collectionId . 'List'] = [
'type' => Type::listOf($objectType),
'args' => Mapper::args('list'),
'resolve' => Resolvers::documentList(
$utopia,
$databaseId,
$collectionId,
$urls['list'],
$params['list'],
),
'complexity' => $complexity,
];
$mutationFields[$collectionId . 'Create'] = [
'type' => $objectType,
'args' => $attributes,
'resolve' => Resolvers::documentCreate(
$utopia,
$databaseId,
$collectionId,
$urls['create'],
$params['create'],
)
];
$mutationFields[$collectionId . 'Update'] = [
'type' => $objectType,
'args' => \array_merge(
Mapper::args('id'),
\array_map(
fn ($attr) => $attr['type'] = Type::getNullableType($attr['type']),
foreach ($collections as $databaseId => $databaseCollections) {
foreach ($databaseCollections as $collectionId => $attributes) {
$objectType = new ObjectType([
'name' => $collectionId,
'fields' => \array_merge(
["_id" => ['type' => Type::string()]],
$attributes
),
]);
$attributes = \array_merge(
$attributes,
Mapper::args('mutate')
);
$queryFields[$collectionId . 'Get'] = [
'type' => $objectType,
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentGet(
$utopia,
$databaseId,
$collectionId,
$urls['get'],
)
),
'resolve' => Resolvers::documentUpdate(
$utopia,
$databaseId,
$collectionId,
$urls['update'],
$params['update'],
)
];
$mutationFields[$collectionId . 'Delete'] = [
'type' => Mapper::model('none'),
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentDelete(
$utopia,
$databaseId,
$collectionId,
$urls['delete'],
)
];
];
$queryFields[$collectionId . 'List'] = [
'type' => Type::listOf($objectType),
'args' => Mapper::args('list'),
'resolve' => Resolvers::documentList(
$utopia,
$databaseId,
$collectionId,
$urls['list'],
$params['list'],
),
'complexity' => $complexity,
];
$mutationFields[$collectionId . 'Create'] = [
'type' => $objectType,
'args' => $attributes,
'resolve' => Resolvers::documentCreate(
$utopia,
$databaseId,
$collectionId,
$urls['create'],
$params['create'],
)
];
$mutationFields[$collectionId . 'Update'] = [
'type' => $objectType,
'args' => \array_merge(
Mapper::args('id'),
\array_map(
fn ($attr) => $attr['type'] = Type::getNullableType($attr['type']),
$attributes
)
),
'resolve' => Resolvers::documentUpdate(
$utopia,
$databaseId,
$collectionId,
$urls['update'],
$params['update'],
)
];
$mutationFields[$collectionId . 'Delete'] = [
'type' => Mapper::model('none'),
'args' => Mapper::args('id'),
'resolve' => Resolvers::documentDelete(
$utopia,
$databaseId,
$collectionId,
$urls['delete'],
)
];
}
}
$offset += $limit;
}
+16 -7
View File
@@ -15,10 +15,13 @@ class Types
*
* @return Json
*/
public static function json(): Type
public static function json(): Json
{
if (Registry::has(Json::class)) {
return Registry::get(Json::class);
$type = Registry::get(Json::class);
if ($type instanceof Json) {
return $type;
}
}
$type = new Json();
Registry::set(Json::class, $type);
@@ -28,12 +31,15 @@ class Types
/**
* Get the JSON type.
*
* @return Json
* @return Assoc
*/
public static function assoc(): Type
public static function assoc(): Assoc
{
if (Registry::has(Assoc::class)) {
return Registry::get(Assoc::class);
$type = Registry::get(Assoc::class);
if ($type instanceof Assoc) {
return $type;
}
}
$type = new Assoc();
Registry::set(Assoc::class, $type);
@@ -45,10 +51,13 @@ class Types
*
* @return InputFile
*/
public static function inputFile(): Type
public static function inputFile(): InputFile
{
if (Registry::has(InputFile::class)) {
return Registry::get(InputFile::class);
$type = Registry::get(InputFile::class);
if ($type instanceof InputFile) {
return $type;
}
}
$type = new InputFile();
Registry::set(InputFile::class, $type);
+1 -3
View File
@@ -273,11 +273,9 @@ class Mapper
case \Appwrite\Auth\Validator\Password::class:
case \Appwrite\Event\Validator\Event::class:
case \Appwrite\Event\Validator\FunctionEvent::class:
case \Appwrite\Network\Validator\CNAME::class:
case \Utopia\Emails\Validator\Email::class:
case \Appwrite\Network\Validator\Redirect::class:
case \Appwrite\Network\Validator\DNS::class:
case \Appwrite\Network\Validator\Origin::class:
case \Appwrite\Task\Validator\Cron::class:
case \Appwrite\Utopia\Database\Validator\CustomId::class:
case \Utopia\Database\Validator\Key::class:
@@ -286,7 +284,7 @@ class Mapper
case \Utopia\Validator\HexColor::class:
case \Utopia\Validator\Host::class:
case \Utopia\Validator\IP::class:
case \Utopia\Validator\Origin::class:
case \Appwrite\Network\Validator\Origin::class:
case \Utopia\Validator\Text::class:
case \Utopia\Validator\URL::class:
case \Utopia\Validator\WhiteList::class:
+25
View File
@@ -13,6 +13,7 @@ use Utopia\Database\Exception\Limit;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\PDO;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
abstract class Migration
@@ -204,6 +205,30 @@ abstract class Migration
}
}
/**
* @param array<Query> $queries
* @return \Generator<int, Document>
* @throws Exception
*/
protected function documentsIterator(string $collection, array $queries = []): \Generator
{
$offset = 0;
do {
$documents = $this->dbForProject->find($collection, [
...$queries,
Query::limit($this->limit),
Query::offset($offset),
]);
foreach ($documents as $document) {
yield $document;
}
$offset += \count($documents);
} while (\count($documents) === $this->limit);
}
/**
* Creates collection from the config collection.
*
+4 -5
View File
@@ -1224,7 +1224,7 @@ class V15 extends Migration
* @param \Utopia\Database\Document $document
* @return \Utopia\Database\Document
*/
protected function fixDocument(Document $document)
protected function fixDocument(Document $document): Document
{
switch ($document->getCollection()) {
case 'cache':
@@ -1234,7 +1234,7 @@ class V15 extends Migration
* skipping migration for 'cache' and 'variables'.
* 'users' already migrated.
*/
return;
return $document;
case '_metadata':
/**
@@ -1480,7 +1480,6 @@ class V15 extends Migration
* Filter from the 'encrypt' filter.
*
* @param string $value
* @return string|false
*/
protected function encryptFilter(string $value): string
{
@@ -1492,8 +1491,8 @@ class V15 extends Migration
'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
'method' => OpenSSL::CIPHER_AES_128_GCM,
'iv' => \bin2hex($iv),
'tag' => \bin2hex($tag ?? ''),
'tag' => \bin2hex($tag),
'version' => '1',
]);
]) ?: '';
}
}
+1 -1
View File
@@ -452,7 +452,7 @@ class V20 extends Migration
Query::equal('period', ['1d']),
]);
$value = $query ?? 0;
$value = $query;
$this->createInfMetric($to, $value);
}
+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
@@ -48,7 +48,7 @@ final class Cors
/**
* Build CORS headers for a given request origin.
*
* @return array<string,string>
* @return array<string, int|string>
*/
public function headers(string $origin): array
{
+1 -1
View File
@@ -18,7 +18,7 @@ class OpenSSL
*
* @return string
*/
public static function encrypt($data, $method, $key, $options = 0, $iv = '', &$tag = null, $aad = '', $tag_length = 16)
public static function encrypt($data, $method, $key, $options = 0, $iv = '', ?string &$tag = null, $aad = '', $tag_length = 16)
{
return \openssl_encrypt($data, $method, $key, $options, $iv, $tag, $aad, $tag_length);
}
@@ -49,7 +49,6 @@ class Action extends PlatformAction
$image = new Image(\file_get_contents($path));
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
@@ -204,7 +204,6 @@ class Get extends Action
$image = new Image($data);
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
@@ -95,7 +95,6 @@ class Get extends Action
}
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$response
@@ -90,7 +90,7 @@ class Get extends Action
}
}
$rand = \substr($code, -1);
$rand = (int) \substr((string) $code, -1);
$rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand;
@@ -17,7 +17,7 @@ class Action extends AppwriteAction
return $this->context;
}
public function setHttpPath(string $path): AppwriteAction
public function setHttpPath(string $path): self
{
if (\str_contains($path, '/tablesdb')) {
$this->context = DATABASE_TYPE_TABLESDB;
@@ -28,7 +28,8 @@ class Action extends AppwriteAction
if (\str_contains($path, '/vectorsdb')) {
$this->context = DATABASE_TYPE_VECTORSDB;
}
return parent::setHttpPath($path);
parent::setHttpPath($path);
return $this;
}
/**
@@ -24,7 +24,7 @@ abstract class Action extends DatabasesAction
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): DatabasesAction
public function setHttpPath(string $path): self
{
if (str_contains($path, '/tablesdb/')) {
$this->context = ROWS;
@@ -47,7 +47,8 @@ abstract class Action extends DatabasesAction
],
];
return parent::setHttpPath($path);
parent::setHttpPath($path);
return $this;
}
protected function getDatabasesOperationReadMetric(): string
@@ -406,8 +407,6 @@ abstract class Action extends DatabasesAction
if (\is_array($related)) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} elseif (empty($relations)) {
$document->setAttribute($relationship->getAttribute('key'), null);
}
}
@@ -209,7 +209,7 @@ class Create extends Action
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() . ' with relationship ' . $this->getStructureContext());
}
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) {
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $authorization) {
$allowedPermissions = [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
@@ -122,7 +122,6 @@ class Update extends Action
$dbForDatabases = $getDatabasesDB($database);
// Read permission should not be required for update
/** @var Document $document */
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
if ($transactionId !== null) {
@@ -147,7 +147,7 @@ class XList extends Action
$cacheKeyBase = \sprintf(
'%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s',
$dbForProject->getCacheName(),
$hostname ?? '',
$hostname,
$dbForProject->getNamespace(),
$dbForProject->getTenant(),
$collectionId,
@@ -99,8 +99,6 @@ class Update extends Action
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
$enabled ??= $collection->getAttribute('enabled', true);
$collection = $dbForProject->updateDocument(
'database_' . $database->getSequence(),
$collectionId,
@@ -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)) {
@@ -103,6 +103,9 @@ class XList extends Action
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
$output[$i] = new Document([
'event' => $log['event'],
@@ -121,9 +124,9 @@ class XList extends Action
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel'],
'deviceName' => $deviceName,
'deviceBrand' => $deviceBrand,
'deviceModel' => $deviceModel,
]);
$record = $geodb->get($log['ip']);
@@ -33,7 +33,7 @@ abstract class Action extends DatabasesAction
return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_WRITES;
}
public function setHttpPath(string $path): DatabasesAction
public function setHttpPath(string $path): self
{
switch (true) {
case str_contains($path, '/tablesdb'):
@@ -50,7 +50,8 @@ abstract class Action extends DatabasesAction
$this->databaseType = VECTORSDB;
break;
}
return parent::setHttpPath($path);
parent::setHttpPath($path);
return $this;
}
/**
@@ -239,7 +239,7 @@ class Create extends Action
}
}
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) {
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $operations) {
$dbForProject->createDocuments('transactionLogs', $staged);
return $dbForProject->increaseDocumentAttribute(
'transactions',
@@ -105,8 +105,7 @@ class Update extends Action
* @throws Exception
* @throws \Throwable
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Structure
* @throws StructureException
* @throws \Utopia\Http\Exception
*/
public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
@@ -183,7 +182,7 @@ class Update extends Action
$dbForDatabases = $getDatabasesDB($databaseDoc);
try {
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'committing',
])));
@@ -97,6 +97,9 @@ class XList extends Action
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
$output[$i] = new Document([
'event' => $log['event'],
@@ -115,9 +118,9 @@ class XList extends Action
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel'],
'deviceName' => $deviceName,
'deviceBrand' => $deviceBrand,
'deviceModel' => $deviceModel,
]);
$record = $geodb->get($log['ip']);
@@ -130,9 +130,11 @@ class Create extends CollectionAction
$indexes[] = new Document($index);
}
try {
// passing null in creates only creates the metadata collection
if (!$dbForDatabases->exists(null, Database::METADATA)) {
$dbForDatabases->create();
try {
$dbForDatabases->create();
} catch (DuplicateException) {
}
}
$dbForDatabases->createCollection(
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
@@ -88,16 +88,11 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
switch ($type) {
case 'output':
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForFunctions;
break;
}
[$path, $device] = match ($type) {
'output' => [$deployment->getAttribute('buildPath', ''), $deviceForBuilds],
'source' => [$deployment->getAttribute('sourcePath', ''), $deviceForFunctions],
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid deployment download type.'),
};
if (!$device->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
@@ -213,7 +213,10 @@ class Create extends Base
$current = new Document();
foreach ($sessions as $session) {
/** @var Utopia\Database\Document $session */
if (!$session instanceof Document) {
continue;
}
if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { // Find most recent active session for user ID and JWT headers
$current = $session;
}
@@ -237,11 +240,11 @@ class Create extends Base
]);
$executionId = ID::unique();
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
$headers['x-appwrite-user-id'] = $user->getId();
$headers['x-appwrite-user-jwt'] = $jwt;
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
@@ -350,16 +353,18 @@ class Create extends Base
}
}
$this->enqueueDeletes(
$project,
$function->getSequence(),
$executionsRetentionCount,
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$queueForDeletes
);
->setProject($project)
->setResource($function->getSequence())
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
return $response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($execution, Response::MODEL_EXECUTION);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($execution, Response::MODEL_EXECUTION);
return;
}
$durationStart = \microtime(true);
@@ -370,7 +375,7 @@ class Create extends Base
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
]);
@@ -537,32 +542,18 @@ class Create extends Base
}
}
$this->enqueueDeletes(
$project,
$function->getSequence(),
$executionsRetentionCount,
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$queueForDeletes
);
->setProject($project)
->setResource($function->getSequence())
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($execution, Response::MODEL_EXECUTION);
}
private function enqueueDeletes(
Document $project,
string $resourceId,
int $executionsRetentionCount,
DeleteEvent $queueForDeletes
): void {
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
$queueForDeletes
->setProject($project)
->setResource($resourceId)
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
->trigger();
}
}
}
@@ -424,8 +424,8 @@ class Create extends Base
/** Trigger Realtime Events */
$queueForRealtime
->from($ruleCreate)
->setSubscribers(['console', $project->getId()])
->from($ruleCreate)
->trigger();
}
}
@@ -170,8 +170,6 @@ class Update extends Base
$runtime = $function->getAttribute('runtime');
}
$enabled ??= $function->getAttribute('enabled', true);
$repositoryId = $function->getAttribute('repositoryId', '');
$repositoryInternalId = $function->getAttribute('repositoryInternalId', '');
@@ -450,7 +450,7 @@ class Builds extends Action
$providerCommitHash = \trim($stdout);
$deployment->setAttribute('providerCommitHash', $providerCommitHash ?? '');
$deployment->setAttribute('providerCommitHash', $providerCommitHash);
$deployment->setAttribute('providerCommitAuthorUrl', APP_VCS_GITHUB_URL);
$deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME);
$deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function");
@@ -862,7 +862,7 @@ class Builds extends Action
if (\str_contains($logs, '{APPWRITE_DETECTION_SEPARATOR_START}')) {
[$logsBefore, $detectionLogsStart] = \explode('{APPWRITE_DETECTION_SEPARATOR_START}', $logs, 2);
[$detectionLogs, $logsAfter] = \explode('{APPWRITE_DETECTION_SEPARATOR_END}', $detectionLogsStart, 2);
$logs = ($logsBefore ?? '') . ($logsAfter ?? '');
$logs = $logsBefore . $logsAfter;
}
$deployment->setAttribute('buildLogs', $logs);
@@ -1203,6 +1203,8 @@ class Builds extends Action
protected function sendUsage(Document $resource, Document $deployment, Document $project, Context $usage, UsagePublisher $publisherForUsage): void
{
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$cpus = (int) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT);
$memory = (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT);
switch ($deployment->getAttribute('status')) {
case 'ready':
@@ -1364,6 +1366,8 @@ class Builds extends Action
Realtime $queueForRealtime,
array $platform
): void {
$deployment = new Document();
try {
if ($resource->getAttribute('providerSilentMode', false) === true) {
return;
@@ -1444,7 +1448,7 @@ class Builds extends Action
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$previewUrl = match ($resource->getCollection()) {
'functions' => '',
'sites' => ! empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
'sites' => !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
default => throw new \Exception('Invalid resource type')
};
@@ -83,7 +83,8 @@ class Update extends Action
// If rule is already verified or in certificate generation state, don't queue for verification again
if ($rule->getAttribute('status') === RULE_STATUS_VERIFIED || $rule->getAttribute('status') === RULE_STATUS_CERTIFICATE_GENERATING) {
return $response->dynamic($rule, Response::MODEL_PROXY_RULE);
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
return;
}
try {
@@ -87,16 +87,11 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
switch ($type) {
case 'output':
$path = $deployment->getAttribute('buildPath', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('sourcePath', '');
$device = $deviceForSites;
break;
}
[$path, $device] = match ($type) {
'output' => [$deployment->getAttribute('buildPath', ''), $deviceForBuilds],
'source' => [$deployment->getAttribute('sourcePath', ''), $deviceForSites],
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid deployment download type.'),
};
if (!$device->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
@@ -172,8 +172,6 @@ class Update extends Base
$framework = $site->getAttribute('framework');
}
$enabled ??= $site->getAttribute('enabled', true);
$repositoryId = $site->getAttribute('repositoryId', '');
$repositoryInternalId = $site->getAttribute('repositoryInternalId', '');
@@ -286,6 +286,8 @@ class Create extends Action
$mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption
$fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption
$data = '';
$iv = '';
$tag = null;
// Compression
$algorithm = $bucket->getAttribute('compression', Compression::NONE);
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) {
@@ -99,12 +99,8 @@ class Update extends Action
$permissions ??= $bucket->getPermissions();
$maximumFileSize ??= $bucket->getAttribute('maximumFileSize', (int) System::getEnv('_APP_STORAGE_LIMIT', 0));
$allowedFileExtensions ??= $bucket->getAttribute('allowedFileExtensions', []);
$enabled ??= $bucket->getAttribute('enabled', true);
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
$transformations ??= $bucket->getAttribute('transformations', true);
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
@@ -17,6 +17,7 @@ use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;
use Throwable;
use Utopia\Auth\Proofs\Password;
use Utopia\Auth\Proofs\Token;
use Utopia\Database\Database;
@@ -102,6 +103,8 @@ class Create extends Action
{
$isAppUser = $user->isApp($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$invitee = new Document();
$hash = '';
if (empty($url)) {
if (! $isAppUser && ! $isPrivilegedUser) {
@@ -145,9 +148,6 @@ class Create extends Action
}
} elseif (! empty($phone)) {
$invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
if (! $invitee->isEmpty() && ! empty($email) && $invitee->getAttribute('email', '') !== $email) {
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409);
}
}
if ($invitee->isEmpty()) { // Create new user if no user with same email found
@@ -110,10 +110,7 @@ class Update extends Action
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($providerRepositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -65,6 +65,7 @@ class Get extends Action
}
$state = \json_decode($state, true);
$redirectFailure = $state['failure'] ?? '';
$projectId = $state['projectId'] ?? '';
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -74,10 +75,11 @@ class Get extends Action
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
return;
}
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
@@ -165,10 +167,11 @@ class Get extends Action
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
return;
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
@@ -49,6 +49,8 @@ trait Deployment
) {
$errors = [];
foreach ($repositories as $repository) {
$logBase = 'vcs.github.event.repo.unknown';
try {
$repositoryId = $repository->getId();
$projectId = $repository->getAttribute('projectId');
@@ -70,6 +72,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();
@@ -105,18 +109,11 @@ trait Deployment
$owner = $github->getOwnerName($providerInstallationId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$repositoryName = $github->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$isAuthorized = !$external;
if (!$isAuthorized && !empty($providerPullRequestId)) {
@@ -289,10 +286,7 @@ trait Deployment
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$repositoryName = $github->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -499,7 +493,7 @@ trait Deployment
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
$previewUrl = !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
if (!empty($previewUrl)) {
$comment = new Comment($platform);
@@ -522,10 +516,7 @@ trait Deployment
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$repositoryName = $github->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -561,6 +552,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);
@@ -83,7 +83,7 @@ class Create extends Action
default => null,
};
return $response->json($parsedPayload);
$response->json($parsedPayload);
}
protected function preprocessEvent(Request $request)
+13 -9
View File
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\ClamAV\Network;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Config\Config;
use Utopia\Console;
@@ -13,6 +12,8 @@ use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Logger\Logger;
use Utopia\Messaging\Adapter\Email as EmailAdapter;
use Utopia\Messaging\Messages\Email as EmailMessage;
use Utopia\Platform\Action;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
@@ -124,7 +125,7 @@ class Doctor extends Action
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG', '');
try {
$loggingProvider = new DSN($providerConfig ?? '');
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
@@ -212,15 +213,18 @@ class Doctor extends Action
}
try {
/* @var PHPMailer $mail */
$mail = $register->get('smtp');
/** @var EmailAdapter $smtp */
$smtp = $register->get('smtp');
$mail->addAddress('demo@example.com', 'Example.com');
$mail->Subject = 'Test SMTP Connection';
$mail->Body = 'Hello World';
$mail->AltBody = 'Hello World';
$emailMessage = new EmailMessage(
to: ['demo@example.com'],
subject: 'Test SMTP Connection',
content: 'Hello World',
fromName: \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')),
fromEmail: System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
);
$mail->send();
$smtp->send($emailMessage);
Console::success('🟢 ' . str_pad("SMTP", 50, '.') . 'connected');
} catch (\Throwable) {
Console::error('🔴 ' . str_pad("SMTP", 47, '.') . 'disconnected');
+2 -2
View File
@@ -227,7 +227,7 @@ class Install extends Action
// Fall back to CLI mode
$enableAssistant = false;
$assistantExistsInOldCompose = false;
if ($existingInstallation && isset($compose)) {
if ($existingInstallation) {
try {
$assistantService = $compose->getService('appwrite-assistant');
$assistantExistsInOldCompose = $assistantService !== null;
@@ -1343,7 +1343,7 @@ class Install extends Action
{
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $arg) {
if (\str_starts_with($arg, '--') && !\str_starts_with($arg, '--interactive')) {
if (\str_starts_with($arg, '--')) {
return true;
}
}
+2
View File
@@ -566,6 +566,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$repoBranch = $language['repoBranch'] ?? 'main';
if ($git && !empty($gitUrl)) {
$prUrls = [];
// Generate commit message: use provided message, AI changelog, or fallback
if (! empty($message)) {
$commitMessage = $message;
@@ -60,7 +60,7 @@ class StatsResources extends Action
$interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600');
Console::loop(function () use ($queueForStatsResources, $dbForPlatform) {
Console::loop(function () use ($queueForStatsResources) {
$last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours'));
/**
+2 -2
View File
@@ -30,7 +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', true, new Boolean(true), 'Run database migration after upgrade', true)
->param('migrate', false, new Boolean(true), 'Run database migration after upgrade', true)
->callback($this->action(...));
}
@@ -42,7 +42,7 @@ class Upgrade extends Install
string $interactive,
bool $noStart,
string $database,
bool $migrate = true,
bool $migrate = false,
): void {
$this->isUpgrade = true;
$this->migrate = $migrate;
+1 -4
View File
@@ -6,7 +6,6 @@ use Exception;
use Throwable;
use Utopia\Console;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
@@ -51,13 +50,11 @@ class Audits extends Action
/**
* @param Message $message
* @param callable $getProjectDB
* @param Document $project
* @param callable $getAudit
* @param callable(Document): \Utopia\Audit\Audit $getAudit
* @return Commit|NoCommit
* @throws Throwable
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Structure
*/
public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit
+8 -14
View File
@@ -24,7 +24,6 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
@@ -363,7 +362,6 @@ class Deletes extends Action
/**
* @param Document $project
* @param callable $getProjectDB
* @param Document $target
* @return void
* @throws Exception
*/
@@ -438,7 +436,6 @@ class Deletes extends Action
* @param string $resource
* @param string|null $resourceType
* @return void
* @throws Authorization
* @throws Exception
*/
private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource, ?string $resourceType = null): void
@@ -518,7 +515,6 @@ class Deletes extends Action
}
/**
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $hourlyUsageRetentionDatetime
* @return void
@@ -586,7 +582,6 @@ class Deletes extends Action
* @param Database $dbForPlatform
* @param Document $document
* @return void
* @throws Authorization
* @throws DatabaseException
* @throws Conflict
* @throws Restricted
@@ -623,7 +618,6 @@ class Deletes extends Action
* @param Document $document
* @return void
* @throws Exception
* @throws Authorization
* @throws DatabaseException
*/
protected function deleteProject(Database $dbForPlatform, callable $getProjectDB, callable $getDatabasesDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Document $document): void
@@ -952,7 +946,7 @@ class Deletes extends Action
// fast path, no need to list anything!
$delete($dbForProject, $resourceInternalId, $resourceType);
} else {
$processResource = function (string $type) use ($dbForProject, $delete, $resourceType) {
$processResource = function (string $type) use ($dbForProject, $delete) {
$this->listByGroup(
collection: $type,
queries: [Query::select(['$id', '$sequence'])],
@@ -1109,7 +1103,7 @@ class Deletes extends Action
Query::equal('resourceInternalId', [$siteInternalId]),
Query::equal('resourceType', ['sites']),
Query::orderAsc()
], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
], $dbForProject, function (Document $document) use ($deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds, &$deploymentIds) {
$deploymentInternalIds[] = $document->getSequence();
$deploymentIds[] = $document->getId();
$this->deleteBuildFiles($deviceForBuilds, $document);
@@ -1172,7 +1166,7 @@ class Deletes extends Action
Query::equal('deploymentResourceInternalId', [$functionInternalId]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::orderAsc()
], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) {
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
});
@@ -1196,7 +1190,7 @@ class Deletes extends Action
Query::equal('resourceInternalId', [$functionInternalId]),
Query::equal('resourceType', ['functions']),
Query::orderAsc()
], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
$deploymentInternalIds[] = $document->getSequence();
$this->deleteDeploymentFiles($deviceForFunctions, $document);
$this->deleteBuildFiles($deviceForBuilds, $document);
@@ -1321,7 +1315,7 @@ class Deletes extends Action
/**
* @param Device $device
* @param Document $build
* @param Document $deployment
* @return void
*/
private function deleteBuildFiles(Device $device, Document $deployment): void
@@ -1631,9 +1625,9 @@ class Deletes extends Action
try {
$dbForProject->deleteDocuments('transactions', [
Query::lessThan('expiresAt', DateTime::format(new \DateTime())),
], onNext: function (Document $transaction) use ($dbForProject, $project, &$transactionInternalIds) {
], onNext: function (Document $transaction) use (&$transactionInternalIds) {
$transactionInternalIds[] = $transaction->getSequence();
}, onError: function (Throwable $th) use ($project) {
}, onError: function (Throwable $th) {
// Swallow errors to avoid breaking the cleanup process
});
} catch (Throwable $th) {
@@ -1646,7 +1640,7 @@ class Deletes extends Action
$dbForProject->deleteDocuments('transactionLogs', [
Query::equal('transactionInternalId', $transactionInternalIds),
], onError: function (Throwable $th) use ($project) {
], onError: function (Throwable $th) {
// Swallow errors to avoid breaking the cleanup process
});
}
+9 -6
View File
@@ -33,7 +33,7 @@ class Functions extends Action
}
/**
* @throws Exception
* @throws \Exception
*/
public function __construct()
{
@@ -256,7 +256,7 @@ class Functions extends Action
* @param Document $user
* @param string|null $jwt
* @param string|null $event
* @throws Exception
* @throws \Exception
*/
private function fail(
string $message,
@@ -271,10 +271,10 @@ class Functions extends Action
?string $event = null,
): void {
$executionId = ID::unique();
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-trigger'] = $trigger;
$headers['x-appwrite-event'] = $event ?? '';
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-id'] = $user->getId();
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
$headersFiltered = [];
@@ -458,8 +458,8 @@ class Functions extends Action
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' => $body ?? '',
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_EVENT_DATA' => $body,
'APPWRITE_FUNCTION_EVENT' => $headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
@@ -508,6 +508,9 @@ class Functions extends Action
]);
/** Execute function */
$error = null;
$errorCode = 0;
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];
+53 -68
View File
@@ -4,10 +4,13 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Template\Template;
use Exception;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Runtime;
use Utopia\Database\Document;
use Utopia\Logger\Log;
use Utopia\Messaging\Adapter\Email as EmailAdapter;
use Utopia\Messaging\Adapter\Email\SMTP;
use Utopia\Messaging\Messages\Email as EmailMessage;
use Utopia\Messaging\Messages\Email\Attachment;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Registry\Registry;
@@ -49,9 +52,9 @@ class Mails extends Action
/**
* @param Message $message
* @param Document $project
* @param Registry $register
* @param Log $log
* @throws \PHPMailer\PHPMailer\Exception
* @return void
* @throws Exception
*/
@@ -132,36 +135,38 @@ class Mails extends Action
// render() will return the subject in <p> tags, so use strip_tags() to remove them
$subject = \strip_tags($subjectTemplate->render());
/** @var PHPMailer $mail */
$mail = empty($smtp)
/** @var EmailAdapter $adapter */
$adapter = empty($smtp)
? $register->get('smtp')
: $this->getMailer($smtp);
: new SMTP(
host: $smtp['host'],
port: (int) $smtp['port'],
username: $smtp['username'] ?? '',
password: $smtp['password'] ?? '',
smtpSecure: $smtp['secure'] ?? '',
smtpAutoTLS: false,
xMailer: 'Appwrite Mailer',
timeout: 10,
keepAlive: true,
timelimit: 30,
);
$mail->clearAddresses();
$mail->clearAllRecipients();
$mail->clearReplyTos();
$mail->clearAttachments();
$mail->clearBCCs();
$mail->clearCCs();
$mail->addAddress($recipient, $name);
$mail->Subject = $subject;
$mail->Body = $body;
// Resolve from/replyTo using fallback hierarchy: Custom options > SMTP config > Defaults
$defaultFromEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$defaultFromName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$mail->AltBody = $body;
$mail->AltBody = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $mail->AltBody);
$mail->AltBody = \strip_tags($mail->AltBody);
$mail->AltBody = \trim($mail->AltBody);
$replyTo = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$replyToName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$fromEmail = !empty($smtp) ? ($smtp['senderEmail'] ?? $defaultFromEmail) : $defaultFromEmail;
$fromName = !empty($smtp) ? ($smtp['senderName'] ?? $defaultFromName) : $defaultFromName;
$replyTo = $defaultFromEmail;
$replyToName = $defaultFromName;
$customMailOptions = $payload['customMailOptions'] ?? [];
// fallback hierarchy: Custom options > SMTP config > Defaults.
if (!empty($customMailOptions['senderEmail']) || !empty($customMailOptions['senderName'])) {
$fromEmail = $customMailOptions['senderEmail'] ?? $mail->From;
$fromName = $customMailOptions['senderName'] ?? $mail->FromName;
$mail->setFrom($fromEmail, $fromName);
if (!empty($customMailOptions['senderEmail'])) {
$fromEmail = $customMailOptions['senderEmail'];
}
if (!empty($customMailOptions['senderName'])) {
$fromName = $customMailOptions['senderName'];
}
if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) {
@@ -172,18 +177,32 @@ class Mails extends Action
$replyToName = $smtp['senderName'] ?? $replyToName;
}
$mail->addReplyTo($replyTo, $replyToName);
$attachments = null;
if (!empty($attachment['content'] ?? '')) {
$mail->AddStringAttachment(
base64_decode($attachment['content']),
$attachment['filename'] ?? 'unknown.file',
$attachment['encoding'] ?? PHPMailer::ENCODING_BASE64,
$attachment['type'] ?? 'plain/text'
);
$attachments = [
new Attachment(
name: $attachment['filename'] ?? 'unknown.file',
path: '',
type: $attachment['type'] ?? 'plain/text',
content: \base64_decode($attachment['content']),
),
];
}
$emailMessage = new EmailMessage(
to: [['email' => $recipient, 'name' => $name]],
subject: $subject,
content: $body,
fromName: $fromName,
fromEmail: $fromEmail,
replyToName: $replyToName,
replyToEmail: $replyTo,
attachments: $attachments,
html: true,
);
try {
$mail->send();
$adapter->send($emailMessage);
} catch (\Throwable $error) {
if ($type === 'smtp') {
throw new Exception('Error sending mail: ' . $error->getMessage(), 401);
@@ -191,38 +210,4 @@ class Mails extends Action
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
}
}
/**
* @param array $smtp
* @return PHPMailer
* @throws \PHPMailer\PHPMailer\Exception
*/
protected function getMailer(array $smtp): PHPMailer
{
$mail = new PHPMailer(true);
$mail->isSMTP();
$username = $smtp['username'];
$password = $smtp['password'];
$mail->XMailer = 'Appwrite Mailer';
$mail->Host = $smtp['host'];
$mail->Port = $smtp['port'];
$mail->SMTPAuth = (!empty($username) && !empty($password));
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = $smtp['secure'];
$mail->SMTPAutoTLS = false;
$mail->SMTPKeepAlive = true;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
$mail->setFrom($smtp['senderEmail'], $smtp['senderName']);
$mail->isHTML();
return $mail;
}
}
+2 -2
View File
@@ -285,7 +285,7 @@ class Messaging extends Action
try {
$response = $adapter->send($data);
$deliveredTotal += $response['deliveredTo'];
$deliveredTotal += (int) $response['deliveredTo'];
foreach ($response['results'] as $result) {
if ($result['status'] === 'failure') {
$deliveryErrors[] = "Failed sending to target {$result['recipient']} with error: {$result['error']}";
@@ -380,7 +380,7 @@ class Messaging extends Action
]));
// Delete any attachments that were downloaded to local storage
if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) {
if ($providerType === MESSAGE_TYPE_EMAIL) {
if ($deviceForFiles->getType() === Storage::DEVICE_LOCAL) {
return;
}
+41 -19
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'),
};
}
@@ -393,6 +408,7 @@ class Migrations extends Action
$tempAPIKey = $this->generateAPIKey($project);
$transfer = $source = $destination = null;
$aggregatedResources = [];
$host = System::getEnv('_APP_MIGRATION_HOST');
if (empty($host)) {
@@ -429,7 +445,6 @@ class Migrations extends Action
$destination
);
$aggregatedResources = [];
/** Start Transfer */
if (empty($source->getErrors())) {
$migration->setAttribute('stage', 'migrating');
@@ -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
@@ -208,7 +208,7 @@ class StatsResources extends Action
{
$totalFiles = 0;
$totalStorage = 0;
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalFiles, &$totalStorage) {
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $region, &$totalFiles, &$totalStorage) {
try {
$files = $dbForProject->count('bucket_' . $bucket->getSequence());
} catch (Throwable $th) {
+4 -4
View File
@@ -140,7 +140,7 @@ class StatsUsage extends Action
/**
* @param Message $message
* @param callable(): Database $getProjectDB
* @param callable(Document): Database $getProjectDB
* @param callable(): Database $getLogsDB
* @param Registry $register
* @return void
@@ -212,7 +212,7 @@ class StatsUsage extends Action
* @param Document $project
* @param Document $document
* @param array $metrics
* @param callable(): Database $getProjectDB
* @param callable(Document): Database $getProjectDB
* @param string $databaseType Database type from context
* @return void
*/
@@ -394,7 +394,7 @@ class StatsUsage extends Action
/**
* Commit stats to DB
* @param callable(): Database $getProjectDB
* @param callable(Document): Database $getProjectDB
* @return void
*/
public function commitToDb(callable $getProjectDB): void
@@ -459,7 +459,7 @@ class StatsUsage extends Action
/**
* Sort by unique index key reduce locks/deadlocks
*/
usort($projectStats['stats'], function ($a, $b) use ($sequence) {
usort($projectStats['stats'], function ($a, $b) {
// Metric DESC
$cmp = strcmp($b['metric'], $a['metric']);
if ($cmp !== 0) {
+1 -1
View File
@@ -233,7 +233,7 @@ class Webhooks extends Action
$template->setParam('{{webhook}}', $webhook->getAttribute('name'));
$template->setParam('{{project}}', $project->getAttribute('name'));
$template->setParam('{{url}}', $webhook->getAttribute('url'));
$template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code');
$template->setParam('{{error}}', 'The server returned ' . $statusCode . ' status code');
$template->setParam('{{path}}', "/console/project-$region-$projectId/settings/webhooks/$webhookId");
$template->setParam('{{attempts}}', $attempts);
+1
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Promises;
/** @phpstan-consistent-constructor */
abstract class Promise
{
protected const STATE_PENDING = 1;
+1 -1
View File
@@ -197,7 +197,7 @@ class Method
public function isHidden(): bool|array
{
return $this->hide ?? false;
return $this->hide;
}
public function isPackaging(): bool
@@ -204,9 +204,6 @@ abstract class Format
*
* Get services value
*
* @param array $services
*
* @return self
*/
public function getServices(): array
{
@@ -79,8 +79,8 @@ class OpenAPI3 extends Format
$output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '<YOUR_API_KEY>'];
}
if (isset($output['securityDefinitions']['JWT'])) {
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
if (isset($output['components']['securitySchemes']['JWT'])) {
$output['components']['securitySchemes']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
}
if (isset($output['components']['securitySchemes']['Locale'])) {
@@ -99,7 +99,7 @@ class OpenAPI3 extends Format
$sdk = $route->getLabel('sdk', false);
if (empty($sdk)) {
if ($sdk === false) {
continue;
}
@@ -125,7 +125,9 @@ class OpenAPI3 extends Format
$namespace = $sdk->getNamespace() ?? 'default';
$desc ??= '';
if ($desc === null) {
$desc = '';
}
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
$temp = [
@@ -163,7 +165,7 @@ class OpenAPI3 extends Format
];
}
if (!empty($additionalMethods)) {
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
$temp['x-appwrite']['methods'] = [];
foreach ($additionalMethods as $methodObj) {
/** @var Method $methodObj */
@@ -329,7 +331,7 @@ class OpenAPI3 extends Format
if (($response->getCode() ?? 500) === 204) {
$temp['responses'][(string)$response->getCode() ?? '500']['description'] = 'No content';
unset($temp['responses'][(string)$response->getCode() ?? '500']['schema']);
unset($temp['responses'][(string)$response->getCode() ?? '500']['content']);
}
}
@@ -383,7 +385,7 @@ class OpenAPI3 extends Format
$validator = $validator->getValidator();
}
$class = !empty($validator)
$class = $validator instanceof Validator
? \get_class($validator)
: '';
@@ -431,7 +433,7 @@ class OpenAPI3 extends Format
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case \Utopia\Database\Validator\DatetimeValidator::class:
case \Utopia\Database\Validator\Datetime::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'datetime';
$node['schema']['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
@@ -463,7 +465,6 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = ($param['example'] ?? '') ?: 'https://example.com';
break;
case \Utopia\Validator\JSON::class:
case \Utopia\Validator\Mock::class:
case \Utopia\Validator\Assoc::class:
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['type'] = 'object';
@@ -563,12 +564,6 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\Length::class:
$node['schema']['type'] = $validator->getType();
if (!empty($param['example'])) {
$node['schema']['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\WhiteList::class:
if ($array) {
$validator = $validator->getValidator();
@@ -96,10 +96,9 @@ class Swagger2 extends Format
$scope = $route->getLabel('scope', '');
/** @var Method $sdk */
$sdk = $route->getLabel('sdk', false);
if (empty($sdk)) {
if ($sdk === false) {
continue;
}
@@ -127,7 +126,9 @@ class Swagger2 extends Format
$sdkPlatforms = array_values(array_unique($sdkPlatforms));
$namespace = $sdk->getNamespace() ?? 'default';
$desc ??= '';
if ($desc === null) {
$desc = '';
}
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
$temp = [
@@ -171,7 +172,7 @@ class Swagger2 extends Format
$temp['produces'][] = $produces;
}
if (!empty($additionalMethods)) {
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
$temp['x-appwrite']['methods'] = [];
foreach ($additionalMethods as $methodObj) {
/** @var Method $methodObj */
@@ -388,7 +389,7 @@ class Swagger2 extends Format
$validator = $validator->getValidator();
}
$class = !empty($validator)
$class = $validator instanceof Validator
? \get_class($validator)
: '';
@@ -436,7 +437,7 @@ class Swagger2 extends Format
$node['type'] = $validator->getType();
$node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case \Utopia\Database\Validator\DatetimeValidator::class:
case \Utopia\Database\Validator\Datetime::class:
$node['type'] = $validator->getType();
$node['format'] = 'datetime';
$node['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
@@ -479,7 +480,6 @@ class Swagger2 extends Format
}
break;
case \Utopia\Validator\JSON::class:
case \Utopia\Validator\Mock::class:
case \Utopia\Validator\Assoc::class:
$node['type'] = 'object';
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
@@ -552,12 +552,6 @@ class Swagger2 extends Format
$node['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\Length::class:
$node['type'] = $validator->getType();
if (!empty($param['example'])) {
$node['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\WhiteList::class:
if ($array) {
$validator = $validator->getValidator();
@@ -86,7 +86,6 @@ class User extends Document
/**
* Check if user is anonymous.
*
* @param Document $this
* @return bool
*/
public function isAnonymous(): bool
@@ -153,7 +152,6 @@ class User extends Document
/**
* Verify session and check that its not expired.
*
* @param array<Document> $sessions
* @param string $secret
*
* @return bool|string
+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);
@@ -29,8 +29,8 @@ class DatabasesStringTypesTest extends Scope
protected function setupDatabaseAndCollection(): array
{
$cacheKey = $this->getProject()['$id'] ?? 'default';
if (!empty(static::$setupCache[$cacheKey])) {
return static::$setupCache[$cacheKey];
if (!empty(self::$setupCache[$cacheKey])) {
return self::$setupCache[$cacheKey];
}
$projectId = $this->getProject()['$id'];
@@ -135,12 +135,12 @@ class DatabasesStringTypesTest extends Scope
// Wait for all attributes to be available
$this->waitForAllAttributes($databaseId, $collectionId);
static::$setupCache[$cacheKey] = [
self::$setupCache[$cacheKey] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
];
return static::$setupCache[$cacheKey];
return self::$setupCache[$cacheKey];
}
public function testCreateDatabase(): void

Some files were not shown because too many files have changed in this diff Show More