Compare commits

..
Author SHA1 Message Date
loks0n 5d15632bf8 dev: split development compose 2026-02-10 14:56:36 +00:00
Matej BačoandGitHub 51f036491a Merge pull request #11278 from appwrite/feat-list-api-keys-queries
Feat: queries param on list api keys
2026-02-10 12:47:00 +01:00
Matej Bačo 4df0934023 Comment fix 2026-02-10 12:01:38 +01:00
Matej Bačo e2071bd5dd add backwards compatibility 2026-02-10 11:48:29 +01:00
Matej Bačo 0621a32aa6 Fix tests; speed up tests; fix 5xx error 2026-02-10 11:43:22 +01:00
Matej Bačo e1cbbe3943 Merge branch '1.8.x' into feat-list-api-keys-queries 2026-02-10 10:46:56 +01:00
Matej Bačo a87263a571 Fix failing test 2026-02-09 20:07:33 +01:00
Matej Bačo dafa97879c AI review fixes 2026-02-09 17:12:30 +01:00
Matej Bačo 29915ddd3b Manual fixes of agent code 2026-02-09 16:58:44 +01:00
Matej Bačo 6df5556473 Implement queries param to list api keys 2026-02-09 14:58:36 +01:00
24 changed files with 794 additions and 354 deletions
+4 -6
View File
@@ -71,12 +71,10 @@ jobs:
run: docker compose down -v
- name: Installing latest version
run: |
rm docker-compose.yml
rm .env
curl https://appwrite.io/install/compose -o docker-compose.yml
curl https://appwrite.io/install/env -o .env
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
docker compose up -d
curl https://appwrite.io/install/compose -o docker-compose.latest.yml
curl https://appwrite.io/install/env -o .env.latest
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env.latest
docker compose -f docker-compose.latest.yml --env-file .env.latest up -d
sleep 10
- name: Benchmark Latest
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
+5
View File
@@ -1139,6 +1139,11 @@ return [
'description' => 'Key with the requested ID could not be found.',
'code' => 404,
],
Exception::KEY_ALREADY_EXISTS => [
'name' => Exception::KEY_ALREADY_EXISTS,
'description' => 'Key with the same ID already exists. Try again with a different ID.',
'code' => 409,
],
Exception::PLATFORM_NOT_FOUND => [
'name' => Exception::PLATFORM_NOT_FOUND,
'description' => 'Platform with the requested ID could not be found.',
+48 -8
View File
@@ -15,16 +15,20 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Keys;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Validator\PublicDomain;
use Utopia\Http;
@@ -1129,7 +1133,11 @@ Http::post('/v1/projects/:projectId/keys')
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
]);
$key = $dbForPlatform->createDocument('keys', $key);
try {
$key = $dbForPlatform->createDocument('keys', $key);
} catch (Duplicate) {
throw new Exception(Exception::KEY_ALREADY_EXISTS);
}
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
@@ -1156,10 +1164,11 @@ Http::get('/v1/projects/:projectId/keys')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('queries', [], new Keys(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Keys::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -1167,15 +1176,46 @@ Http::get('/v1/projects/:projectId/keys')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$keys = $dbForPlatform->find('keys', [
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
Query::limit(5000),
]);
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Backwards compatibility
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
$queries[] = Query::limit(5000);
}
$queries[] = Query::equal('resourceType', ['projects']);
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$keyId = $cursor->getValue();
$cursorDocument = $dbForPlatform->getDocument('keys', $keyId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$keys = $dbForPlatform->find('keys', $queries);
$response->dynamic(new Document([
'keys' => $keys,
'total' => $includeTotal ? count($keys) : 0,
'total' => $includeTotal ? $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT) : 0,
]), Response::MODEL_KEY_LIST);
});
+374
View File
@@ -0,0 +1,374 @@
# Development overrides for the Appwrite stack.
# This file is automatically loaded by `docker compose up` alongside docker-compose.yml.
# It adds source-code volume mounts, build configuration, debug ports, and dev tools.
services:
traefik:
ports:
- 8080:80
- 9500:8080
appwrite:
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
VERSION: dev
ports:
- 9501:80
dns:
- 172.16.238.100
volumes:
- /var/run/docker.sock:/var/run/docker.sock # Only needed for tests
- ./docker-compose.yml:/usr/src/code/docker-compose.yml # Only needed for tests
- ./.env:/usr/src/code/.env # Only needed for tests
- ./phpunit.xml:/usr/src/code/phpunit.xml
- ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app
- ./docs:/usr/src/code/docs
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
- ./dev:/usr/src/code/dev
# - ./vendor/utopia-php/framework:/usr/src/code/vendor/utopia-php/framework
depends_on:
- coredns
appwrite-realtime:
ports:
- 9505:80
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-audits:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-webhooks:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- request-catcher-sms
- request-catcher-webhook
appwrite-worker-deletes:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-databases:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-builds:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-screenshots:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-certificates:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-executions:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-functions:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-mails:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- maildev
appwrite-worker-messaging:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-migrations:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
appwrite-task-maintenance:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-interval:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-stats-resources:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-stats-resources:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-stats-usage:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-scheduler-functions:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-scheduler-executions:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-scheduler-messages:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
# Testing infrastructure
coredns: # DNS server for testing purposes (Proxy APIs)
image: coredns/coredns:1.12.4
container_name: appwrite-coredns
restart: unless-stopped
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
command: ["-conf", "/mnt/resources/Corefile"]
networks:
appwrite:
ipv4_address: 172.16.238.100
volumes:
- ./tests/resources/coredns:/mnt/resources:ro
# Dev Tools ------------------------------------------------------------------------------------------
#
# The Appwrite Team uses the following tools to help debug, monitor and diagnose the Appwrite stack
#
# Here is a description of the different tools and why are we using them:
#
# MailCatcher - An SMTP server. Catches all system emails and displays them in a nice UI.
# RequestCatcher - An HTTP server. Catches all system https calls and displays them using a simple HTTP API. Used to debug & tests webhooks and HTTP tasks
# Redis Insight - A nice UI for exploring Redis data
# Adminer - A nice UI for exploring MariaDB data
# GraphQl Explorer - A nice UI for exploring GraphQL API
maildev: # used mainly for dev tests
image: appwrite/mailcatcher:1.1.1
container_name: appwrite-mailcatcher
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
ports:
- "9503:1080"
networks:
- appwrite
- gateway
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_maildev.loadbalancer.server.port=1080"
- "traefik.http.routers.appwrite_maildev_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_maildev_http.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_http.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_maildev_https.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_https.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.tls=true"
environment:
- MAILDEV_INCOMING_USER=${_APP_SMTP_USERNAME}
- MAILDEV_INCOMING_PASS=${_APP_SMTP_PASSWORD}
request-catcher-webhook: # used mainly for dev tests (mock HTTP webhook)
image: appwrite/requestcatcher:1.0.0
container_name: appwrite-requestcatcher-webhook
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
ports:
- "9504:5000"
networks:
- appwrite
request-catcher-sms: # used mainly for dev tests (mock SMS auth secret)
image: appwrite/requestcatcher:1.0.0
container_name: appwrite-requestcatcher-sms
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
ports:
- "9507:5000"
networks:
- appwrite
adminer:
image: adminer
container_name: appwrite-adminer
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
restart: always
ports:
- 9506:8080
networks:
- appwrite
- gateway
environment:
- ADMINER_DESIGN=pepa-linha
- ADMINER_DEFAULT_SERVER=mariadb
- ADMINER_DEFAULT_USERNAME=root
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
- ADMINER_DEFAULT_DB=appwrite
configs:
- source: adminer-index.php
target: /var/www/html/index.php
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.tls=true"
redis-insight:
image: redis/redisinsight:latest
restart: unless-stopped
networks:
- appwrite
- gateway
environment:
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
configs:
- source: redisinsight-connections.json
target: /mnt/connections.json
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
ports:
- "8081:5540"
graphql-explorer:
container_name: appwrite-graphql-explorer
image: appwrite/altair:0.3.0
restart: unless-stopped
networks:
- appwrite
ports:
- "9509:3000"
environment:
- SERVER_URL=http://localhost/v1/graphql
mariadb:
ports:
- "3306:3306"
redis:
ports:
- "6379:6379"
# Dev Tools End ------------------------------------------------------------------------------------------
configs:
redisinsight-connections.json:
content: |
[
{
"compressor": "NONE",
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
"host": "redis",
"port": 6379,
"name": "redis:6379",
"db": 0,
"username": "default",
"password": null,
"connectionType": "STANDALONE",
"nameFromProvider": null,
"provider": "REDIS",
"lastConnection": "2025-10-16T09:22:02.591Z",
"modules": [
{
"name": "ReJSON",
"version": 20808,
"semanticVersion": "2.8.8"
},
{
"name": "search",
"version": 21015,
"semanticVersion": "2.10.15"
}
],
"tls": false,
"tlsServername": null,
"verifyServerCert": null,
"caCert": null,
"clientCert": null,
"ssh": false,
"sshOptions": null,
"forceStandalone": false,
"tags": []
}
]
adminer-index.php:
content: |
<?php
if(!count($$_GET)) {
$$_POST['auth'] = [
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'driver' => 'server', /* seems to autodetect the driver from server settings */
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
];
}
include './adminer.php';
-297
View File
@@ -1,8 +1,3 @@
# WARNING!
# This is a development version of THE Appwrite docker-compose.yml file.
# Avoid using this file in your production environment.
# We're exposing here sensitive ports and mounting code volumes for rapid development and debugging of the server stack.
x-logging: &x-logging
logging:
driver: "json-file"
@@ -28,9 +23,7 @@ services:
- --accesslog=true
ports:
- 80:80
- 8080:80
- 443:443
- 9500:8080
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- appwrite-config:/storage/config:ro
@@ -52,19 +45,8 @@ services:
container_name: appwrite
<<: *x-logging
image: appwrite-dev
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
VERSION: dev
ports:
- 9501:80
networks:
- appwrite
dns:
- 172.16.238.100
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
@@ -80,9 +62,6 @@ services:
- traefik.http.routers.appwrite_api_https.service=appwrite_api
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock # Only needed for tests
- ./docker-compose.yml:/usr/src/code/docker-compose.yml # Only needed for tests
- ./.env:/usr/src/code/.env # Only needed for tests
- appwrite-uploads:/storage/uploads:rw
- appwrite-imports:/storage/imports:rw
- appwrite-cache:/storage/cache:rw
@@ -91,19 +70,9 @@ services:
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- ./phpunit.xml:/usr/src/code/phpunit.xml
- ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app
- ./docs:/usr/src/code/docs
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
- ./dev:/usr/src/code/dev
# - ./vendor/utopia-php/framework:/usr/src/code/vendor/utopia-php/framework
depends_on:
- mariadb
- redis
- coredns
# - clamav
entrypoint:
- php
- -e
@@ -261,8 +230,6 @@ services:
container_name: appwrite-realtime
image: appwrite-dev
restart: unless-stopped
ports:
- 9505:80
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
@@ -279,9 +246,6 @@ services:
- traefik.http.routers.appwrite_realtime_wss.tls=true
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -313,9 +277,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -343,14 +304,9 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
- request-catcher-sms
- request-catcher-webhook
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -387,8 +343,6 @@ services:
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- appwrite-certificates:/storage/certificates:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -441,9 +395,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -477,8 +428,6 @@ services:
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -549,8 +498,6 @@ services:
- appwrite
volumes:
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -613,8 +560,6 @@ services:
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -648,9 +593,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
redis:
condition: service_started
@@ -679,9 +621,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
redis:
condition: service_started
@@ -726,13 +665,8 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- maildev
# - smtp
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -764,8 +698,6 @@ services:
- appwrite
volumes:
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
environment:
@@ -821,9 +753,6 @@ services:
volumes:
- appwrite-imports:/storage/imports:rw
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
depends_on:
- mariadb
environment:
@@ -861,9 +790,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -906,9 +832,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -945,9 +868,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -977,9 +897,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -1009,9 +926,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -1041,9 +955,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -1070,9 +981,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -1098,9 +1006,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -1201,8 +1106,6 @@ services:
- appwrite
volumes:
- appwrite-mariadb:/var/lib/mysql:rw
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
@@ -1220,156 +1123,11 @@ services:
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--maxmemory-samples 5
ports:
- "6379:6379"
networks:
- appwrite
volumes:
- appwrite-redis:/data:rw
coredns: # DNS server for testing purposes (Proxy APIs)
image: coredns/coredns:1.12.4
container_name: appwrite-coredns
restart: unless-stopped
<<: *x-logging
command: ["-conf", "/mnt/resources/Corefile"]
# If you need to debug CoreDNS, do it from "appwrite container", or port forward:
# ports:
# - "53:53"
networks:
appwrite:
ipv4_address: 172.16.238.100
volumes:
- ./tests/resources/coredns:/mnt/resources:ro
# Dev Tools Start ------------------------------------------------------------------------------------------
#
# The Appwrite Team uses the following tools to help debug, monitor and diagnose the Appwrite stack
#
# Here is a description of the different tools and why are we using them:
#
# MailCatcher - An SMTP server. Catches all system emails and displays them in a nice UI.
# RequestCatcher - An HTTP server. Catches all system https calls and displays them using a simple HTTP API. Used to debug & tests webhooks and HTTP tasks
# Redis Insight - A nice UI for exploring Redis data
# Adminer - A nice UI for exploring MariaDB data
# GraphQl Explorer - A nice UI for exploring GraphQL API
maildev: # used mainly for dev tests
image: appwrite/mailcatcher:1.1.1
container_name: appwrite-mailcatcher
<<: *x-logging
ports:
- "9503:1080"
networks:
- appwrite
- gateway
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_maildev.loadbalancer.server.port=1080"
- "traefik.http.routers.appwrite_maildev_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_maildev_http.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_http.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_maildev_https.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_https.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.tls=true"
environment:
- MAILDEV_INCOMING_USER=${_APP_SMTP_USERNAME}
- MAILDEV_INCOMING_PASS=${_APP_SMTP_PASSWORD}
request-catcher-webhook: # used mainly for dev tests (mock HTTP webhook)
image: appwrite/requestcatcher:1.0.0
container_name: appwrite-requestcatcher-webhook
<<: *x-logging
ports:
- "9504:5000"
networks:
- appwrite
request-catcher-sms: # used mainly for dev tests (mock SMS auth secret)
image: appwrite/requestcatcher:1.0.0
container_name: appwrite-requestcatcher-sms
<<: *x-logging
ports:
- "9507:5000"
networks:
- appwrite
adminer:
image: adminer
container_name: appwrite-adminer
<<: *x-logging
restart: always
ports:
- 9506:8080
networks:
- appwrite
- gateway
environment:
- ADMINER_DESIGN=pepa-linha
- ADMINER_DEFAULT_SERVER=mariadb
- ADMINER_DEFAULT_USERNAME=root
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
- ADMINER_DEFAULT_DB=appwrite
configs:
- source: adminer-index.php
target: /var/www/html/index.php
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.tls=true"
redis-insight:
image: redis/redisinsight:latest
restart: unless-stopped
networks:
- appwrite
- gateway
environment:
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
configs:
- source: redisinsight-connections.json
target: /mnt/connections.json
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
ports:
- "8081:5540"
graphql-explorer:
container_name: appwrite-graphql-explorer
image: appwrite/altair:0.3.0
restart: unless-stopped
networks:
- appwrite
ports:
- "9509:3000"
environment:
- SERVER_URL=http://localhost/v1/graphql
# Dev Tools End ------------------------------------------------------------------------------------------
networks:
gateway:
name: gateway
@@ -1381,61 +1139,6 @@ networks:
runtimes:
name: runtimes
configs:
redisinsight-connections.json:
content: |
[
{
"compressor": "NONE",
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
"host": "redis",
"port": 6379,
"name": "redis:6379",
"db": 0,
"username": "default",
"password": null,
"connectionType": "STANDALONE",
"nameFromProvider": null,
"provider": "REDIS",
"lastConnection": "2025-10-16T09:22:02.591Z",
"modules": [
{
"name": "ReJSON",
"version": 20808,
"semanticVersion": "2.8.8"
},
{
"name": "search",
"version": 21015,
"semanticVersion": "2.10.15"
}
],
"tls": false,
"tlsServername": null,
"verifyServerCert": null,
"caCert": null,
"clientCert": null,
"ssh": false,
"sshOptions": null,
"forceStandalone": false,
"tags": []
}
]
adminer-index.php:
content: |
<?php
if(!count($$_GET)) {
$$_POST['auth'] = [
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'driver' => 'server', /* seems to autodetect the driver from server settings */
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
];
}
include './adminer.php';
volumes:
appwrite-mariadb:
appwrite-redis:
+2 -1
View File
@@ -20,7 +20,8 @@ class Audit extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME));
->setQueue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
->setClass(System::getEnv('_APP_AUDITS_CLASS_NAME', Event::AUDITS_CLASS_NAME));
}
/**
+2 -1
View File
@@ -19,7 +19,8 @@ class Build extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME));
->setQueue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
->setClass(System::getEnv('_APP_BUILDS_CLASS_NAME', Event::BUILDS_CLASS_NAME));
}
/**
+2 -1
View File
@@ -20,7 +20,8 @@ class Certificate extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME));
->setQueue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
->setClass(System::getEnv('_APP_CERTIFICATES_CLASS_NAME', Event::CERTIFICATES_CLASS_NAME));
}
/**
+3
View File
@@ -5,6 +5,7 @@ namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\DSN\DSN;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class Database extends Event
{
@@ -23,6 +24,8 @@ class Database extends Event
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this->setClass(System::getEnv('_APP_DATABASE_CLASS_NAME', Event::DATABASE_CLASS_NAME));
}
/**
+2 -1
View File
@@ -21,7 +21,8 @@ class Delete extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME));
->setQueue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
->setClass(System::getEnv('_APP_DELETE_CLASS_NAME', Event::DELETE_CLASS_NAME));
}
/**
+37
View File
@@ -10,35 +10,50 @@ use Utopia\Queue\Queue;
class Event
{
public const DATABASE_QUEUE_NAME = 'v1-database';
public const DATABASE_CLASS_NAME = 'DatabaseV1';
public const DELETE_QUEUE_NAME = 'v1-deletes';
public const DELETE_CLASS_NAME = 'DeletesV1';
public const AUDITS_QUEUE_NAME = 'v1-audits';
public const AUDITS_CLASS_NAME = 'AuditsV1';
public const MAILS_QUEUE_NAME = 'v1-mails';
public const MAILS_CLASS_NAME = 'MailsV1';
public const FUNCTIONS_QUEUE_NAME = 'v1-functions';
public const FUNCTIONS_CLASS_NAME = 'FunctionsV1';
public const FUNCTIONS_QUEUE_TTL = 60 * 60 * 24 * 7; // 7 days
public const STATS_RESOURCES_QUEUE_NAME = 'v1-stats-resources';
public const STATS_RESOURCES_CLASS_NAME = 'StatsResourcesV1';
public const STATS_USAGE_QUEUE_NAME = 'v1-stats-usage';
public const STATS_USAGE_CLASS_NAME = 'StatsUsageV1';
public const WEBHOOK_QUEUE_NAME = 'v1-webhooks';
public const WEBHOOK_CLASS_NAME = 'WebhooksV1';
public const CERTIFICATES_QUEUE_NAME = 'v1-certificates';
public const CERTIFICATES_CLASS_NAME = 'CertificatesV1';
public const BUILDS_QUEUE_NAME = 'v1-builds';
public const BUILDS_CLASS_NAME = 'BuildsV1';
public const SCREENSHOTS_QUEUE_NAME = 'v1-screenshots';
public const SCREENSHOTS_CLASS_NAME = 'ScreenshotsV1';
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
public const MESSAGING_CLASS_NAME = 'MessagingV1';
public const EXECUTIONS_QUEUE_NAME = 'v1-executions';
public const EXECUTIONS_CLASS_NAME = 'ExecutionsV1';
public const MIGRATIONS_QUEUE_NAME = 'v1-migrations';
public const MIGRATIONS_CLASS_NAME = 'MigrationsV1';
protected string $queue = '';
protected string $class = '';
protected string $event = '';
protected array $params = [];
protected array $sensitive = [];
@@ -291,6 +306,28 @@ class Event
return $this->context[$key] ?? null;
}
/**
* Set class used for this event.
* @param string $class
* @return self
*/
public function setClass(string $class): self
{
$this->class = $class;
return $this;
}
/**
* Get class used for this event.
*
* @return string
*/
public function getClass(): string
{
return $this->class;
}
/**
* Set param of event.
*
+2 -1
View File
@@ -14,7 +14,8 @@ class Execution extends Event
parent::__construct($publisher);
$this
->setQueue(Event::EXECUTIONS_QUEUE_NAME);
->setQueue(Event::EXECUTIONS_QUEUE_NAME)
->setClass(Event::EXECUTIONS_CLASS_NAME);
}
/**
+1
View File
@@ -25,6 +25,7 @@ class Func extends Event
$this
->setQueue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME))
->setClass(System::getEnv('_APP_FUNCTIONS_CLASS_NAME', Event::FUNCTIONS_CLASS_NAME))
->setTTL(Event::FUNCTIONS_QUEUE_TTL);
}
+2 -1
View File
@@ -25,7 +25,8 @@ class Mail extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME));
->setQueue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
->setClass(System::getEnv('_APP_MAILS_CLASS_NAME', Event::MAILS_CLASS_NAME));
}
/**
+2 -1
View File
@@ -20,7 +20,8 @@ class Messaging extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME));
->setQueue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
->setClass(System::getEnv('_APP_MESSAGING_CLASS_NAME', Event::MESSAGING_CLASS_NAME));
}
/**
+2 -1
View File
@@ -17,7 +17,8 @@ class Migration extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME));
->setQueue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME))
->setClass(System::getEnv('_APP_MIGRATIONS_CLASS_NAME', Event::MIGRATIONS_CLASS_NAME));
}
/**
+2 -1
View File
@@ -15,7 +15,8 @@ class Screenshot extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME));
->setQueue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
->setClass(System::getEnv('_APP_SCREENSHOTS_CLASS_NAME', Event::SCREENSHOTS_CLASS_NAME));
}
public function setDeploymentId(string $deploymentId): self
+2 -1
View File
@@ -14,7 +14,8 @@ class StatsResources extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME));
->setQueue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
->setClass(System::getEnv('_APP_STATS_RESOURCES_CLASS_NAME', Event::STATS_RESOURCES_CLASS_NAME));
}
/**
+2 -1
View File
@@ -19,7 +19,8 @@ class StatsUsage extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME));
->setQueue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
->setClass(System::getEnv('_APP_STATS_USAGE_CLASS_NAME', Event::STATS_USAGE_CLASS_NAME));
}
/**
+2 -1
View File
@@ -12,7 +12,8 @@ class Webhook extends Event
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME));
->setQueue(System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME))
->setClass(System::getEnv('_APP_WEBHOOK_CLASS_NAME', Event::WEBHOOK_CLASS_NAME));
}
/**
+1
View File
@@ -318,6 +318,7 @@ class Exception extends \Exception
/** Keys */
public const string KEY_NOT_FOUND = 'key_not_found';
public const string KEY_ALREADY_EXISTS = 'key_already_exists';
/** Variables */
public const string VARIABLE_NOT_FOUND = 'variable_not_found';
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Keys extends Base
{
public const ALLOWED_ATTRIBUTES = [
'expire',
'accessedAt',
'name',
'scopes',
];
public function __construct()
{
parent::__construct('keys', self::ALLOWED_ATTRIBUTES);
}
}
@@ -1168,7 +1168,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'duration' => 60, // Set session duration to 1 minute
'duration' => 10, // Set session duration to 10 seconds
]);
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1177,7 +1177,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertArrayHasKey('platforms', $response['body']);
$this->assertArrayHasKey('webhooks', $response['body']);
$this->assertArrayHasKey('keys', $response['body']);
$this->assertEquals(60, $response['body']['authDuration']);
$this->assertEquals(10, $response['body']['authDuration']);
$projectId = $response['body']['$id'];
@@ -1218,44 +1218,30 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
// Check session doesn't expire too soon.
sleep(30);
// Eventually session expires, within 15 seconds (10+variance)
$this->assertEventually(function () use ($projectId, $sessionCookie) {
// Get User
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'Cookie' => $sessionCookie,
]));
// Get User
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'Cookie' => $sessionCookie,
]));
$this->assertEquals(401, $response['headers']['status-code']);
}, timeoutMs: 15 * 1000);
$this->assertEquals(200, $response['headers']['status-code']);
// Wait just over a minute
sleep(35);
// Get User
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'Cookie' => $sessionCookie,
]));
$this->assertEquals(401, $response['headers']['status-code']);
// Set session duration to 15s
// Set session duration to 10min
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/duration', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'duration' => 15, // seconds
'duration' => 600, // seconds
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(15, $response['body']['authDuration']);
// Wait 20 seconds, ensure non-valid session
\sleep(20);
$this->assertEquals(600, $response['body']['authDuration']);
// Ensure session is still expired (new duration only affects new sessions)
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
@@ -3157,7 +3143,7 @@ class ProjectsConsoleClientTest extends Scope
/**
* Test for SUCCESS with custom ID
*/
$customKeyId = 'key-with-custom-id';
$customKeyId = \uniqid() . 'custom-id';
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -3170,6 +3156,20 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertSame($customKeyId, $response['body']['$id']);
/**
* Test for FAILURE with custom ID
*/
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => $customKeyId,
'name' => 'Key Custom',
'scopes' => ['teams.read', 'teams.write'],
]);
$this->assertEquals(409, $response['headers']['status-code']);
/**
* Test for SUCCESS with magic string ID
*/
@@ -3216,19 +3216,258 @@ class ProjectsConsoleClientTest extends Scope
{
$id = $data['projectId'] ?? '';
/** Create a second key with an expiry for query testing */
$expireDate = DateTime::addSeconds(new \DateTime(), 3600);
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test 2',
'scopes' => ['users.read'],
'expire' => $expireDate,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$key2Id = $response['body']['$id'];
/** List all keys (no queries) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(5, $response['body']['total']);
$this->assertCount(5, $response['body']['keys']);
/** List keys with limit */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals(5, $response['body']['total']);
/** List keys with offset */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::offset(1)->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['keys']);
$this->assertEquals(5, $response['body']['total']);
/** List keys with cursor after */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => $data['keyId']]))->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals(5, $response['body']['total']);
$this->assertEquals($key2Id, $response['body']['keys'][0]['$id']);
/** List keys filtering by expire (lessThan now — should match none) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::lessThan('expire', (new \DateTime())->format('Y-m-d H:i:s'))->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
/** List keys filtering by expire (greaterThan now — should match the key with expiry) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::greaterThan('expire', (new \DateTime())->format('Y-m-d H:i:s'))->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
$this->assertCount(1, $response['body']['keys']);
/** List keys filtering by name (equal — exact match) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Key Test'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals('Key Test', $response['body']['keys'][0]['name']);
/** List keys filtering by name (equal — multiple values) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Key Test', 'Key Test 2'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(2, $response['body']['total']);
$this->assertCount(2, $response['body']['keys']);
/** List keys filtering by name (equal — no match) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Non Existent Key'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
$this->assertCount(0, $response['body']['keys']);
/** List keys filtering by scopes (contains — match key with teams.read) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('scopes', ['teams.read'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(4, $response['body']['total']);
$this->assertCount(4, $response['body']['keys']);
/** List keys filtering by scopes (contains — match key with users.read) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('scopes', ['users.read'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
$this->assertCount(1, $response['body']['keys']);
/** List keys filtering by scopes (contains — no match) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('scopes', ['databases.read'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
$this->assertCount(0, $response['body']['keys']);
/** List keys filtering by name and scopes combined */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Key Test'])->toString(),
Query::contains('scopes', ['teams.read'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals('Key Test', $response['body']['keys'][0]['name']);
/** List keys with orderDesc */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::orderDesc('$createdAt')->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(5, $response['body']['keys']);
$this->assertGreaterThan($response['body']['keys'][1]['$createdAt'], $response['body']['keys'][0]['$createdAt']);
/** List keys with total disabled */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false,
'queries' => [
Query::limit(1)->toString()
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals(0, $response['body']['total']);
/**
* Test for FAILURE
*/
/** Test invalid query attribute */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('secret', ['test'])->toString(),
]
]);
$this->assertEquals(400, $response['headers']['status-code']);
/** Test invalid cursor */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => 'invalid']))->toString(),
]
]);
$this->assertEquals(400, $response['headers']['status-code']);
return $data;
}
+9
View File
@@ -21,6 +21,7 @@ class EventTest extends TestCase
$this->queue = 'v1-tests' . uniqid();
$this->object = new Event($this->publisher);
$this->object->setClass('TestsV1');
$this->object->setQueue($this->queue);
}
@@ -32,6 +33,14 @@ class EventTest extends TestCase
$this->object->setQueue($this->queue);
}
public function testClass(): void
{
$this->assertEquals('TestsV1', $this->object->getClass());
$this->object->setClass('TestsV2');
$this->assertEquals('TestsV2', $this->object->getClass());
$this->object->setClass('TestsV1');
}
public function testParams(): void
{