Merge branch '1.6.x' into feat-bulk-operations

# Conflicts:
#	app/config/specs/open-api3-latest-client.json
#	app/config/specs/open-api3-latest-console.json
#	app/config/specs/open-api3-latest-server.json
#	app/config/specs/swagger2-latest-client.json
#	app/config/specs/swagger2-latest-console.json
#	app/config/specs/swagger2-latest-server.json
#	app/controllers/api/databases.php
#	app/controllers/api/functions.php
#	app/controllers/api/migrations.php
#	app/controllers/api/projects.php
#	app/controllers/api/proxy.php
#	app/controllers/api/storage.php
#	app/controllers/api/users.php
#	app/controllers/api/vcs.php
#	composer.json
#	composer.lock
#	docs/references/account/create-push-target.md
#	docs/references/account/delete-push-target.md
#	docs/references/account/update-push-target.md
#	src/Appwrite/SDK/Method.php
#	src/Appwrite/Specification/Format/OpenAPI3.php
#	src/Appwrite/Specification/Format/Swagger2.php
This commit is contained in:
Bradley Schofield
2025-01-23 11:19:20 +09:00
593 changed files with 11539 additions and 11115 deletions
+2
View File
@@ -23,6 +23,7 @@ _APP_OPENSSL_KEY_V1=your-secret-key
_APP_DOMAIN=traefik
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_TARGET=localhost
_APP_RULES_FORMAT=md5
_APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379
_APP_REDIS_PASS=
@@ -109,3 +110,4 @@ _APP_MESSAGE_EMAIL_TEST_DSN=
_APP_MESSAGE_PUSH_TEST_DSN=
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
_APP_PROJECT_REGIONS=default
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
+33
View File
@@ -0,0 +1,33 @@
name: "Console SDK Preview"
on:
pull_request:
paths:
- 'app/config/specs/*-latest-console.json'
jobs:
setup:
name: Setup & Build Console SDK
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Load and Start Appwrite
run: |
docker compose build
docker compose up -d
docker compose exec appwrite sdks --platform=console --sdk=web --version=latest --git=no
sudo chown -R $USER:$USER ./app/sdks/console-web
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Build and Publish SDK
working-directory: ./app/sdks/console-web
run: |
npm install
npm run build
npx pkg-pr-new publish --comment=update
+30 -6
View File
@@ -127,6 +127,11 @@ jobs:
Messaging,
Migrations
]
tables-mode: [
'Project',
'Shared V1',
'Shared V2',
]
steps:
- name: checkout
@@ -145,16 +150,33 @@ jobs:
docker compose up -d
sleep 30
- name: Run ${{matrix.service}} Tests
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
- name: Run ${{matrix.service}} Shared Tables Tests
run: _APP_DATABASE_SHARED_TABLES=database_db_main docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
- name: Run ${{ matrix.service }} tests with ${{ matrix.tables-mode }} table mode
run: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
else
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
export _APP_DATABASE_SHARED_TABLES_V1=
fi
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug
benchmarking:
name: Benchmark
runs-on: ubuntu-latest
needs: setup
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -215,6 +237,7 @@ jobs:
path: benchmark.json
retention-days: 7
- name: Find Comment
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/find-comment@v3
id: fc
with:
@@ -222,9 +245,10 @@ jobs:
comment-author: 'github-actions[bot]'
body-includes: Benchmark results
- name: Comment on PR
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: benchmark.txt
edit-mode: replace
edit-mode: replace
+1
View File
@@ -16,3 +16,4 @@ dev/yasd_init.php
.phpunit.result.cache
Makefile
appwrite.json
/.zed/
+114
View File
@@ -1,3 +1,117 @@
# Version 1.6.1
## What's Changed
### Notable changes
* Remove JPEG fallback for webp by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8746
* Add heic and avif support by @lohanidamodar in https://github.com/appwrite/appwrite/pull/7718
* Add new runtimes by @Meldiron in https://github.com/appwrite/appwrite/pull/8771
* Remove audits deletion by @shimonewman in https://github.com/appwrite/appwrite/pull/8766
* Bump assistant by @loks0n in https://github.com/appwrite/appwrite/pull/8801
* Change max queries values to 500 by @fogelito in https://github.com/appwrite/appwrite/pull/8802
* Allow '.wav' as 'audio/x-wav' as well by @basert in https://github.com/appwrite/appwrite/pull/8846
* Use 1 instead of 0.5 cpu for default function specification by @loks0n in https://github.com/appwrite/appwrite/pull/8848
* Update function runtimes by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8781
* Add a realtime heartbeat by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/8943
### Fixes
* Trigger functions event only if event is not paused by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8526
* Update docker-compose to restart usage-dump by @feschaffa in https://github.com/appwrite/appwrite/pull/8642
* Fix typo in scheduler base by @fogelito in https://github.com/appwrite/appwrite/pull/8691
* Add domain and force HTTPS env vars to mail worker by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8722
* Fix webp by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8732
* Ignore junction tables by @fogelito in https://github.com/appwrite/appwrite/pull/8728
* Fix logger throwing fatal error by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8724
* Fix missing protocol for testing SMTP by @byawitz in https://github.com/appwrite/appwrite/pull/8749
* Make create execution async loose by @loks0n in https://github.com/appwrite/appwrite/pull/8707
* Fix invalid cursor value by @fogelito in https://github.com/appwrite/appwrite/pull/8109
* Fix target deletes by @abnegate in https://github.com/appwrite/appwrite/pull/8833
* Fix translation commas by @loks0n in https://github.com/appwrite/appwrite/pull/8892
* Fix Migrations having source creds being overwritten and add Migration tests by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8897
* Fix validator usage for updating string size by @abnegate in https://github.com/appwrite/appwrite/pull/8890
* Fix create user event not triggering by @loks0n in https://github.com/appwrite/appwrite/pull/8718
* Improve error handling and logging in the database worker by @fogelito in https://github.com/appwrite/appwrite/pull/8944
* Remove inaccurate info about leaving the URL parameter empty by @ebenezerdon in https://github.com/appwrite/appwrite/pull/8963
* Ensure indexes are updated when updating an attribute key by @fogelito in https://github.com/appwrite/appwrite/pull/8971
* Remove duplicate dart-2.16 runtime template by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8972
* Fix team invites with existing session by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9006
* Improve handling of HTTP requests by dispatching to safe workers by @Meldiron in https://github.com/appwrite/appwrite/pull/9016
* Fix users create session secret by @stnguyen90 in https://github.com/appwrite/appwrite/pull/9019
* Fix swoole task warning by @Meldiron in https://github.com/appwrite/appwrite/pull/9025
### Miscellaneous
* Update Init copy by @adityaoberai in https://github.com/appwrite/appwrite/pull/8557
* Fix security scan permissions and comment by @EVDOG4LIFE in https://github.com/appwrite/appwrite/pull/8525
* Add Trivy security scans by @btme0011 in https://github.com/appwrite/appwrite/pull/6876
* Update database stack by @abnegate in https://github.com/appwrite/appwrite/pull/8564
* Bump database by @abnegate in https://github.com/appwrite/appwrite/pull/8573
* Sync main with 1.5.x by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8589
* Add AWS to one-click installs by @byawitz in https://github.com/appwrite/appwrite/pull/8593
* Update Init copy in readme by @adityaoberai in https://github.com/appwrite/appwrite/pull/8618
* Sync main into 1.6.x by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8685
* Sync 1.6.x into main by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8686
* Feat coroutines by @Meldiron in https://github.com/appwrite/appwrite/pull/7826
* Sync main into 1.6.x by @Meldiron in https://github.com/appwrite/appwrite/pull/8719
* Sentence casing endpoint API reference by @choir241 in https://github.com/appwrite/appwrite/pull/8617
* DB storage metrics by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8404
* Fix exception thrown when optional array attribute does not exist by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8391
* Add projects channels to realtime by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/8735
* Base for console roles support by @lohanidamodar in https://github.com/appwrite/appwrite/pull/8565
* Remove DB disk storage calculation by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8745
* Messaging adapter default values by @shimonewman in https://github.com/appwrite/appwrite/pull/8742
* Add payload response type by @loks0n in https://github.com/appwrite/appwrite/pull/8720
* Fix flaky functions tests by @loks0n in https://github.com/appwrite/appwrite/pull/8682
* Migrations Backups by @fogelito in https://github.com/appwrite/appwrite/pull/8186
* Add test for response and request filters by @vermakhushboo in https://github.com/appwrite/appwrite/pull/8697
* Bump version in SECURITY.md by @EVDOG4LIFE in https://github.com/appwrite/appwrite/pull/8755
* Add originalId attribute to databases collection by @fogelito in https://github.com/appwrite/appwrite/pull/8764
* Fix Walter References by @ItzNotABug in https://github.com/appwrite/appwrite/pull/8757
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8769
* Move new attributes by @abnegate in https://github.com/appwrite/appwrite/pull/8777
* Add ping endpoint by @loks0n in https://github.com/appwrite/appwrite/pull/8761
* Fix GitHub action caching by @loks0n in https://github.com/appwrite/appwrite/pull/8772
* Chore release ruby SDK by @abnegate in https://github.com/appwrite/appwrite/pull/8767
* Call migration success on success by @abnegate in https://github.com/appwrite/appwrite/pull/8782
* Update utopia-php/system to 0.9.0 by @basert in https://github.com/appwrite/appwrite/pull/8780
* Move createDocument from api to worker by @vermakhushboo in https://github.com/appwrite/appwrite/pull/8776
* Add missing indexes by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8803
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8809
* Fix typo in BLR region by @stnguyen90 in https://github.com/appwrite/appwrite/pull/8756
* Add tests for project variables by @vermakhushboo in https://github.com/appwrite/appwrite/pull/8815
* Replace 'Expires' with 'Cache-Control: private' header to avoid CDN caching by @basert in https://github.com/appwrite/appwrite/pull/8836
* Allow blocking based on resource attributes by @basert in https://github.com/appwrite/appwrite/pull/8812
* Check if resource is blocked inside functions worker by @basert in https://github.com/appwrite/appwrite/pull/8855
* Fix missing allow attribute by @abnegate in https://github.com/appwrite/appwrite/pull/8889
* Revert function execution order by @basert in https://github.com/appwrite/appwrite/pull/8857
* Use resource type constants by @basert in https://github.com/appwrite/appwrite/pull/8895
* Update Database lib by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8680
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8917
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8923
* Update database for transaction counter fixes with retries by @abnegate in https://github.com/appwrite/appwrite/pull/8927
* Validate string permissions by @fogelito in https://github.com/appwrite/appwrite/pull/8929
* Add PubSub adapter support by @basert in https://github.com/appwrite/appwrite/pull/8905
* List memberships as client by @loks0n in https://github.com/appwrite/appwrite/pull/8913
* Fix XDebug Extension not being removed by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8891
* Update database by @abnegate in https://github.com/appwrite/appwrite/pull/8946
* Use utopia compression by @loks0n in https://github.com/appwrite/appwrite/pull/8938
* Make compression minimum size configurable by @loks0n in https://github.com/appwrite/appwrite/pull/8947
* Revert "Update database" by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8949
* Fix setpaused by @loks0n in https://github.com/appwrite/appwrite/pull/8948
* Use getDocument instead of find() for rules by @christyjacob4 in https://github.com/appwrite/appwrite/pull/8951
* Remove double fetch from migrations worker by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8956
* Fix memberships privacy MFA by @loks0n in https://github.com/appwrite/appwrite/pull/8969
* Add telemetry by @basert in https://github.com/appwrite/appwrite/pull/8960
* Send migration errors individually by @PineappleIOnic in https://github.com/appwrite/appwrite/pull/8959
* Add console sdk previews by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/8990
* Unset index length by @fogelito in https://github.com/appwrite/appwrite/pull/8978
* Update base to 0.9.5 by @basert in https://github.com/appwrite/appwrite/pull/9005
* Sync main into 1.6.x by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9011
* Improved shared tables V2 by @abnegate in https://github.com/appwrite/appwrite/pull/9013
* Ensure backwards compatibility for 1.6.x by @christyjacob4 in https://github.com/appwrite/appwrite/pull/9018
# Version 1.6.0
## What's Changed
+12
View File
@@ -613,6 +613,18 @@ If you need to clear the cache, you can do so by running the following command:
docker compose exec redis redis-cli FLUSHALL
```
## Using preview domains locally
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud woekspaces such as Gitpod or GitHub Codespaces.
To use preview domains on Cloud workspaces, you can visit hostname provided by them, and supply function's preview domain as URL parameter:
```
https://8080-appwrite-appwrite-mjeb3ebilwv.ws-eu116.gitpod.io/ping?preview=672b3c7eab1ac523ccf5.functions.localhost
```
The path was set to `/ping` intentionally. Visiting `/` for preview domains might trigger Console background worker, and trigger redirect to Console without our preview URL param. Visiting different path ensures this doesnt happen.
## Tutorials
From time to time, our team will add tutorials that will help contributors find their way in the Appwrite source code. Below is a list of currently available tutorials:
+1 -1
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:0.9.3 AS final
FROM appwrite/base:0.9.5 AS final
LABEL maintainer="team@appwrite.io"
+3 -3
View File
@@ -67,7 +67,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
### Windows
@@ -79,7 +79,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
#### PowerShell
@@ -89,7 +89,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+3 -3
View File
@@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
### Windows
@@ -87,7 +87,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
#### PowerShell
@@ -97,7 +97,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.6.0
appwrite/appwrite:1.6.1
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
+14 -13
View File
@@ -52,7 +52,7 @@ CLI::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
CLI::setResource('dbForConsole', function ($pools, $cache) {
CLI::setResource('dbForPlatform', function ($pools, $cache) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -67,9 +67,9 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
->pop()
->getResource();
$dbForConsole = new Database($dbAdapter, $cache);
$dbForPlatform = new Database($dbAdapter, $cache);
$dbForConsole
$dbForPlatform
->setNamespace('_console')
->setMetadata('host', \gethostname())
->setMetadata('project', 'console');
@@ -78,7 +78,7 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
$collections = Config::getParam('collections', [])['console'];
$last = \array_key_last($collections);
if (!($dbForConsole->exists($dbForConsole->getDatabase(), $last))) { /** TODO cache ready variable using registry */
if (!($dbForPlatform->exists($dbForPlatform->getDatabase(), $last))) { /** TODO cache ready variable using registry */
throw new Exception('Tables not ready yet.');
}
@@ -94,15 +94,15 @@ CLI::setResource('dbForConsole', function ($pools, $cache) {
throw new Exception("Console is not ready yet. Please try again later.");
}
return $dbForConsole;
return $dbForPlatform;
}, ['pools', 'cache']);
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -114,8 +114,9 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -136,10 +137,10 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
->getResource();
$database = new Database($dbAdapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -157,7 +158,7 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
}, ['pools', 'dbForPlatform', 'cache']);
CLI::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
@@ -180,7 +181,7 @@ CLI::setResource('logError', function (Registry $register) {
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
+15 -6147
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
<?php
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
return [
'collections' => [
'$collection' => ID::custom('databases'),
'$id' => ID::custom('collections'),
'name' => 'Collections',
'attributes' => [
[
'$id' => ID::custom('databaseInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('databaseId'),
'type' => Database::VAR_STRING,
'signed' => true,
'size' => Database::LENGTH_KEY,
'format' => '',
'filters' => [],
'required' => true,
'default' => null,
'array' => false,
],
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'size' => 256,
'required' => true,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('enabled'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => true,
'default' => null,
'array' => false,
],
[
'$id' => ID::custom('documentSecurity'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => true,
'default' => null,
'array' => false,
],
[
'$id' => ID::custom('attributes'),
'type' => Database::VAR_STRING,
'size' => 1000000,
'required' => false,
'signed' => true,
'array' => false,
'filters' => ['subQueryAttributes'],
],
[
'$id' => ID::custom('indexes'),
'type' => Database::VAR_STRING,
'size' => 1000000,
'required' => false,
'signed' => true,
'array' => false,
'filters' => ['subQueryIndexes'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_fulltext_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [256],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_enabled'),
'type' => Database::INDEX_KEY,
'attributes' => ['enabled'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_documentSecurity'),
'type' => Database::INDEX_KEY,
'attributes' => ['documentSecurity'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
]
];
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -349,11 +349,6 @@ return [
'description' => 'Team with the requested ID could not be found.',
'code' => 404,
],
Exception::TEAM_INVITE_ALREADY_EXISTS => [
'name' => Exception::TEAM_INVITE_ALREADY_EXISTS,
'description' => 'User has already been invited or is already a member of this team',
'code' => 409,
],
Exception::TEAM_INVITE_NOT_FOUND => [
'name' => Exception::TEAM_INVITE_NOT_FOUND,
'description' => 'The requested team invitation could not be found.',
@@ -686,7 +681,7 @@ return [
],
Exception::ATTRIBUTE_LIMIT_EXCEEDED => [
'name' => Exception::ATTRIBUTE_LIMIT_EXCEEDED,
'description' => 'The maximum number of attributes has been reached.',
'description' => 'The maximum number or size of attributes for this collection has been reached.',
'code' => 400,
],
Exception::ATTRIBUTE_VALUE_INVALID => [
@@ -731,6 +726,11 @@ return [
'description' => 'Index invalid.',
'code' => 400,
],
Exception::INDEX_DEPENDENCY => [
'name' => Exception::INDEX_DEPENDENCY,
'description' => 'Attribute cannot be renamed or deleted. Please remove the associated index first.',
'code' => 409,
],
/** Project Errors */
Exception::PROJECT_NOT_FOUND => [
+3 -2
View File
@@ -11,7 +11,7 @@ const TEMPLATE_RUNTIMES = [
],
'DART' => [
'name' => 'dart',
'versions' => ['3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16']
'versions' => ['3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16']
],
'GO' => [
'name' => 'go',
@@ -1567,7 +1567,8 @@ return [
'required' => false,
'type' => 'number'
]
]
],
'scopes' => []
],
[
'icon' => 'icon-chip',
+16 -16
View File
@@ -11,7 +11,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '16.0.2',
'version' => '16.1.0',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
@@ -59,7 +59,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '13.0.0',
'version' => '13.1.1',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -77,7 +77,7 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '7.0.0',
'version' => '7.1.0',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
@@ -112,7 +112,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '6.0.0',
'version' => '6.1.0',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@@ -134,7 +134,7 @@ return [
[
'key' => 'react-native',
'name' => 'React Native',
'version' => '0.5.0',
'version' => '0.6.0',
'url' => 'https://github.com/appwrite/sdk-for-react-native',
'package' => 'https://npmjs.com/package/react-native-appwrite',
'enabled' => true,
@@ -217,7 +217,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '6.0.0',
'version' => '6.2.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -245,7 +245,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '14.1.0',
'version' => '14.2.0',
'url' => 'https://github.com/appwrite/sdk-for-node',
'package' => 'https://www.npmjs.com/package/node-appwrite',
'enabled' => true,
@@ -263,7 +263,7 @@ return [
[
'key' => 'deno',
'name' => 'Deno',
'version' => '12.1.0',
'version' => '12.2.0',
'url' => 'https://github.com/appwrite/sdk-for-deno',
'package' => 'https://deno.land/x/appwrite',
'enabled' => true,
@@ -281,7 +281,7 @@ return [
[
'key' => 'php',
'name' => 'PHP',
'version' => '12.1.0',
'version' => '12.2.0',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
@@ -299,7 +299,7 @@ return [
[
'key' => 'python',
'name' => 'Python',
'version' => '6.1.0',
'version' => '6.2.0',
'url' => 'https://github.com/appwrite/sdk-for-python',
'package' => 'https://pypi.org/project/appwrite/',
'enabled' => true,
@@ -317,7 +317,7 @@ return [
[
'key' => 'ruby',
'name' => 'Ruby',
'version' => '12.1.1',
'version' => '12.2.0',
'url' => 'https://github.com/appwrite/sdk-for-ruby',
'package' => 'https://rubygems.org/gems/appwrite',
'enabled' => true,
@@ -335,7 +335,7 @@ return [
[
'key' => 'go',
'name' => 'Go',
'version' => '0.2.0',
'version' => '0.3.0',
'url' => 'https://github.com/appwrite/sdk-for-go',
'package' => 'https://github.com/appwrite/sdk-for-go',
'enabled' => true,
@@ -353,7 +353,7 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.10.1',
'version' => '0.11.0',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => true,
@@ -371,7 +371,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '12.1.0',
'version' => '12.2.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
@@ -389,7 +389,7 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '6.1.0',
'version' => '6.2.0',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,
@@ -411,7 +411,7 @@ return [
[
'key' => 'swift',
'name' => 'Swift',
'version' => '6.1.0',
'version' => '6.2.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,
+1 -1
View File
@@ -6,7 +6,7 @@ return [
Specification::S_05VCPU_512MB => [
'slug' => Specification::S_05VCPU_512MB,
'memory' => 512,
'cpus' => 1 // TODO: revert this, it's a temporary change to test function performance.
'cpus' => 0.5
],
Specification::S_1VCPU_512MB => [
'slug' => Specification::S_1VCPU_512MB,
+4 -4
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2784,7 +2784,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -5766,7 +5766,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5851,7 +5851,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
+146 -363
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2795,7 +2795,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -9488,7 +9488,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -10145,7 +10146,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -13673,7 +13675,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13751,7 +13753,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13897,7 +13899,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14045,7 +14047,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14120,7 +14122,7 @@
},
"data": {
"type": "object",
"description": "Additional Data for push notification.",
"description": "Additional key-value pair data for push notification.",
"x-example": "{}"
},
"action": {
@@ -14140,7 +14142,7 @@
},
"sound": {
"type": "string",
"description": "Sound for push notification. Available only for Android and IOS Platform.",
"description": "Sound for push notification. Available only for Android and iOS Platform.",
"x-example": "<SOUND>"
},
"color": {
@@ -14154,9 +14156,9 @@
"x-example": "<TAG>"
},
"badge": {
"type": "string",
"description": "Badge for push notification. Available only for IOS Platform.",
"x-example": "<BADGE>"
"type": "integer",
"description": "Badge for push notification. Available only for iOS Platform.",
"x-example": null
},
"draft": {
"type": "boolean",
@@ -14167,12 +14169,31 @@
"type": "string",
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device state and may not deliver notifications immediately. \"high\" will always attempt to immediately deliver the notification.",
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
},
"required": [
"messageId",
"title",
"body"
"messageId"
]
}
}
@@ -14202,7 +14223,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14331,6 +14352,27 @@
"type": "string",
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device battery state and may send notifications later. \"high\" will always attempt to immediately deliver the notification.",
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
}
}
@@ -14361,7 +14403,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14472,7 +14514,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14586,7 +14628,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14641,7 +14683,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14705,7 +14747,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14782,7 +14824,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14859,7 +14901,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14937,7 +14979,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15044,7 +15086,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15154,7 +15196,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15241,7 +15283,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15331,7 +15373,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15448,7 +15490,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15568,7 +15610,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15665,7 +15707,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15765,7 +15807,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15872,7 +15914,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15982,7 +16024,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16127,7 +16169,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16274,7 +16316,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16371,7 +16413,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16471,7 +16513,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16568,7 +16610,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16668,7 +16710,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16765,7 +16807,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16865,7 +16907,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16962,7 +17004,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17062,7 +17104,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17117,7 +17159,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17181,7 +17223,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17258,7 +17300,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17335,7 +17377,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17411,7 +17453,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17496,7 +17538,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17558,7 +17600,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17637,7 +17679,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17701,7 +17743,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17778,7 +17820,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17864,7 +17906,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17956,7 +17998,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18021,7 +18063,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18098,7 +18140,7 @@
},
"x-appwrite": {
"method": "list",
"weight": 339,
"weight": 338,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18264,7 +18306,7 @@
},
"x-appwrite": {
"method": "getAppwriteReport",
"weight": 341,
"weight": 340,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18339,7 +18381,7 @@
},
"\/migrations\/firebase": {
"post": {
"summary": "Migrate Firebase data (Service Account)",
"summary": "Migrate Firebase data",
"operationId": "migrationsCreateFirebaseMigration",
"tags": [
"migrations"
@@ -18359,7 +18401,7 @@
},
"x-appwrite": {
"method": "createFirebaseMigration",
"weight": 336,
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18415,177 +18457,6 @@
}
}
},
"\/migrations\/firebase\/deauthorize": {
"get": {
"summary": "Revoke Appwrite's authorization to access Firebase projects",
"operationId": "migrationsDeleteFirebaseAuth",
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "File"
}
},
"x-appwrite": {
"method": "deleteFirebaseAuth",
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/delete-firebase-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/oauth": {
"post": {
"summary": "Migrate Firebase data (OAuth)",
"operationId": "migrationsCreateFirebaseOAuthMigration",
"tags": [
"migrations"
],
"description": "",
"responses": {
"202": {
"description": "Migration",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/migration"
}
}
}
}
},
"x-appwrite": {
"method": "createFirebaseOAuthMigration",
"weight": 335,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/create-firebase-o-auth-migration.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"requestBody": {
"content": {
"application\/json": {
"schema": {
"type": "object",
"properties": {
"resources": {
"type": "array",
"description": "List of resources to migrate",
"x-example": null,
"items": {
"type": "string"
}
},
"projectId": {
"type": "string",
"description": "Project ID of the Firebase Project",
"x-example": "<PROJECT_ID>"
}
},
"required": [
"resources",
"projectId"
]
}
}
}
}
}
},
"\/migrations\/firebase\/projects": {
"get": {
"summary": "List Firebase projects",
"operationId": "migrationsListFirebaseProjects",
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migrations Firebase Projects List",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/firebaseProjectList"
}
}
}
}
},
"x-appwrite": {
"method": "listFirebaseProjects",
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/list-firebase-projects.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.read",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
]
}
},
"\/migrations\/firebase\/report": {
"get": {
"summary": "Generate a report on Firebase data",
@@ -18608,7 +18479,7 @@
},
"x-appwrite": {
"method": "getFirebaseReport",
"weight": 342,
"weight": 341,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18660,80 +18531,6 @@
]
}
},
"\/migrations\/firebase\/report\/oauth": {
"get": {
"summary": "Generate a report on Firebase data using OAuth",
"operationId": "migrationsGetFirebaseReportOAuth",
"tags": [
"migrations"
],
"description": "",
"responses": {
"200": {
"description": "Migration Report",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/migrationReport"
}
}
}
}
},
"x-appwrite": {
"method": "getFirebaseReportOAuth",
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "migrations\/get-firebase-report-o-auth.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/migrations\/migration-firebase-report.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "migrations.write",
"platforms": [
"console"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "resources",
"description": "List of resources to migrate",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
}
},
"in": "query"
},
{
"name": "projectId",
"description": "Project ID",
"required": true,
"schema": {
"type": "string",
"x-example": "<PROJECT_ID>"
},
"in": "query"
}
]
}
},
"\/migrations\/nhost": {
"post": {
"summary": "Migrate NHost data",
@@ -18756,7 +18553,7 @@
},
"x-appwrite": {
"method": "createNHostMigration",
"weight": 338,
"weight": 337,
"cookies": false,
"type": "",
"deprecated": false,
@@ -18869,7 +18666,7 @@
},
"x-appwrite": {
"method": "getNHostReport",
"weight": 349,
"weight": 343,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19004,7 +18801,7 @@
},
"x-appwrite": {
"method": "createSupabaseMigration",
"weight": 337,
"weight": 336,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19111,7 +18908,7 @@
},
"x-appwrite": {
"method": "getSupabaseReport",
"weight": 348,
"weight": 342,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19237,7 +19034,7 @@
},
"x-appwrite": {
"method": "get",
"weight": 340,
"weight": 339,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19297,7 +19094,7 @@
},
"x-appwrite": {
"method": "retry",
"weight": 350,
"weight": 344,
"cookies": false,
"type": "",
"deprecated": false,
@@ -19350,7 +19147,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 351,
"weight": 345,
"cookies": false,
"type": "",
"deprecated": false,
@@ -32749,30 +32546,6 @@
"migrations"
]
},
"firebaseProjectList": {
"description": "Migrations Firebase Projects List",
"type": "object",
"properties": {
"total": {
"type": "integer",
"description": "Total number of projects documents that matched your query.",
"x-example": 5,
"format": "int32"
},
"projects": {
"type": "array",
"description": "List of projects.",
"items": {
"$ref": "#\/components\/schemas\/firebaseProject"
},
"x-example": ""
}
},
"required": [
"total",
"projects"
]
},
"specificationList": {
"description": "Specifications List",
"type": "object",
@@ -35109,7 +34882,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
@@ -36976,7 +36749,8 @@
"resourceId": {
"type": "string",
"description": "Resource ID.",
"x-example": "5e5ea5c16897e"
"x-example": "5e5ea5c16897e",
"nullable": true
},
"name": {
"type": "string",
@@ -36988,10 +36762,16 @@
"description": "The value of this metric at the timestamp.",
"x-example": 1,
"format": "int32"
},
"estimate": {
"type": "number",
"description": "The estimated value of this metric at the end of the period.",
"x-example": 1,
"format": "double",
"nullable": true
}
},
"required": [
"resourceId",
"name",
"value"
]
@@ -37807,6 +37587,26 @@
"$ref": "#\/components\/schemas\/metricBreakdown"
},
"x-example": []
},
"authPhoneTotal": {
"type": "integer",
"description": "Total aggregated number of phone auth.",
"x-example": 0,
"format": "int32"
},
"authPhoneEstimate": {
"type": "number",
"description": "Estimated total aggregated cost of phone auth.",
"x-example": 0,
"format": "double"
},
"authPhoneCountryBreakdown": {
"type": "array",
"description": "Aggregated breakdown in totals of phone auth by country.",
"items": {
"$ref": "#\/components\/schemas\/metricBreakdown"
},
"x-example": []
}
},
"required": [
@@ -37831,7 +37631,10 @@
"databasesStorageBreakdown",
"executionsMbSecondsBreakdown",
"buildsMbSecondsBreakdown",
"functionsStorageBreakdown"
"functionsStorageBreakdown",
"authPhoneTotal",
"authPhoneEstimate",
"authPhoneCountryBreakdown"
]
},
"headers": {
@@ -38707,26 +38510,6 @@
"size",
"version"
]
},
"firebaseProject": {
"description": "MigrationFirebaseProject",
"type": "object",
"properties": {
"projectId": {
"type": "string",
"description": "Project ID.",
"x-example": "my-project"
},
"displayName": {
"type": "string",
"description": "Project display name.",
"x-example": "My Project"
}
},
"required": [
"projectId",
"displayName"
]
}
},
"securitySchemes": {
+101 -59
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2451,7 +2451,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -8596,7 +8596,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -9019,7 +9020,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12527,7 +12529,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12606,7 +12608,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12753,7 +12755,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12902,7 +12904,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12978,7 +12980,7 @@
},
"data": {
"type": "object",
"description": "Additional Data for push notification.",
"description": "Additional key-value pair data for push notification.",
"x-example": "{}"
},
"action": {
@@ -12998,7 +13000,7 @@
},
"sound": {
"type": "string",
"description": "Sound for push notification. Available only for Android and IOS Platform.",
"description": "Sound for push notification. Available only for Android and iOS Platform.",
"x-example": "<SOUND>"
},
"color": {
@@ -13012,9 +13014,9 @@
"x-example": "<TAG>"
},
"badge": {
"type": "string",
"description": "Badge for push notification. Available only for IOS Platform.",
"x-example": "<BADGE>"
"type": "integer",
"description": "Badge for push notification. Available only for iOS Platform.",
"x-example": null
},
"draft": {
"type": "boolean",
@@ -13025,12 +13027,31 @@
"type": "string",
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device state and may not deliver notifications immediately. \"high\" will always attempt to immediately deliver the notification.",
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
},
"required": [
"messageId",
"title",
"body"
"messageId"
]
}
}
@@ -13060,7 +13081,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13190,6 +13211,27 @@
"type": "string",
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device battery state and may send notifications later. \"high\" will always attempt to immediately deliver the notification.",
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
}
}
@@ -13220,7 +13262,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13332,7 +13374,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13447,7 +13489,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13503,7 +13545,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13568,7 +13610,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13646,7 +13688,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13724,7 +13766,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13803,7 +13845,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13911,7 +13953,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14022,7 +14064,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14110,7 +14152,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14201,7 +14243,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14319,7 +14361,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14440,7 +14482,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14538,7 +14580,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14639,7 +14681,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14747,7 +14789,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14858,7 +14900,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15004,7 +15046,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15152,7 +15194,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15250,7 +15292,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15351,7 +15393,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15449,7 +15491,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15550,7 +15592,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15648,7 +15690,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15749,7 +15791,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15847,7 +15889,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15948,7 +15990,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16004,7 +16046,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16069,7 +16111,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16147,7 +16189,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16225,7 +16267,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16302,7 +16344,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16388,7 +16430,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16451,7 +16493,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16531,7 +16573,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16596,7 +16638,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16674,7 +16716,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16761,7 +16803,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16855,7 +16897,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16921,7 +16963,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -25746,7 +25788,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
+11 -7
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2371,7 +2371,7 @@
"tags": [
"account"
],
"description": "Use this endpoint to update an existing push target for messages for the currently logged in user.",
"description": "Use this endpoint to register a device for push notifications. Provide a target ID (custom or generated using ID.unique()), a device identifier (usually a device token), and optionally specify which provider should send notifications to this target. The target is automatically linked to the current session and includes device information like brand and model.",
"responses": {
"201": {
"description": "Target",
@@ -2449,7 +2449,7 @@
"tags": [
"account"
],
"description": "Use this endpoint to create a new push target for messages for the currently logged in user.",
"description": "Update the currently logged in user's push notification target. You can modify the target's identifier (device token) and provider ID (token, email, phone etc.). The target must exist and belong to the current user. If you change the provider ID, notifications will be sent through the new messaging provider instead.",
"responses": {
"200": {
"description": "Target",
@@ -2526,7 +2526,7 @@
"tags": [
"account"
],
"description": "Use this endpoint to delete an existing push target for messages for the currently logged in user.",
"description": "Delete a push notification target for the currently logged in user. After deletion, the device will no longer receive push notifications. The target must exist and belong to the current user.",
"responses": {
"204": {
"description": "No content"
@@ -2657,7 +2657,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -4400,9 +4400,13 @@
{
"name": "createDocuments",
"parameters": [
"databaseId",
"collectionId",
"documents"
],
"required": [
"databaseId",
"collectionId",
"documents"
],
"responses": [
@@ -5757,7 +5761,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -5839,7 +5843,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
File diff suppressed because it is too large Load Diff
+104 -60
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2343,7 +2343,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -7416,9 +7416,13 @@
{
"name": "createDocuments",
"parameters": [
"databaseId",
"collectionId",
"documents"
],
"required": [
"databaseId",
"collectionId",
"documents"
],
"responses": [
@@ -9485,7 +9489,7 @@
"tags": [
"functions"
],
"description": "",
"description": "Create a new build for an existing function deployment. This endpoint allows you to rebuild a deployment with the updated function configuration, including its entrypoint and build commands if they have been modified The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.",
"responses": {
"204": {
"description": "No content"
@@ -9498,7 +9502,7 @@
"type": "",
"deprecated": false,
"demo": "functions\/create-build.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/functions\/create-deployment-build.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/functions\/create-build.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -9563,7 +9567,7 @@
"tags": [
"functions"
],
"description": "",
"description": "Cancel an ongoing function deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.",
"responses": {
"200": {
"description": "Build",
@@ -12300,7 +12304,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12376,7 +12380,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12520,7 +12524,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12666,7 +12670,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12739,7 +12743,7 @@
},
"data": {
"type": "object",
"description": "Additional Data for push notification.",
"description": "Additional key-value pair data for push notification.",
"x-example": "{}"
},
"action": {
@@ -12759,7 +12763,7 @@
},
"sound": {
"type": "string",
"description": "Sound for push notification. Available only for Android and IOS Platform.",
"description": "Sound for push notification. Available only for Android and iOS Platform.",
"x-example": "<SOUND>"
},
"color": {
@@ -12773,9 +12777,9 @@
"x-example": "<TAG>"
},
"badge": {
"type": "string",
"description": "Badge for push notification. Available only for IOS Platform.",
"x-example": "<BADGE>"
"type": "integer",
"description": "Badge for push notification. Available only for iOS Platform.",
"x-example": null
},
"draft": {
"type": "boolean",
@@ -12786,12 +12790,31 @@
"type": "string",
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device state and may not deliver notifications immediately. \"high\" will always attempt to immediately deliver the notification.",
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
},
"required": [
"messageId",
"title",
"body"
"messageId"
]
}
}
@@ -12821,7 +12844,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 398,
"weight": 392,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12948,6 +12971,27 @@
"type": "string",
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device battery state and may send notifications later. \"high\" will always attempt to immediately deliver the notification.",
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
}
}
@@ -12978,7 +13022,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13087,7 +13131,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13199,7 +13243,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13252,7 +13296,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 399,
"weight": 393,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13314,7 +13358,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13389,7 +13433,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13464,7 +13508,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13540,7 +13584,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13645,7 +13689,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13753,7 +13797,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13838,7 +13882,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13926,7 +13970,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14041,7 +14085,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14159,7 +14203,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14254,7 +14298,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14352,7 +14396,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14457,7 +14501,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14565,7 +14609,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14708,7 +14752,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14853,7 +14897,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14948,7 +14992,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15046,7 +15090,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15141,7 +15185,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15239,7 +15283,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15334,7 +15378,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15432,7 +15476,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15527,7 +15571,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15625,7 +15669,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15678,7 +15722,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15740,7 +15784,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15815,7 +15859,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15890,7 +15934,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15964,7 +16008,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16047,7 +16091,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16107,7 +16151,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16184,7 +16228,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16246,7 +16290,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16321,7 +16365,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16405,7 +16449,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16496,7 +16540,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16559,7 +16603,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -25170,7 +25214,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
+4 -4
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2922,7 +2922,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -5981,7 +5981,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6070,7 +6070,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
File diff suppressed because it is too large Load Diff
+110 -62
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2604,7 +2604,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -8720,7 +8720,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -9166,7 +9167,8 @@
"bun-1.0",
"bun-1.1",
"go-1.23",
"static-1"
"static-1",
"flutter-3.24"
],
"x-enum-name": null,
"x-enum-keys": []
@@ -12752,7 +12754,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12830,7 +12832,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12991,7 +12993,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13149,7 +13151,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13194,13 +13196,13 @@
"title": {
"type": "string",
"description": "Title for push notification.",
"default": null,
"default": "",
"x-example": "<TITLE>"
},
"body": {
"type": "string",
"description": "Body for push notification.",
"default": null,
"default": "",
"x-example": "<BODY>"
},
"topics": {
@@ -13232,7 +13234,7 @@
},
"data": {
"type": "object",
"description": "Additional Data for push notification.",
"description": "Additional key-value pair data for push notification.",
"default": {},
"x-example": "{}"
},
@@ -13256,7 +13258,7 @@
},
"sound": {
"type": "string",
"description": "Sound for push notification. Available only for Android and IOS Platform.",
"description": "Sound for push notification. Available only for Android and iOS Platform.",
"default": "",
"x-example": "<SOUND>"
},
@@ -13273,10 +13275,10 @@
"x-example": "<TAG>"
},
"badge": {
"type": "string",
"description": "Badge for push notification. Available only for IOS Platform.",
"default": "",
"x-example": "<BADGE>"
"type": "integer",
"description": "Badge for push notification. Available only for iOS Platform.",
"default": -1,
"x-example": null
},
"draft": {
"type": "boolean",
@@ -13289,12 +13291,34 @@
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"default": null,
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"default": false,
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"default": false,
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device state and may not deliver notifications immediately. \"high\" will always attempt to immediately deliver the notification.",
"default": "high",
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
},
"required": [
"messageId",
"title",
"body"
"messageId"
]
}
}
@@ -13325,7 +13349,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13467,6 +13491,30 @@
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"default": null,
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"default": null,
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"default": null,
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device battery state and may send notifications later. \"high\" will always attempt to immediately deliver the notification.",
"default": null,
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
}
}
@@ -13498,7 +13546,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13619,7 +13667,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13738,7 +13786,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13798,7 +13846,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13863,7 +13911,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13940,7 +13988,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14017,7 +14065,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14095,7 +14143,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14213,7 +14261,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14329,7 +14377,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14423,7 +14471,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14515,7 +14563,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 352,
"weight": 346,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14645,7 +14693,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14773,7 +14821,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14879,7 +14927,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14983,7 +15031,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 353,
"weight": 347,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15101,7 +15149,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15217,7 +15265,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15379,7 +15427,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15538,7 +15586,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15644,7 +15692,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15748,7 +15796,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15854,7 +15902,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15958,7 +16006,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16064,7 +16112,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16168,7 +16216,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16274,7 +16322,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16378,7 +16426,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16438,7 +16486,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16503,7 +16551,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16580,7 +16628,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16657,7 +16705,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16733,7 +16781,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16826,7 +16874,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16889,7 +16937,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16973,7 +17021,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17038,7 +17086,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17115,7 +17163,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17199,7 +17247,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17293,7 +17341,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17361,7 +17409,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -26233,7 +26281,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
+11 -7
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2508,7 +2508,7 @@
"tags": [
"account"
],
"description": "Use this endpoint to update an existing push target for messages for the currently logged in user.",
"description": "Use this endpoint to register a device for push notifications. Provide a target ID (custom or generated using ID.unique()), a device identifier (usually a device token), and optionally specify which provider should send notifications to this target. The target is automatically linked to the current session and includes device information like brand and model.",
"responses": {
"201": {
"description": "Target",
@@ -2591,7 +2591,7 @@
"tags": [
"account"
],
"description": "Use this endpoint to create a new push target for messages for the currently logged in user.",
"description": "Update the currently logged in user's push notification target. You can modify the target's identifier (device token) and provider ID (token, email, phone etc.). The target must exist and belong to the current user. If you change the provider ID, notifications will be sent through the new messaging provider instead.",
"responses": {
"200": {
"description": "Target",
@@ -2665,7 +2665,7 @@
"tags": [
"account"
],
"description": "Use this endpoint to delete an existing push target for messages for the currently logged in user.",
"description": "Delete a push notification target for the currently logged in user. After deletion, the device will no longer receive push notifications. The target must exist and belong to the current user.",
"responses": {
"204": {
"description": "No content"
@@ -2805,7 +2805,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -4578,9 +4578,13 @@
{
"name": "createDocuments",
"parameters": [
"databaseId",
"collectionId",
"documents"
],
"required": [
"databaseId",
"collectionId",
"documents"
],
"responses": [
@@ -5980,7 +5984,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -6064,7 +6068,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
File diff suppressed because it is too large Load Diff
+113 -63
View File
@@ -1,7 +1,7 @@
{
"swagger": "2.0",
"info": {
"version": "1.6.0",
"version": "1.6.1",
"title": "Appwrite",
"description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)",
"termsOfService": "https:\/\/appwrite.io\/policy\/terms",
@@ -2501,7 +2501,7 @@
"tags": [
"account"
],
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"description": "Sends the user an email with a secret key for creating a session. If the provided user ID has not been registered, a new user will be created. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [POST \/v1\/account\/sessions\/token](https:\/\/appwrite.io\/docs\/references\/cloud\/client-web\/account#createSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour.\n\nA user is limited to 10 active sessions at a time by default. [Learn more about session limits](https:\/\/appwrite.io\/docs\/authentication-security#limits).\n",
"responses": {
"201": {
"description": "Token",
@@ -7583,9 +7583,13 @@
{
"name": "createDocuments",
"parameters": [
"databaseId",
"collectionId",
"documents"
],
"required": [
"databaseId",
"collectionId",
"documents"
],
"responses": [
@@ -9666,7 +9670,7 @@
"tags": [
"functions"
],
"description": "",
"description": "Create a new build for an existing function deployment. This endpoint allows you to rebuild a deployment with the updated function configuration, including its entrypoint and build commands if they have been modified The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.",
"responses": {
"204": {
"description": "No content"
@@ -9679,7 +9683,7 @@
"type": "",
"deprecated": false,
"demo": "functions\/create-build.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/functions\/create-deployment-build.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/functions\/create-build.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -9745,7 +9749,7 @@
"tags": [
"functions"
],
"description": "",
"description": "Cancel an ongoing function deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.",
"responses": {
"200": {
"description": "Build",
@@ -12550,7 +12554,7 @@
},
"x-appwrite": {
"method": "listMessages",
"weight": 392,
"weight": 386,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12625,7 +12629,7 @@
},
"x-appwrite": {
"method": "createEmail",
"weight": 389,
"weight": 383,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12783,7 +12787,7 @@
},
"x-appwrite": {
"method": "updateEmail",
"weight": 396,
"weight": 390,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12938,7 +12942,7 @@
},
"x-appwrite": {
"method": "createPush",
"weight": 391,
"weight": 385,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12980,13 +12984,13 @@
"title": {
"type": "string",
"description": "Title for push notification.",
"default": null,
"default": "",
"x-example": "<TITLE>"
},
"body": {
"type": "string",
"description": "Body for push notification.",
"default": null,
"default": "",
"x-example": "<BODY>"
},
"topics": {
@@ -13018,7 +13022,7 @@
},
"data": {
"type": "object",
"description": "Additional Data for push notification.",
"description": "Additional key-value pair data for push notification.",
"default": {},
"x-example": "{}"
},
@@ -13042,7 +13046,7 @@
},
"sound": {
"type": "string",
"description": "Sound for push notification. Available only for Android and IOS Platform.",
"description": "Sound for push notification. Available only for Android and iOS Platform.",
"default": "",
"x-example": "<SOUND>"
},
@@ -13059,10 +13063,10 @@
"x-example": "<TAG>"
},
"badge": {
"type": "string",
"description": "Badge for push notification. Available only for IOS Platform.",
"default": "",
"x-example": "<BADGE>"
"type": "integer",
"description": "Badge for push notification. Available only for iOS Platform.",
"default": -1,
"x-example": null
},
"draft": {
"type": "boolean",
@@ -13075,12 +13079,34 @@
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"default": null,
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"default": false,
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"default": false,
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device state and may not deliver notifications immediately. \"high\" will always attempt to immediately deliver the notification.",
"default": "high",
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
},
"required": [
"messageId",
"title",
"body"
"messageId"
]
}
}
@@ -13111,7 +13137,7 @@
},
"x-appwrite": {
"method": "updatePush",
"weight": 398,
"weight": 392,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13250,6 +13276,30 @@
"description": "Scheduled delivery time for message in [ISO 8601](https:\/\/www.iso.org\/iso-8601-date-and-time-format.html) format. DateTime value must be in future.",
"default": null,
"x-example": null
},
"contentAvailable": {
"type": "boolean",
"description": "If set to true, the notification will be delivered in the background. Available only for iOS Platform.",
"default": null,
"x-example": false
},
"critical": {
"type": "boolean",
"description": "If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.",
"default": null,
"x-example": false
},
"priority": {
"type": "string",
"description": "Set the notification priority. \"normal\" will consider device battery state and may send notifications later. \"high\" will always attempt to immediately deliver the notification.",
"default": null,
"x-example": "normal",
"enum": [
"normal",
"high"
],
"x-enum-name": "MessagePriority",
"x-enum-keys": []
}
}
}
@@ -13281,7 +13331,7 @@
},
"x-appwrite": {
"method": "createSms",
"weight": 390,
"weight": 384,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13399,7 +13449,7 @@
},
"x-appwrite": {
"method": "updateSms",
"weight": 397,
"weight": 391,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13515,7 +13565,7 @@
},
"x-appwrite": {
"method": "getMessage",
"weight": 395,
"weight": 389,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13570,7 +13620,7 @@
},
"x-appwrite": {
"method": "delete",
"weight": 399,
"weight": 393,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13632,7 +13682,7 @@
},
"x-appwrite": {
"method": "listMessageLogs",
"weight": 393,
"weight": 387,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13706,7 +13756,7 @@
},
"x-appwrite": {
"method": "listTargets",
"weight": 394,
"weight": 388,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13780,7 +13830,7 @@
},
"x-appwrite": {
"method": "listProviders",
"weight": 364,
"weight": 358,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13855,7 +13905,7 @@
},
"x-appwrite": {
"method": "createApnsProvider",
"weight": 363,
"weight": 357,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13970,7 +14020,7 @@
},
"x-appwrite": {
"method": "updateApnsProvider",
"weight": 376,
"weight": 370,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14083,7 +14133,7 @@
},
"x-appwrite": {
"method": "createFcmProvider",
"weight": 362,
"weight": 356,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14174,7 +14224,7 @@
},
"x-appwrite": {
"method": "updateFcmProvider",
"weight": 375,
"weight": 369,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14263,7 +14313,7 @@
},
"x-appwrite": {
"method": "createMailgunProvider",
"weight": 354,
"weight": 348,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14390,7 +14440,7 @@
},
"x-appwrite": {
"method": "updateMailgunProvider",
"weight": 367,
"weight": 361,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14515,7 +14565,7 @@
},
"x-appwrite": {
"method": "createMsg91Provider",
"weight": 357,
"weight": 351,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14618,7 +14668,7 @@
},
"x-appwrite": {
"method": "updateMsg91Provider",
"weight": 370,
"weight": 364,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14719,7 +14769,7 @@
},
"x-appwrite": {
"method": "createSendgridProvider",
"weight": 355,
"weight": 349,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14834,7 +14884,7 @@
},
"x-appwrite": {
"method": "updateSendgridProvider",
"weight": 368,
"weight": 362,
"cookies": false,
"type": "",
"deprecated": false,
@@ -14947,7 +14997,7 @@
},
"x-appwrite": {
"method": "createSmtpProvider",
"weight": 356,
"weight": 350,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15106,7 +15156,7 @@
},
"x-appwrite": {
"method": "updateSmtpProvider",
"weight": 369,
"weight": 363,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15262,7 +15312,7 @@
},
"x-appwrite": {
"method": "createTelesignProvider",
"weight": 358,
"weight": 352,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15365,7 +15415,7 @@
},
"x-appwrite": {
"method": "updateTelesignProvider",
"weight": 371,
"weight": 365,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15466,7 +15516,7 @@
},
"x-appwrite": {
"method": "createTextmagicProvider",
"weight": 359,
"weight": 353,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15569,7 +15619,7 @@
},
"x-appwrite": {
"method": "updateTextmagicProvider",
"weight": 372,
"weight": 366,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15670,7 +15720,7 @@
},
"x-appwrite": {
"method": "createTwilioProvider",
"weight": 360,
"weight": 354,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15773,7 +15823,7 @@
},
"x-appwrite": {
"method": "updateTwilioProvider",
"weight": 373,
"weight": 367,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15874,7 +15924,7 @@
},
"x-appwrite": {
"method": "createVonageProvider",
"weight": 361,
"weight": 355,
"cookies": false,
"type": "",
"deprecated": false,
@@ -15977,7 +16027,7 @@
},
"x-appwrite": {
"method": "updateVonageProvider",
"weight": 374,
"weight": 368,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16078,7 +16128,7 @@
},
"x-appwrite": {
"method": "getProvider",
"weight": 366,
"weight": 360,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16133,7 +16183,7 @@
},
"x-appwrite": {
"method": "deleteProvider",
"weight": 377,
"weight": 371,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16195,7 +16245,7 @@
},
"x-appwrite": {
"method": "listProviderLogs",
"weight": 365,
"weight": 359,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16269,7 +16319,7 @@
},
"x-appwrite": {
"method": "listSubscriberLogs",
"weight": 386,
"weight": 380,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16343,7 +16393,7 @@
},
"x-appwrite": {
"method": "listTopics",
"weight": 379,
"weight": 373,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16416,7 +16466,7 @@
},
"x-appwrite": {
"method": "createTopic",
"weight": 378,
"weight": 372,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16506,7 +16556,7 @@
},
"x-appwrite": {
"method": "getTopic",
"weight": 381,
"weight": 375,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16566,7 +16616,7 @@
},
"x-appwrite": {
"method": "updateTopic",
"weight": 382,
"weight": 376,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16645,7 +16695,7 @@
},
"x-appwrite": {
"method": "deleteTopic",
"weight": 383,
"weight": 377,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16707,7 +16757,7 @@
},
"x-appwrite": {
"method": "listTopicLogs",
"weight": 380,
"weight": 374,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16781,7 +16831,7 @@
},
"x-appwrite": {
"method": "listSubscribers",
"weight": 385,
"weight": 379,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16862,7 +16912,7 @@
},
"x-appwrite": {
"method": "createSubscriber",
"weight": 384,
"weight": 378,
"cookies": false,
"type": "",
"deprecated": false,
@@ -16953,7 +17003,7 @@
},
"x-appwrite": {
"method": "getSubscriber",
"weight": 387,
"weight": 381,
"cookies": false,
"type": "",
"deprecated": false,
@@ -17016,7 +17066,7 @@
},
"x-appwrite": {
"method": "deleteSubscriber",
"weight": 388,
"weight": 382,
"cookies": false,
"type": "",
"deprecated": false,
@@ -25674,7 +25724,7 @@
},
"schedule": {
"type": "string",
"description": "Function execution schedult in CRON format.",
"description": "Function execution schedule in CRON format.",
"x-example": "5 4 * * *"
},
"timeout": {
+18
View File
@@ -268,6 +268,24 @@ return [
'question' => '',
'filter' => ''
],
[
'name' => '_APP_COMPRESSION_ENABLED',
'description' => 'This option allows you to enable or disable the response compression for the Appwrite API. It\'s enabled by default with value "enabled", and to disable it, pass value "disabled".',
'introduction' => '1.6.0',
'default' => 'enabled',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_COMPRESSION_MIN_SIZE_BYTES',
'description' => 'This option allows you to set the minimum size in bytes for the response compression to be applied. The default value is 1024 bytes.',
'introduction' => '1.6.0',
'default' => 1024,
'required' => false,
'question' => '',
'filter' => ''
]
],
],
[
+82 -6
View File
@@ -17,6 +17,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
@@ -32,7 +33,9 @@ use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Identities;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use libphonenumber\PhoneNumberUtil;
use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit as EventAudit;
use Utopia\Config\Config;
@@ -280,7 +283,7 @@ App::post('/v1/account')
->desc('Create account')
->groups(['api', 'account', 'auth'])
->label('scope', 'sessions.write')
->label('auth.type', 'emailPassword')
->label('auth.type', 'email-password')
->label('audits.event', 'user.create')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
@@ -847,7 +850,7 @@ App::post('/v1/account/sessions/email')
->groups(['api', 'account', 'auth', 'session'])
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'sessions.write')
->label('auth.type', 'emailPassword')
->label('auth.type', 'email-password')
->label('audits.event', 'session.create')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
@@ -2429,7 +2432,10 @@ App::post('/v1/account/tokens/phone')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('locale')
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) {
->inject('timelimit')
->inject('queueForUsage')
->inject('plan')
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, Usage $queueForUsage, array $plan) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -2566,6 +2572,27 @@ App::post('/v1/account/tokens/phone')
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS);
if (isset($plan['authPhone'])) {
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
$timelimit
->setParam('{organizationId}', $project->getAttribute('teamId'));
$abuse = new Abuse($timelimit);
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$helper = PhoneNumberUtil::getInstance();
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
}
$queueForUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
}
// Set to unhashed secret for events and server responses
@@ -3651,7 +3678,10 @@ App::post('/v1/account/verification/phone')
->inject('queueForMessaging')
->inject('project')
->inject('locale')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) {
->inject('timelimit')
->inject('queueForUsage')
->inject('plan')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, Usage $queueForUsage, array $plan) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -3734,6 +3764,27 @@ App::post('/v1/account/verification/phone')
->setMessage($messageDoc)
->setRecipients([$user->getAttribute('phone')])
->setProviderType(MESSAGE_TYPE_SMS);
if (isset($plan['authPhone'])) {
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
$timelimit
->setParam('{organizationId}', $project->getAttribute('teamId'));
$abuse = new Abuse($timelimit);
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$helper = PhoneNumberUtil::getInstance();
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
}
$queueForUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
}
// Set to unhashed secret for events and server responses
@@ -4259,7 +4310,10 @@ App::post('/v1/account/mfa/challenge')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForMails')
->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Locale $locale, Document $project, Request $request, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails) {
->inject('timelimit')
->inject('queueForUsage')
->inject('plan')
->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Locale $locale, Document $project, Request $request, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails, callable $timelimit, Usage $queueForUsage, array $plan) {
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
$code = Auth::codeGenerator();
@@ -4307,6 +4361,7 @@ App::post('/v1/account/mfa/challenge')
$message = $message->render();
$phone = $user->getAttribute('phone');
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage(new Document([
@@ -4315,8 +4370,29 @@ App::post('/v1/account/mfa/challenge')
'content' => $code,
],
]))
->setRecipients([$user->getAttribute('phone')])
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS);
if (isset($plan['authPhone'])) {
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
$timelimit
->setParam('{organizationId}', $project->getAttribute('teamId'));
$abuse = new Abuse($timelimit);
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$helper = PhoneNumberUtil::getInstance();
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
}
$queueForUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
break;
case Type::EMAIL:
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
+15 -15
View File
@@ -66,9 +66,9 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
unset($image);
};
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForConsole, ?Logger $logger) {
$getUserGitHub = function (string $userId, Document $project, Database $dbForProject, Database $dbForPlatform, ?Logger $logger) {
try {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
@@ -127,7 +127,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
do {
$previousAccessToken = $gitHubSession->getAttribute('providerAccessToken');
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
$sessions = $user->getAttribute('sessions', []);
$gitHubSession = new Document();
@@ -619,14 +619,14 @@ App::get('/v1/cards/cloud')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -637,7 +637,7 @@ App::get('/v1/cards/cloud')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
@@ -826,14 +826,14 @@ App::get('/v1/cards/cloud-back')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -843,7 +843,7 @@ App::get('/v1/cards/cloud-back')
$userId = $user->getId();
$email = $user->getAttribute('email', '');
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$githubId = $gitHub['id'] ?? '';
$isHero = \array_key_exists($email, $heroes);
@@ -904,14 +904,14 @@ App::get('/v1/cards/cloud-og')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('response')
->inject('heroes')
->inject('contributors')
->inject('employees')
->inject('logger')
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForConsole, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForConsole->getDocument('users', $userId));
->action(function (string $userId, string $mock, int $width, int $height, Document $user, Document $project, Database $dbForProject, Database $dbForPlatform, Response $response, array $heroes, array $contributors, array $employees, ?Logger $logger) use ($getUserGitHub) {
$user = Authorization::skip(fn () => $dbForPlatform->getDocument('users', $userId));
if ($user->isEmpty() && empty($mock)) {
throw new Exception(Exception::USER_NOT_FOUND);
@@ -926,7 +926,7 @@ App::get('/v1/cards/cloud-og')
$email = $user->getAttribute('email', '');
$createdAt = new \DateTime($user->getCreatedAt());
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForConsole, $logger);
$gitHub = $getUserGitHub($user->getId(), $project, $dbForProject, $dbForPlatform, $logger);
$githubName = $gitHub['name'] ?? '';
$githubId = $gitHub['id'] ?? '';
+1 -1
View File
@@ -88,7 +88,7 @@ App::post('/v1/console/assistant')
->param('prompt', '', new Text(2000), 'Prompt. A string containing questions asked to the AI assistant.')
->inject('response')
->action(function (string $prompt, Response $response) {
$ch = curl_init('http://appwrite-assistant:3003/');
$ch = curl_init('http://appwrite-assistant:3003/v1/models/assistant/prompt');
$responseHeaders = [];
$query = json_encode(['prompt' => $prompt]);
$headers = ['accept: text/event-stream'];
+112 -33
View File
@@ -24,7 +24,6 @@ use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
@@ -41,6 +40,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Index as IndexValidator;
use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\Queries;
@@ -158,7 +158,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
} catch (DuplicateException) {
throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS);
} catch (LimitException) {
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
} catch (\Throwable $e) {
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId());
@@ -202,7 +202,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS);
} catch (LimitException) {
$dbForProject->deleteDocument('attributes', $attribute->getId());
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED, 'Attribute limit exceeded');
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
} catch (\Throwable $e) {
$dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId());
$dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
@@ -296,15 +296,9 @@ function updateAttribute(
$attribute->setAttribute('size', $size);
}
$formatOptions = $attribute->getAttribute('formatOptions');
switch ($attribute->getAttribute('format')) {
case APP_DATABASE_ATTRIBUTE_INT_RANGE:
case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE:
if ($min === $formatOptions['min'] && $max === $formatOptions['max']) {
break;
}
if ($min > $max) {
throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value');
}
@@ -391,30 +385,42 @@ function updateAttribute(
size: $size,
required: $required,
default: $default,
formatOptions: $options ?? null,
formatOptions: $options,
newKey: $newKey ?? null
);
} catch (TruncateException) {
throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE);
} catch (NotFoundException) {
throw new Exception(Exception::ATTRIBUTE_NOT_FOUND);
} catch (LimitException) {
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
}
}
if (!empty($newKey) && $key !== $newKey) {
// Delete attribute and recreate since we can't modify IDs
$original = clone $attribute;
$dbForProject->deleteDocument('attributes', $attribute->getId());
$originalUid = $attribute->getId();
$attribute
->setAttribute('$id', ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $newKey))
->setAttribute('key', $newKey);
try {
$attribute = $dbForProject->createDocument('attributes', $attribute);
} catch (DatabaseException|PDOException) {
$attribute = $dbForProject->createDocument('attributes', $original);
$dbForProject->updateDocument('attributes', $originalUid, $attribute);
/**
* @var Document $index
*/
foreach ($collection->getAttribute('indexes') as $index) {
/**
* @var string[] $attributes
*/
$attributes = $index->getAttribute('attributes', []);
$found = \array_search($key, $attributes);
if ($found !== false) {
$attributes[$found] = $newKey;
$index->setAttribute('attributes', $attributes);
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
}
} else {
$attribute = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute);
@@ -2633,6 +2639,18 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
throw new Exception(Exception::ATTRIBUTE_NOT_FOUND);
}
/**
* Check index dependency
*/
$validator = new IndexDependencyValidator(
$collection->getAttribute('indexes'),
$dbForProject->getAdapter()->getSupportForCastIndexArray(),
);
if (! $validator->isValid($attribute)) {
throw new Exception(Exception::INDEX_DEPENDENCY);
}
// Only update status if removing available attribute
if ($attribute->getAttribute('status') === 'available') {
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
@@ -2810,7 +2828,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
$attributeType = $oldAttributes[$attributeIndex]['type'];
$attributeSize = $oldAttributes[$attributeIndex]['size'];
$attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false;
if ($attributeType === Database::VAR_RELATIONSHIP) {
@@ -2824,10 +2841,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$lengths[$i] = null;
if ($attributeType === Database::VAR_STRING) {
$lengths[$i] = $attributeSize; // set attribute size as index length only for strings
}
if ($attributeArray === true) {
$lengths[$i] = Database::ARRAY_INDEX_LENGTH;
$orders[$i] = null;
@@ -2850,7 +2863,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$validator = new IndexValidator(
$collection->getAttribute('attributes'),
$dbForProject->getAdapter()->getMaxIndexLength()
$dbForProject->getAdapter()->getMaxIndexLength(),
$dbForProject->getAdapter()->getInternalIndexesKeys(),
);
if (!$validator->isValid($index)) {
throw new Exception(Exception::INDEX_INVALID, $validator->getDescription());
@@ -3236,7 +3250,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$document->setAttribute('$permissions', $permissions);
};
$checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database) {
$operations = 0;
$checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, &$operations) {
$operations++;
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization($permission);
@@ -3391,6 +3409,13 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$processDocument($collection, $document);
}
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations)
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
$response->addHeader('X-Debug-Operations', $operations);
if ($isBulk) {
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -3437,7 +3462,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode, Usage $queueForUsage) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -3473,7 +3499,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$documentId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId));
@@ -3488,12 +3513,16 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$documents = $dbForProject->find('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries);
$total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, APP_LIMIT_COUNT);
$operations = 0;
// Add $collectionId and $databaseId for all documents
$processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database): bool {
$processDocument = (function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, &$operations): bool {
if ($document->isEmpty()) {
return false;
}
$operations++;
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
@@ -3506,8 +3535,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
if (\in_array(\gettype($related), ['array', 'object'])) {
$operations++;
}
continue;
}
if (!\is_array($related)) {
$relations = [$related];
} else {
@@ -3515,6 +3549,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
// todo: Use local cache for this getDocument
$relatedCollection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId));
foreach ($relations as $index => $doc) {
@@ -3539,6 +3574,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$processDocument($collection, $document);
}
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
$select = \array_reduce($queries, function ($result, $query) {
return $result || ($query->getMethod() === Query::TYPE_SELECT);
}, false);
@@ -3598,9 +3640,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
->inject('response')
->inject('dbForProject')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode, Usage $queueForUsage) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -3627,12 +3669,16 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
throw new Exception(Exception::DOCUMENT_NOT_FOUND);
}
$operations = 0;
// Add $collectionId and $databaseId for all documents
$processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) {
$processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database, &$operations) {
if ($document->isEmpty()) {
return;
}
$operations++;
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
@@ -3645,8 +3691,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
if (\in_array(\gettype($related), ['array', 'object'])) {
$operations++;
}
continue;
}
if (!\is_array($related)) {
$related = [$related];
}
@@ -3666,6 +3717,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$processDocument($collection, $document);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
$response->dynamic($document, Response::MODEL_DOCUMENT);
});
@@ -3774,6 +3832,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
}
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
@@ -3815,7 +3874,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->inject('dbForProject')
->inject('queueForEvents')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Usage $queueForUsage) {
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@@ -3882,7 +3942,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$data['$permissions'] = $permissions;
$newDocument = new Document($data);
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database) {
$operations = 0;
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) {
$operations++;
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
@@ -3950,6 +4015,13 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$setCollection($collection, $newDocument);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
try {
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,
@@ -4266,6 +4338,13 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
$processDocument($collection, $document);
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1)
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
$response->addHeader('X-Debug-Operations', 1);
$relationships = \array_map(
fn ($document) => $document->getAttribute('key'),
\array_filter(
+77 -43
View File
@@ -28,6 +28,7 @@ use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Rule;
use Executor\Executor;
use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
@@ -188,15 +189,45 @@ App::post('/v1/functions')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('timelimit')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, callable $timelimit, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github) use ($redeployVcs) {
$functionId = ($functionId == 'unique()') ? ID::unique() : $functionId;
// Temporary abuse check
$abuseCheck = function () use ($project, $timelimit, $response) {
$abuseKey = "projectId:{projectId},url:{url}";
$abuseLimit = App::getEnv('_APP_FUNCTIONS_CREATION_ABUSE_LIMIT', 50);
$abuseTime = 86400; // 1 day
$timeLimit = $timelimit($abuseKey, $abuseLimit, $abuseTime);
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{url}', '/v1/functions');
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = $timeLimit->time() + $abuseTime;
$response
->addHeader('X-RateLimit-Limit', $limit)
->addHeader('X-RateLimit-Remaining', $remaining)
->addHeader('X-RateLimit-Reset', $time);
$enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
if ($enabled && $abuse->check()) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
}
};
$abuseCheck();
$allowList = \array_filter(\explode(',', System::getEnv('_APP_FUNCTIONS_RUNTIMES', '')));
if (!empty($allowList) && !\in_array($runtime, $allowList)) {
@@ -217,7 +248,7 @@ App::post('/v1/functions')
->setAttribute('version', $templateVersion);
}
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -259,7 +290,7 @@ App::post('/v1/functions')
]));
$schedule = Authorization::skip(
fn () => $dbForConsole->createDocument('schedules', new Document([
fn () => $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'), // Todo replace with projects region
'resourceType' => 'function',
'resourceId' => $function->getId(),
@@ -278,7 +309,7 @@ App::post('/v1/functions')
if (!empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForConsole->createDocument('repositories', new Document([
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -338,10 +369,11 @@ App::post('/v1/functions')
if (!empty($functionsDomain)) {
$routeSubdomain = ID::unique();
$domain = "{$routeSubdomain}.{$functionsDomain}";
$ruleId = md5($domain);
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
$rule = Authorization::skip(
fn () => $dbForConsole->createDocument('rules', new Document([
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
@@ -378,6 +410,7 @@ App::post('/v1/functions')
$allEvents = Event::generateEvents('rules.[ruleId].create', [
'ruleId' => $rule->getId(),
]);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
@@ -850,9 +883,9 @@ App::put('/v1/functions/:functionId')
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('gitHub')
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForConsole, GitHub $github) use ($redeployVcs) {
->action(function (string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, ?string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github) use ($redeployVcs) {
// TODO: If only branch changes, re-deploy
$function = $dbForProject->getDocument('functions', $functionId);
@@ -860,7 +893,7 @@ App::put('/v1/functions/:functionId')
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
if (!empty($installationId) && $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -891,7 +924,7 @@ App::put('/v1/functions/:functionId')
// Git disconnect logic. Disconnecting only when providerRepositoryId is empty, allowing for continue updates without disconnecting git
if ($isConnected && ($providerRepositoryId !== null && empty($providerRepositoryId))) {
$repositories = $dbForConsole->find('repositories', [
$repositories = $dbForPlatform->find('repositories', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::equal('resourceInternalId', [$function->getInternalId()]),
Query::equal('resourceType', ['function']),
@@ -899,7 +932,7 @@ App::put('/v1/functions/:functionId')
]);
foreach ($repositories as $repository) {
$dbForConsole->deleteDocument('repositories', $repository->getId());
$dbForPlatform->deleteDocument('repositories', $repository->getId());
}
$providerRepositoryId = '';
@@ -915,7 +948,7 @@ App::put('/v1/functions/:functionId')
if (!$isConnected && !empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForConsole->createDocument('repositories', new Document([
$repository = $dbForPlatform->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -997,12 +1030,12 @@ App::put('/v1/functions/:functionId')
}
// Inform scheduler if function is still active
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents->setParam('functionId', $function->getId());
@@ -1127,8 +1160,8 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('dbForConsole')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
@@ -1156,12 +1189,12 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
])));
// Inform scheduler if function is still active
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents
->setParam('functionId', $function->getId())
@@ -1196,8 +1229,8 @@ App::delete('/v1/functions/:functionId')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('dbForConsole')
->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -1210,11 +1243,11 @@ App::delete('/v1/functions/:functionId')
}
// Inform scheduler to no longer run function
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
@@ -1672,7 +1705,7 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/build')
->label('sdk', new Method(
namespace: 'functions',
name: 'createBuild',
description: '/docs/references/functions/create-deployment-build.md',
description: '/docs/references/functions/create-build.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
@@ -1864,13 +1897,13 @@ App::post('/v1/functions/:functionId/executions')
->inject('request')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('user')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForConsole, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) {
->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) {
$async = \strval($async) === 'true' || \strval($async) === '1';
if (!$async && !is_null($scheduledAt)) {
@@ -2061,7 +2094,7 @@ App::post('/v1/functions/:functionId/executions')
'userId' => $user->getId()
];
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => ScheduleExecutions::getSupportedResource(),
'resourceId' => $execution->getId(),
@@ -2413,9 +2446,9 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
->param('executionId', '', new UID(), 'Execution ID.')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForConsole, Event $queueForEvents) {
->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -2441,7 +2474,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
}
if ($status === 'scheduled') {
$schedule = $dbForConsole->findOne('schedules', [
$schedule = $dbForPlatform->findOne('schedules', [
Query::equal('resourceId', [$execution->getId()]),
Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]),
Query::equal('active', [true]),
@@ -2452,7 +2485,7 @@ App::delete('/v1/functions/:functionId/executions/:executionId')
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
}
}
@@ -2490,8 +2523,8 @@ App::post('/v1/functions/:functionId/variables')
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $key, string $value, Response $response, Database $dbForProject, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -2524,12 +2557,12 @@ App::post('/v1/functions/:functionId/variables')
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -2540,6 +2573,7 @@ App::get('/v1/functions/:functionId/variables')
->desc('List variables')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label(
'sdk',
new Method(
@@ -2644,8 +2678,8 @@ App::put('/v1/functions/:functionId/variables/:variableId')
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -2676,12 +2710,12 @@ App::put('/v1/functions/:functionId/variables/:variableId')
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
@@ -2710,8 +2744,8 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForPlatform) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@@ -2732,12 +2766,12 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForConsole->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->noContent();
});
+88 -38
View File
@@ -2836,11 +2836,11 @@ App::post('/v1/messaging/messages/email')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -2929,7 +2929,7 @@ App::post('/v1/messaging/messages/email')
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -2989,11 +2989,11 @@ App::post('/v1/messaging/messages/sms')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $content, array $topics, array $users, array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $content, array $topics, array $users, array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3051,7 +3051,7 @@ App::post('/v1/messaging/messages/sms')
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3103,28 +3103,31 @@ App::post('/v1/messaging/messages/push')
]
))
->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('title', '', new Text(256), 'Title for push notification.')
->param('body', '', new Text(64230), 'Body for push notification.')
->param('title', '', new Text(256), 'Title for push notification.', true)
->param('body', '', new Text(64230), 'Body for push notification.', true)
->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true)
->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true)
->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true)
->param('data', null, new JSON(), 'Additional Data for push notification.', true)
->param('data', null, new JSON(), 'Additional key-value pair data for push notification.', true)
->param('action', '', new Text(256), 'Action for push notification.', true)
->param('image', '', new CompoundUID(), 'Image for push notification. Must be a compound bucket ID to file ID of a jpeg, png, or bmp image in Appwrite Storage. It should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->param('icon', '', new Text(256), 'Icon for push notification. Available only for Android and Web Platform.', true)
->param('sound', '', new Text(256), 'Sound for push notification. Available only for Android and IOS Platform.', true)
->param('sound', '', new Text(256), 'Sound for push notification. Available only for Android and iOS Platform.', true)
->param('color', '', new Text(256), 'Color for push notification. Available only for Android Platform.', true)
->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true)
->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true)
->param('badge', -1, new Integer(), 'Badge for push notification. Available only for iOS Platform.', true)
->param('draft', false, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', false, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', false, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, string $badge, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3207,12 +3210,44 @@ App::post('/v1/messaging/messages/push')
$pushData = [];
$keys = ['title', 'body', 'data', 'action', 'image', 'icon', 'sound', 'color', 'tag', 'badge'];
foreach ($keys as $key) {
if (!empty($$key)) {
$pushData[$key] = $$key;
}
if (!empty($title)) {
$pushData['title'] = $title;
}
if (!empty($body)) {
$pushData['body'] = $body;
}
if (!empty($data)) {
$pushData['data'] = $data;
}
if (!empty($action)) {
$pushData['action'] = $action;
}
if (!empty($image)) {
$pushData['image'] = $image;
}
if (!empty($icon)) {
$pushData['icon'] = $icon;
}
if (!empty($sound)) {
$pushData['sound'] = $sound;
}
if (!empty($color)) {
$pushData['color'] = $color;
}
if (!empty($tag)) {
$pushData['tag'] = $tag;
}
if ($badge >= 0) {
$pushData['badge'] = $badge;
}
if ($contentAvailable) {
$pushData['contentAvailable'] = true;
}
if ($critical) {
$pushData['critical'] = true;
}
if (!empty($priority)) {
$pushData['priority'] = $priority;
}
$message = $dbForProject->createDocument('messages', new Document([
@@ -3233,7 +3268,7 @@ App::post('/v1/messaging/messages/push')
->setMessageId($message->getId());
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3561,11 +3596,11 @@ App::patch('/v1/messaging/messages/email/:messageId')
->param('attachments', null, new ArrayList(new CompoundUID()), 'Array of compound ID strings of bucket IDs and file IDs to be attached to the email. They should be formatted as <BUCKET_ID>:<FILE_ID>.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3617,7 +3652,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
}
if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) {
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3632,7 +3667,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
}
if (!\is_null($currentScheduledAt)) {
$schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $message->getAttribute('scheduleId'));
$scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED;
if ($schedule->isEmpty()) {
@@ -3647,7 +3682,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
$schedule->setAttribute('schedule', $scheduledAt);
}
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
}
if (!\is_null($scheduledAt)) {
@@ -3762,11 +3797,11 @@ App::patch('/v1/messaging/messages/sms/:messageId')
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3818,7 +3853,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
}
if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) {
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -3833,7 +3868,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
}
if (!\is_null($currentScheduledAt)) {
$schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $message->getAttribute('scheduleId'));
$scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED;
if ($schedule->isEmpty()) {
@@ -3848,7 +3883,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
$schedule->setAttribute('schedule', $scheduledAt);
}
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
}
if (!\is_null($scheduledAt)) {
@@ -3930,13 +3965,16 @@ App::patch('/v1/messaging/messages/push/:messageId')
->param('badge', null, new Integer(), 'Badge for push notification. Available only for iOS platforms.', true)
->param('draft', null, new Boolean(), 'Is message a draft', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) {
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -3988,7 +4026,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
}
if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) {
$schedule = $dbForConsole->createDocument('schedules', new Document([
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => 'message',
'resourceId' => $message->getId(),
@@ -4003,7 +4041,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
}
if (!\is_null($currentScheduledAt)) {
$schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId'));
$schedule = $dbForPlatform->getDocument('schedules', $message->getAttribute('scheduleId'));
$scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED;
if ($schedule->isEmpty()) {
@@ -4018,7 +4056,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
$schedule->setAttribute('schedule', $scheduledAt);
}
$dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule);
$dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule);
}
if (!\is_null($scheduledAt)) {
@@ -4075,6 +4113,18 @@ App::patch('/v1/messaging/messages/push/:messageId')
$pushData['badge'] = $badge;
}
if (!\is_null($contentAvailable)) {
$pushData['contentAvailable'] = $contentAvailable;
}
if (!\is_null($critical)) {
$pushData['critical'] = $critical;
}
if (!\is_null($priority)) {
$pushData['priority'] = $priority;
}
if (!\is_null($image)) {
[$bucketId, $fileId] = CompoundUID::parse($image);
@@ -4161,10 +4211,10 @@ App::delete('/v1/messaging/messages/:messageId')
))
->param('messageId', '', new UID(), 'Message ID.')
->inject('dbForProject')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('response')
->action(function (string $messageId, Database $dbForProject, Database $dbForConsole, Event $queueForEvents, Response $response) {
->action(function (string $messageId, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4187,7 +4237,7 @@ App::delete('/v1/messaging/messages/:messageId')
if (!empty($scheduleId)) {
try {
$dbForConsole->deleteDocument('schedules', $scheduleId);
$dbForPlatform->deleteDocument('schedules', $scheduleId);
} catch (Exception) {
// Ignore
}
+1 -500
View File
@@ -1,22 +1,16 @@
<?php
use Appwrite\Auth\OAuth2\Firebase as OAuth2Firebase;
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Extend\Exception;
use Appwrite\Permission;
use Appwrite\Role;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Migrations;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
@@ -27,9 +21,7 @@ use Utopia\Migration\Sources\Appwrite;
use Utopia\Migration\Sources\Firebase;
use Utopia\Migration\Sources\NHost;
use Utopia\Migration\Sources\Supabase;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Host;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -97,117 +89,10 @@ App::post('/v1/migrations/appwrite')
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase/oauth')
->groups(['api', 'migrations'])
->desc('Migrate Firebase data (OAuth)')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
name: 'createFirebaseOAuthMigration',
description: '/docs/references/migrations/migration-firebase.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('projectId', '', new Text(65536), 'Project ID of the Firebase Project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('request')
->action(function (array $resources, string $projectId, Response $response, Database $dbForProject, Database $dbForConsole, Document $project, Document $user, Event $queueForEvents, Migration $queueForMigrations, Request $request) {
$firebase = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$firebase->refreshTokens($refreshToken);
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
if ($identity->getAttribute('secrets')) {
$serviceAccount = $identity->getAttribute('secrets');
} else {
$firebase->cleanupServiceAccounts($accessToken, $projectId);
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
$identity = $identity
->setAttribute('secrets', json_encode($serviceAccount));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Firebase::getName(),
'destination' => Appwrite::getName(),
'credentials' => [
'serviceAccount' => json_encode($serviceAccount),
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => []
]));
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
App::post('/v1/migrations/firebase')
->groups(['api', 'migrations'])
->desc('Migrate Firebase data (Service Account)')
->desc('Migrate Firebase data')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
@@ -597,390 +482,6 @@ App::get('/v1/migrations/firebase/report')
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/firebase/report/oauth')
->groups(['api', 'migrations'])
->desc('Generate a report on Firebase data using OAuth')
->label('scope', 'migrations.write')
->label('sdk', new Method(
namespace: 'migrations',
name: 'getFirebaseReportOAuth',
description: '/docs/references/migrations/migration-firebase-report.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_REPORT,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('projectId', '', new Text(65536), 'Project ID')
->inject('response')
->inject('request')
->inject('user')
->inject('dbForConsole')
->action(function (array $resources, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
$firebase = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
if (System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$firebase->refreshTokens($refreshToken);
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
// Get Service Account
if ($identity->getAttribute('secrets')) {
$serviceAccount = $identity->getAttribute('secrets');
} else {
$firebase->cleanupServiceAccounts($accessToken, $projectId);
$serviceAccount = $firebase->createServiceAccount($accessToken, $projectId);
$identity = $identity
->setAttribute('secrets', json_encode($serviceAccount));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
$firebase = new Firebase($serviceAccount);
try {
$report = $firebase->report($resources);
} catch (\Throwable $e) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Source Error: ' . $e->getMessage());
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
App::get('/v1/migrations/firebase/connect')
->desc('Authorize with Firebase')
->groups(['api', 'migrations'])
->label('scope', 'migrations.write')
->label('sdk', new Method(
namespace: 'migrations',
name: 'createFirebaseAuth',
description: '',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_MOVED_PERMANENTLY,
model: Response::MODEL_NONE,
)
],
hide: true,
type: MethodType::WEBAUTH
))
->param('redirect', '', fn ($clients) => new Host($clients), 'URL to redirect back to your Firebase authorization. Only console hostnames are allowed.', true, ['clients'])
->param('projectId', '', new UID(), 'Project ID')
->inject('response')
->inject('request')
->inject('user')
->inject('dbForConsole')
->action(function (string $redirect, string $projectId, Response $response, Request $request, Document $user, Database $dbForConsole) {
$state = \json_encode([
'projectId' => $projectId,
'redirect' => $redirect,
]);
$prefs = $user->getAttribute('prefs', []);
$prefs['migrationState'] = $state;
$user->setAttribute('prefs', $prefs);
$dbForConsole->updateDocument('users', $user->getId(), $user);
$oauth2 = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$url = $oauth2->getLoginURL();
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url);
});
App::get('/v1/migrations/firebase/redirect')
->desc('Capture and receive data on Firebase authorization')
->groups(['api', 'migrations'])
->label('scope', 'public')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->param('code', '', new Text(2048), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->inject('user')
->inject('project')
->inject('request')
->inject('response')
->inject('dbForConsole')
->action(function (string $code, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) {
$state = $user['prefs']['migrationState'] ?? '{}';
$prefs['migrationState'] = '';
$user->setAttribute('prefs', $prefs);
$dbForConsole->updateDocument('users', $user->getId(), $user);
if (empty($state)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Installation requests from organisation members for the Appwrite Google App are currently unsupported.');
}
$state = \json_decode($state, true);
$redirect = $state['redirect'] ?? '';
$projectId = $state['projectId'] ?? '';
$project = $dbForConsole->getDocument('projects', $projectId);
if (empty($redirect)) {
$redirect = $request->getProtocol() . '://' . $request->getHostname() . '/console/project-$projectId/settings/migrations';
}
if ($project->isEmpty()) {
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirect);
return;
}
// OAuth Authroization
if (!empty($code)) {
$oauth2 = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$accessToken = $oauth2->getAccessToken($code);
$refreshToken = $oauth2->getRefreshToken($code);
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
$email = $oauth2->getUserEmail($accessToken);
$oauth2ID = $oauth2->getUserID($accessToken);
if (empty($accessToken)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token.');
}
if (empty($refreshToken)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get refresh token.');
}
if (empty($accessTokenExpiry)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to get access token expiry.');
}
// Makes sure this email is not already used in another identity
$identity = $dbForConsole->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if (!$identity->isEmpty()) {
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
}
if (!$identity->isEmpty()) {
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
} else {
$identity = $dbForConsole->createDocument('identities', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userInternalId' => $user->getInternalId(),
'userId' => $user->getId(),
'provider' => 'firebase',
'providerUid' => $oauth2ID,
'providerEmail' => $email,
'providerAccessToken' => $accessToken,
'providerRefreshToken' => $refreshToken,
'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry),
]));
}
} else {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Missing OAuth2 code.');
}
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirect);
});
App::get('/v1/migrations/firebase/projects')
->desc('List Firebase projects')
->groups(['api', 'migrations'])
->label('scope', 'migrations.read')
->label('sdk', new Method(
namespace: 'migrations',
name: 'listFirebaseProjects',
description: '/docs/references/migrations/migration-firebase-projects.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST,
)
]
))
->inject('user')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('request')
->action(function (Document $user, Response $response, Document $project, Database $dbForConsole, Request $request) {
$firebase = new OAuth2Firebase(
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', ''),
System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', ''),
$request->getProtocol() . '://' . $request->getHostname() . '/v1/migrations/firebase/redirect'
);
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
if (System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_ID', '') === '' || System::getEnv('_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET', '') === '') {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
try {
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
try {
$firebase->refreshTokens($refreshToken);
} catch (\Throwable $e) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $firebase->getAccessToken('');
$refreshToken = $firebase->getRefreshToken('');
$verificationId = $firebase->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Another request is currently refreshing OAuth token. Please try again.');
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$firebase->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
}
$projects = $firebase->getProjects($accessToken);
$output = [];
foreach ($projects as $project) {
$output[] = [
'displayName' => $project['displayName'],
'projectId' => $project['projectId'],
];
}
} catch (\Throwable $e) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$response->dynamic(new Document([
'projects' => $output,
'total' => count($output),
]), Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST);
});
App::get('/v1/migrations/firebase/deauthorize')
->desc('Revoke Appwrite\'s authorization to access Firebase projects')
->groups(['api', 'migrations'])
->label('scope', 'migrations.write')
->label('sdk', new Method(
namespace: 'migrations',
name: 'deleteFirebaseAuth',
description: '/docs/references/migrations/migration-firebase-deauthorize.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
]
))
->inject('user')
->inject('response')
->inject('dbForConsole')
->action(function (Document $user, Response $response, Database $dbForConsole) {
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['firebase']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Not authenticated with Firebase'); //TODO: Replace with USER_IDENTITY_NOT_FOUND
}
$dbForConsole->deleteDocument('identities', $identity->getId());
$response->noContent();
});
App::get('/v1/migrations/supabase/report')
->groups(['api', 'migrations'])
->desc('Generate a report on Supabase Data')
+49 -5
View File
@@ -41,7 +41,8 @@ App::get('/v1/project/usage')
->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true)
->inject('response')
->inject('dbForProject')
->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject) {
->inject('smsRates')
->action(function (string $startDate, string $endDate, string $period, Response $response, Database $dbForProject, array $smsRates) {
$stats = $total = $usage = [];
$format = 'Y-m-d 00:00:00';
$firstDay = (new DateTime($startDate))->format($format);
@@ -267,6 +268,46 @@ App::get('/v1/project/usage')
];
}, $dbForProject->find('functions'));
// This total is includes free and paid SMS usage
$authPhoneTotal = Authorization::skip(fn () => $dbForProject->sum('stats', 'value', [
Query::equal('metric', [METRIC_AUTH_METHOD_PHONE]),
Query::equal('period', ['1d']),
Query::greaterThanEqual('time', $firstDay),
Query::lessThan('time', $lastDay),
]));
// This estimate is only for paid SMS usage
$authPhoneMetrics = Authorization::skip(fn () => $dbForProject->find('stats', [
Query::startsWith('metric', METRIC_AUTH_METHOD_PHONE . '.'),
Query::equal('period', ['1d']),
Query::greaterThanEqual('time', $firstDay),
Query::lessThan('time', $lastDay),
]));
$authPhoneEstimate = 0.0;
$authPhoneCountryBreakdown = [];
foreach ($authPhoneMetrics as $metric) {
$parts = explode('.', $metric->getAttribute('metric'));
$countryCode = $parts[3] ?? null;
if ($countryCode === null) {
continue;
}
$value = $metric->getAttribute('value', 0);
if (isset($smsRates[$countryCode])) {
$authPhoneEstimate += $value * $smsRates[$countryCode];
}
$authPhoneCountryBreakdown[] = [
'name' => $countryCode,
'value' => $value,
'estimate' => isset($smsRates[$countryCode])
? $value * $smsRates[$countryCode]
: 0.0,
];
}
// merge network inbound + outbound
$projectBandwidth = [];
foreach ($usage[METRIC_NETWORK_INBOUND] as $item) {
@@ -313,6 +354,9 @@ App::get('/v1/project/usage')
'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown,
'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown,
'functionsStorageBreakdown' => $functionsStorageBreakdown,
'authPhoneTotal' => $authPhoneTotal,
'authPhoneEstimate' => $authPhoneEstimate,
'authPhoneCountryBreakdown' => $authPhoneCountryBreakdown,
]), Response::MODEL_USAGE_PROJECT);
});
@@ -340,8 +384,8 @@ App::post('/v1/project/variables')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $key, string $value, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variableId = ID::unique();
$variable = new Document([
@@ -459,8 +503,8 @@ App::put('/v1/project/variables/:variableId')
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
File diff suppressed because it is too large Load Diff
+36 -26
View File
@@ -15,6 +15,7 @@ use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
@@ -51,9 +52,9 @@ App::post('/v1/proxy/rules')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('dbForProject')
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForConsole, Database $dbForProject) {
->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) {
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($domain === $mainDomain) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.');
@@ -68,8 +69,15 @@ App::post('/v1/proxy/rules')
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
$ruleId = md5($domain);
$document = $dbForConsole->getDocument('rules', $ruleId);
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$document = $dbForPlatform->getDocument('rules', md5($domain));
} else {
$document = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
}
if (!$document->isEmpty()) {
if ($document->getAttribute('projectId') === $project->getId()) {
@@ -110,7 +118,9 @@ App::post('/v1/proxy/rules')
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
$ruleId = md5($domain->get());
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
@@ -144,7 +154,7 @@ App::post('/v1/proxy/rules')
}
$rule->setAttribute('status', $status);
$rule = $dbForConsole->createDocument('rules', $rule);
$rule = $dbForPlatform->createDocument('rules', $rule);
$queueForEvents->setParam('ruleId', $rule->getId());
@@ -175,8 +185,8 @@ App::get('/v1/proxy/rules')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForPlatform) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -205,7 +215,7 @@ App::get('/v1/proxy/rules')
}
$ruleId = $cursor->getValue();
$cursorDocument = $dbForConsole->getDocument('rules', $ruleId);
$cursorDocument = $dbForPlatform->getDocument('rules', $ruleId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found.");
@@ -216,16 +226,16 @@ App::get('/v1/proxy/rules')
$filterQueries = Query::groupByType($queries)['filters'];
$rules = $dbForConsole->find('rules', $queries);
$rules = $dbForPlatform->find('rules', $queries);
foreach ($rules as $rule) {
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
}
$response->dynamic(new Document([
'rules' => $rules,
'total' => $dbForConsole->count('rules', $filterQueries, APP_LIMIT_COUNT),
'total' => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT),
]), Response::MODEL_PROXY_RULE_LIST);
});
@@ -248,15 +258,15 @@ App::get('/v1/proxy/rules/:ruleId')
->param('ruleId', '', new UID(), 'Rule ID.')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole) {
$rule = $dbForConsole->getDocument('rules', $ruleId);
->inject('dbForPlatform')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForPlatform) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
@@ -286,17 +296,17 @@ App::delete('/v1/proxy/rules/:ruleId')
->param('ruleId', '', new UID(), 'Rule ID.')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForDeletes')
->inject('queueForEvents')
->action(function (string $ruleId, Response $response, Document $project, Database $dbForConsole, Delete $queueForDeletes, Event $queueForEvents) {
$rule = $dbForConsole->getDocument('rules', $ruleId);
->action(function (string $ruleId, Response $response, Document $project, Database $dbForPlatform, Delete $queueForDeletes, Event $queueForEvents) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
$dbForConsole->deleteDocument('rules', $rule->getId());
$dbForPlatform->deleteDocument('rules', $rule->getId());
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
@@ -317,7 +327,7 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
->label('sdk', new Method(
namespace: 'proxy',
name: 'updateRuleVerification',
description: '',
description: '/docs/references/proxy/update-rule-verification.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -331,10 +341,10 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('log')
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForConsole, Log $log) {
$rule = $dbForConsole->getDocument('rules', $ruleId);
->action(function (string $ruleId, Response $response, Certificate $queueForCertificates, Event $queueForEvents, Document $project, Database $dbForPlatform, Log $log) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getInternalId()) {
throw new Exception(Exception::RULE_NOT_FOUND);
@@ -364,7 +374,7 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
}
$dbForConsole->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying'));
// Issue a TLS certificate when domain is verified
$queueForCertificates
@@ -375,7 +385,7 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
$queueForEvents->setParam('ruleId', $rule->getId());
$certificate = $dbForConsole->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$rule->setAttribute('logs', $certificate->getAttribute('logs', ''));
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
+16 -7
View File
@@ -6,6 +6,7 @@ use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\SDK\AuthType;
@@ -78,19 +79,20 @@ App::post('/v1/storage/buckets')
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, string $compression, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
$compression ??= Compression::NONE;
$encryption ??= true;
try {
$files = (Config::getParam('collections', [])['buckets'] ?? [])['files'] ?? [];
if (empty($files)) {
@@ -279,13 +281,13 @@ App::put('/v1/storage/buckets/:bucketId')
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, string $compression, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@@ -298,6 +300,7 @@ App::put('/v1/storage/buckets/:bucketId')
$enabled ??= $bucket->getAttribute('enabled', true);
$encryption ??= $bucket->getAttribute('encryption', true);
$antivirus ??= $bucket->getAttribute('antivirus', true);
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
@@ -938,7 +941,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->inject('mode')
->inject('deviceForFiles')
->inject('deviceForLocal')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal) {
->inject('queueForUsage')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal, Usage $queueForUsage) {
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@@ -1066,6 +1070,11 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
$queueForUsage
->addMetric(METRIC_FILES_TRANSFORMATIONS, 1)
->addMetric(str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_TRANSFORMATIONS), 1)
;
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
->setContentType($contentType)
@@ -1765,7 +1774,7 @@ App::get('/v1/storage/usage')
->label('sdk', new Method(
namespace: 'storage',
name: 'getUsage',
description: '',
description: '/docs/references/storage/get-usage.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
+141 -83
View File
@@ -8,6 +8,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Workers\Deletes;
@@ -21,7 +22,9 @@ use Appwrite\Utopia\Database\Validator\Queries\Memberships;
use Appwrite\Utopia\Database\Validator\Queries\Teams;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use libphonenumber\PhoneNumberUtil;
use MaxMind\Db\Reader;
use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
@@ -462,7 +465,10 @@ App::post('/v1/teams/:teamId/memberships')
->inject('queueForMails')
->inject('queueForMessaging')
->inject('queueForEvents')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents) {
->inject('timelimit')
->inject('queueForUsage')
->inject('plan')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, Usage $queueForUsage, array $plan) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -579,47 +585,58 @@ App::post('/v1/teams/:teamId/memberships')
throw new Exception(Exception::USER_UNAUTHORIZED, 'User is not allowed to send invitations for this team');
}
$secret = Auth::tokenGenerator();
$membershipId = ID::unique();
$membership = new Document([
'$id' => $membershipId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($invitee->getId())),
Permission::update(Role::team($team->getId(), 'owner')),
Permission::delete(Role::user($invitee->getId())),
Permission::delete(Role::team($team->getId(), 'owner')),
],
'userId' => $invitee->getId(),
'userInternalId' => $invitee->getInternalId(),
'teamId' => $team->getId(),
'teamInternalId' => $team->getInternalId(),
'roles' => $roles,
'invited' => DateTime::now(),
'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null,
'confirm' => ($isPrivilegedUser || $isAppUser),
'secret' => Auth::hash($secret),
'search' => implode(' ', [$membershipId, $invitee->getId()])
$membership = $dbForProject->findOne('memberships', [
Query::equal('userInternalId', [$invitee->getInternalId()]),
Query::equal('teamInternalId', [$team->getInternalId()]),
]);
if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership
try {
$membership = Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership));
} catch (Duplicate $th) {
throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS);
}
if ($membership->isEmpty()) {
$secret = Auth::tokenGenerator();
$membershipId = ID::unique();
$membership = new Document([
'$id' => $membershipId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($invitee->getId())),
Permission::update(Role::team($team->getId(), 'owner')),
Permission::delete(Role::user($invitee->getId())),
Permission::delete(Role::team($team->getId(), 'owner')),
],
'userId' => $invitee->getId(),
'userInternalId' => $invitee->getInternalId(),
'teamId' => $team->getId(),
'teamInternalId' => $team->getInternalId(),
'roles' => $roles,
'invited' => DateTime::now(),
'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null,
'confirm' => ($isPrivilegedUser || $isAppUser),
'secret' => Auth::hash($secret),
'search' => implode(' ', [$membershipId, $invitee->getId()])
]);
$membership = ($isPrivilegedUser || $isAppUser) ?
Authorization::skip(fn () => $dbForProject->createDocument('memberships', $membership)) :
$dbForProject->createDocument('memberships', $membership);
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
$dbForProject->purgeCachedDocument('users', $invitee->getId());
} else {
try {
$membership = $dbForProject->createDocument('memberships', $membership);
} catch (Duplicate $th) {
throw new Exception(Exception::TEAM_INVITE_ALREADY_EXISTS);
$membership->setAttribute('invited', DateTime::now());
if ($isPrivilegedUser || $isAppUser) {
$membership->setAttribute('joined', DateTime::now());
$membership->setAttribute('confirm', true);
}
$membership = ($isPrivilegedUser || $isAppUser) ?
Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
$dbForProject->updateDocument('memberships', $membership->getId(), $membership);
}
if ($isPrivilegedUser || $isAppUser) {
$dbForProject->purgeCachedDocument('users', $invitee->getId());
} else {
$url = Template::parseURL($url);
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['membershipId' => $membership->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]);
$url = Template::unParseURL($url);
@@ -689,7 +706,7 @@ App::post('/v1/teams/:teamId/memberships')
'owner' => $user->getAttribute('name'),
'direction' => $locale->getText('settings.direction'),
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
'user' => $user->getAttribute('name'),
'user' => $name,
'team' => $team->getAttribute('name'),
'redirect' => $url,
'project' => $projectName
@@ -701,8 +718,8 @@ App::post('/v1/teams/:teamId/memberships')
->setRecipient($invitee->getAttribute('email'))
->setName($invitee->getAttribute('name'))
->setVariables($emailVariables)
->trigger()
;
->trigger();
} elseif (!empty($phone)) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
@@ -730,6 +747,27 @@ App::post('/v1/teams/:teamId/memberships')
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType('SMS');
if (isset($plan['authPhone'])) {
$timelimit = $timelimit('organization:{organizationId}', $plan['authPhone'], 30 * 24 * 60 * 60); // 30 days
$timelimit
->setParam('{organizationId}', $project->getAttribute('teamId'));
$abuse = new Abuse($timelimit);
if ($abuse->check() && System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled') {
$helper = PhoneNumberUtil::getInstance();
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
}
$queueForUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
}
}
@@ -848,8 +886,11 @@ App::get('/v1/teams/:teamId/memberships')
}, $membershipsPrivacy);
$memberships = array_map(function ($membership) use ($dbForProject, $team, $membershipsPrivacy) {
$user = !empty(array_filter($membershipsPrivacy))
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
: new Document();
if ($membershipsPrivacy['mfa']) {
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
@@ -934,9 +975,11 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
return $privacy || $isPrivilegedUser || $isAppUser;
}, $membershipsPrivacy);
if ($membershipsPrivacy['mfa']) {
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$user = !empty(array_filter($membershipsPrivacy))
? $dbForProject->getDocument('users', $membership->getAttribute('userId'))
: new Document();
if ($membershipsPrivacy['mfa']) {
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
@@ -1111,7 +1154,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
throw new Exception(Exception::TEAM_INVITE_MISMATCH, 'Invite does not belong to current user (' . $user->getAttribute('email') . ')');
}
if ($user->isEmpty()) {
$hasSession = !$user->isEmpty();
if (!$hasSession) {
$user->setAttributes($dbForProject->getDocument('users', $userId)->getArrayCopy()); // Get user
}
@@ -1130,39 +1174,64 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
Authorization::skip(fn () => $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', true)));
// Log user in
// Create session for the user if not logged in
if (!$hasSession) {
Authorization::setRole(Role::user($user->getId())->toString());
Authorization::setRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'factors' => ['email'],
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$session = $dbForProject->createDocument('sessions', $session);
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
Authorization::setRole(Role::user($userId)->toString());
$dbForProject->purgeCachedDocument('users', $user->getId());
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
}
Authorization::setRole(Role::user($userId)->toString());
$response
->addCookie(
name: Auth::$cookieName . '_legacy',
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true
)
->addCookie(
name: Auth::$cookieName,
value: Auth::encodeSession($user->getId(), $secret),
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
secure: ('https' === $protocol),
httponly: true,
sameSite: Config::getParam('cookieSamesite')
)
;
}
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
@@ -1176,22 +1245,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setParam('membershipId', $membership->getId())
;
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
;
}
$response
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic(
$membership
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
->setAttribute('teamName', $team->getAttribute('name'))
->setAttribute('userName', $user->getAttribute('name'))
->setAttribute('userEmail', $user->getAttribute('email')),
Response::MODEL_MEMBERSHIP
);
});
+12 -2
View File
@@ -1984,6 +1984,7 @@ App::post('/v1/users/:userId/sessions')
'provider' => Auth::SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'factors' => ['server'],
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => $expire,
@@ -1993,11 +1994,20 @@ App::post('/v1/users/:userId/sessions')
$detector->getDevice()
));
$session->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]);
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session = $dbForProject->createDocument('sessions', $session);
$dbForProject->purgeCachedDocument('users', $user->getId());
$session
->setAttribute('secret', $secret)
->setAttribute('secret', Auth::encodeSession($user->getId(), $secret))
->setAttribute('countryName', $countryName);
$queueForEvents
@@ -2381,7 +2391,7 @@ App::get('/v1/users/usage')
->label('sdk', new Method(
namespace: 'users',
name: 'getUsage',
description: '',
description: '/docs/references/users/get-usage.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
+112 -132
View File
@@ -47,7 +47,7 @@ use Utopia\VCS\Exception\RepositoryNotFound;
use function Swoole\Coroutine\batch;
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForConsole, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$errors = [];
foreach ($repositories as $resource) {
try {
@@ -58,7 +58,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
$projectId = $resource->getAttribute('projectId');
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
$functionId = $resource->getAttribute('resourceId');
@@ -109,7 +109,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$latestCommentId = '';
if (!empty($providerPullRequestId) && $function->getAttribute('providerSilentMode', false) === false) {
$latestComment = Authorization::skip(fn () => $dbForConsole->findOne('vcsComments', [
$latestComment = Authorization::skip(fn () => $dbForPlatform->findOne('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'),
@@ -130,7 +130,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', '');
$latestComment = Authorization::skip(fn () => $dbForConsole->createDocument('vcsComments', new Document([
$latestComment = Authorization::skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
@@ -151,7 +151,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
}
} elseif (!empty($providerBranch)) {
$latestComments = Authorization::skip(fn () => $dbForConsole->find('vcsComments', [
$latestComments = Authorization::skip(fn () => $dbForPlatform->find('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'),
@@ -272,12 +272,11 @@ App::get('/v1/vcs/github/authorize')
->desc('Install GitHub app')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('sdk.namespace', 'vcs')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('sdk', new Method(
namespace: 'vcs',
name: 'createGitHubInstallation',
description: '',
description: '/docs/references/vcs/create-github-installation.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -332,8 +331,8 @@ App::get('/v1/vcs/github/callback')
->inject('project')
->inject('request')
->inject('response')
->inject('dbForConsole')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForPlatform) {
if (empty($state)) {
$error = 'Installation requests from organisation members for the Appwrite GitHub App are currently unsupported. To proceed with the installation, login to the Appwrite Console and install the GitHub App.';
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
@@ -352,7 +351,7 @@ App::get('/v1/vcs/github/callback')
$redirectSuccess = $state['success'] ?? '';
$redirectFailure = $state['failure'] ?? '';
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
@@ -368,53 +367,6 @@ App::get('/v1/vcs/github/callback')
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
}
$personalSlug = '';
// OAuth Authroization
if (!empty($code)) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$accessToken = $oauth2->getAccessToken($code) ?? '';
$refreshToken = $oauth2->getRefreshToken($code) ?? '';
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code) ?? '';
$personalSlug = $oauth2->getUserSlug($accessToken) ?? '';
$email = $oauth2->getUserEmail($accessToken);
$oauth2ID = $oauth2->getUserID($accessToken);
// Makes sure this email is not already used in another identity
$identity = $dbForConsole->findOne('identities', [
Query::equal('providerEmail', [$email]),
]);
if (!$identity->isEmpty()) {
if ($identity->getAttribute('userInternalId', '') !== $user->getInternalId()) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
} else {
$identity = $dbForConsole->createDocument('identities', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userInternalId' => $user->getInternalId(),
'userId' => $user->getId(),
'provider' => 'github',
'providerUid' => $oauth2ID,
'providerEmail' => $email,
'providerAccessToken' => $accessToken,
'providerRefreshToken' => $refreshToken,
'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry),
]));
}
}
// Create / Update installation
if (!empty($providerInstallationId)) {
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
@@ -424,11 +376,27 @@ App::get('/v1/vcs/github/callback')
$projectInternalId = $project->getInternalId();
$installation = $dbForConsole->findOne('installations', [
$installation = $dbForPlatform->findOne('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::equal('projectInternalId', [$projectInternalId])
]);
$personal = false;
$refreshToken = null;
$accessToken = null;
$accessTokenExpiry = null;
if (!empty($code)) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$accessToken = $oauth2->getAccessToken($code) ?? '';
$refreshToken = $oauth2->getRefreshToken($code) ?? '';
$accessTokenExpiry = DateTime::addSeconds(new \DateTime(), \intval($oauth2->getAccessTokenExpiry($code)));
$personalSlug = $oauth2->getUserSlug($accessToken) ?? '';
$personal = $personalSlug === $owner;
}
if ($installation->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
@@ -446,15 +414,21 @@ App::get('/v1/vcs/github/callback')
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => $personalSlug === $owner
'personal' => $personal,
'personalRefreshToken' => $refreshToken,
'personalAccessToken' => $accessToken,
'personalAccessTokenExpiry' => $accessTokenExpiry,
]);
$installation = $dbForConsole->createDocument('installations', $installation);
$installation = $dbForPlatform->createDocument('installations', $installation);
} else {
$installation = $installation
->setAttribute('organization', $owner)
->setAttribute('personal', $personalSlug === $owner);
$installation = $dbForConsole->updateDocument('installations', $installation->getId(), $installation);
->setAttribute('personal', $personal)
->setAttribute('personalRefreshToken', $refreshToken)
->setAttribute('personalAccessToken', $accessToken)
->setAttribute('personalAccessTokenExpiry', $accessTokenExpiry);
$installation = $dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
}
} else {
$error = 'Installation of the Appwrite GitHub App on organization accounts is restricted to organization owners. As a member of the organization, you do not have the necessary permissions to install this GitHub App. Please contact the organization owner to create the installation from the Appwrite console.';
@@ -483,7 +457,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->label('sdk', new Method(
namespace: 'vcs',
name: 'getRepositoryContents',
description: '',
description: '/docs/references/vcs/get-repository-contents.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -498,9 +472,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -549,7 +523,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr
->label('sdk', new Method(
namespace: 'vcs',
name: 'createRepositoryDetection',
description: '',
description: '/docs/references/vcs/create-repository-detection.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -564,9 +538,9 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -626,7 +600,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->label('sdk', new Method(
namespace: 'vcs',
name: 'listRepositories',
description: '',
description: '/docs/references/vcs/list-repositories.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -640,13 +614,13 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $search, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $installationId, string $search, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
if (empty($search)) {
$search = "";
}
$installation = $dbForConsole->getDocument('installations', $installationId);
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -726,7 +700,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
->label('sdk', new Method(
namespace: 'vcs',
name: 'createRepository',
description: '',
description: '/docs/references/vcs/create-repository.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -742,9 +716,9 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
->inject('user')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $name, bool $private, GitHub $github, Document $user, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $name, bool $private, GitHub $github, Document $user, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -753,17 +727,23 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
if ($installation->getAttribute('personal', false) === true) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$identity = $dbForConsole->findOne('identities', [
Query::equal('provider', ['github']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $installation->getAttribute('personalAccessToken');
$refreshToken = $installation->getAttribute('personalRefreshToken');
$accessTokenExpiry = $installation->getAttribute('personalAccessTokenExpiry');
$accessToken = $identity->getAttribute('providerAccessToken');
$refreshToken = $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $identity->getAttribute('providerAccessTokenExpiry');
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
$identity = $dbForPlatform->findOne('identities', [
Query::equal('provider', ['github']),
Query::equal('userInternalId', [$user->getInternalId()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $accessToken ?? $identity->getAttribute('providerAccessToken');
$refreshToken = $refreshToken ?? $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $accessTokenExpiry ?? $identity->getAttribute('providerAccessTokenExpiry');
}
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
@@ -778,12 +758,12 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories')
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, "Another request is currently refreshing OAuth token. Please try again.");
}
$identity = $identity
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
$installation = $installation
->setAttribute('personalAccessToken', $accessToken)
->setAttribute('personalRefreshToken', $refreshToken)
->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
$dbForConsole->updateDocument('identities', $identity->getId(), $identity);
$dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
}
try {
@@ -832,7 +812,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->label('sdk', new Method(
namespace: 'vcs',
name: 'getRepository',
description: '',
description: '/docs/references/vcs/get-repository.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -846,9 +826,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -886,7 +866,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->label('sdk', new Method(
namespace: 'vcs',
name: 'listRepositoryBranches',
description: '',
description: '/docs/references/vcs/list-repository-branches.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -900,9 +880,9 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:pro
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -940,11 +920,11 @@ App::post('/v1/vcs/github/events')
->inject('gitHub')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
@@ -978,36 +958,36 @@ App::post('/v1/vcs/github/events')
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find functionId from functions table
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
// create new deployment only on push and not when branch is created
if (!$providerBranchCreated) {
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
// TODO: Use worker for this job instead (update function as well)
$providerInstallationId = $parsedPayload["installationId"];
$installations = $dbForConsole->find('installations', [
$installations = $dbForPlatform->find('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::limit(1000)
]);
foreach ($installations as $installation) {
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('installationInternalId', [$installation->getInternalId()]),
Query::limit(1000)
]));
foreach ($repositories as $repository) {
Authorization::skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId()));
Authorization::skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
}
$dbForConsole->deleteDocument('installations', $installation->getId());
$dbForPlatform->deleteDocument('installations', $installation->getId());
}
}
} elseif ($event == $github::EVENT_PULL_REQUEST) {
@@ -1036,12 +1016,12 @@ App::post('/v1/vcs/github/events')
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
@@ -1050,7 +1030,7 @@ App::post('/v1/vcs/github/events')
$external = $parsedPayload["external"] ?? true;
if ($external) {
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
@@ -1061,7 +1041,7 @@ App::post('/v1/vcs/github/events')
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
$repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
}
}
}
@@ -1093,8 +1073,8 @@ App::get('/v1/vcs/installations')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('dbForConsole')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
@@ -1123,7 +1103,7 @@ App::get('/v1/vcs/installations')
}
$installationId = $cursor->getValue();
$cursorDocument = $dbForConsole->getDocument('installations', $installationId);
$cursorDocument = $dbForPlatform->getDocument('installations', $installationId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Installation '{$installationId}' for the 'cursor' value not found.");
@@ -1134,8 +1114,8 @@ App::get('/v1/vcs/installations')
$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForConsole->find('installations', $queries);
$total = $dbForConsole->count('installations', $filterQueries, APP_LIMIT_COUNT);
$results = $dbForPlatform->find('installations', $queries);
$total = $dbForPlatform->count('installations', $filterQueries, APP_LIMIT_COUNT);
$response->dynamic(new Document([
'installations' => $results,
@@ -1162,9 +1142,9 @@ App::get('/v1/vcs/installations/:installationId')
->param('installationId', '', new Text(256), 'Installation Id')
->inject('response')
->inject('project')
->inject('dbForConsole')
->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->inject('dbForPlatform')
->action(function (string $installationId, Response $response, Document $project, Database $dbForPlatform) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation === false || $installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
@@ -1197,16 +1177,16 @@ App::delete('/v1/vcs/installations/:installationId')
->param('installationId', '', new Text(256), 'Installation Id')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForDeletes')
->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole, Delete $queueForDeletes) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->action(function (string $installationId, Response $response, Document $project, Database $dbForPlatform, Delete $queueForDeletes) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
if (!$dbForConsole->deleteDocument('installations', $installation->getId())) {
if (!$dbForPlatform->deleteDocument('installations', $installation->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove installation from DB');
}
@@ -1224,7 +1204,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
->label('sdk', new Method(
namespace: 'vcs',
name: 'updateExternalDeployments',
description: '',
description: '/docs/references/vcs/update-external-deployments.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
@@ -1240,17 +1220,17 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
->inject('request')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForConsole, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$installation = $dbForConsole->getDocument('installations', $installationId);
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = Authorization::skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [
$repository = Authorization::skip(fn () => $dbForPlatform->getDocument('repositories', $repositoryId, [
Query::equal('projectInternalId', [$project->getInternalId()])
]));
@@ -1267,7 +1247,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
// TODO: Delete from array when PR is closed
$repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$repository = Authorization::skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
@@ -1291,7 +1271,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$response->noContent();
});
+105 -53
View File
@@ -33,6 +33,7 @@ use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain;
use Utopia\DSN\DSN;
@@ -49,13 +50,26 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForConsole, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname)
{
$utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
$host = $previewHostname;
}
$route = Authorization::skip(fn () => $dbForConsole->getDocument('rules', md5($host)));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$route = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
} else {
$route = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? new Document();
}
if ($route->isEmpty()) {
if ($host === System::getEnv('_APP_DOMAIN_FUNCTIONS', '')) {
@@ -67,7 +81,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
}
if (System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') {
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL && $host !== System::getEnv('_APP_CONSOLE_DOMAIN', '')) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.');
}
}
@@ -79,8 +93,17 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$projectId = $route->getAttribute('projectId');
$project = Authorization::skip(
fn () => $dbForConsole->getDocument('projects', $projectId)
fn () => $dbForPlatform->getDocument('projects', $projectId)
);
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
$status = $project->getAttribute('services', [])['proxy'];
if (!$status) {
@@ -146,7 +169,7 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$requestHeaders = $request->getHeaders();
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
$dbForProject = $getProjectDB($project);
@@ -371,26 +394,6 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
throw $th;
}
} finally {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger()
;
$queueForFunctions
->setType(Func::TYPE_ASYNC_WRITE)
->setExecution($execution)
@@ -425,6 +428,26 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
->setStatusCode($execution['responseStatusCode'] ?? 200)
->send($body);
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_FUNCTION_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_FUNCTION_CPUS_DEFAULT)))
->setProject($project)
->trigger()
;
return true;
} elseif ($type === 'api') {
$utopia->getRoute()?->label('error', '');
@@ -470,7 +493,7 @@ App::init()
->inject('response')
->inject('console')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('locale')
->inject('localeCodes')
@@ -481,15 +504,16 @@ App::init()
->inject('queueForCertificates')
->inject('queueForFunctions')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked)) {
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
return;
}
}
@@ -537,18 +561,31 @@ App::init()
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
$domainDocument = $dbForConsole->getDocument('rules', md5($envDomain));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($envDomain));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [Query::orderAsc('$id')]);
}
$mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get();
}
if ($mainDomain !== $domain->get()) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
} else {
$domainDocument = $dbForConsole->getDocument('rules', md5($domain->get()));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($domain->get()));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()])
]);
}
if ($domainDocument->isEmpty()) {
$domainDocument = new Document([
'$id' => md5($domain->get()),
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
'$id' => System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(),
'domain' => $domain->get(),
'resourceType' => 'api',
'status' => 'verifying',
@@ -556,7 +593,7 @@ App::init()
'projectInternalId' => 'console'
]);
$domainDocument = $dbForConsole->createDocument('rules', $domainDocument);
$domainDocument = $dbForPlatform->createDocument('rules', $domainDocument);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
@@ -692,22 +729,23 @@ App::options()
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked)) {
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
return;
}
}
@@ -795,6 +833,10 @@ App::error()
break;
case 'Utopia\Database\Exception\NotFound':
$error = new AppwriteException(AppwriteException::COLLECTION_NOT_FOUND, $error->getMessage(), previous: $error);
break;
case 'Utopia\Database\Exception\Dependency':
$error = new AppwriteException(AppwriteException::INDEX_DEPENDENCY, null, previous: $error);
break;
}
$code = $error->getCode();
@@ -822,7 +864,7 @@ App::error()
$adapter = new Sentry($projectId, $key, $host);
$logger = new Logger($adapter);
$logger->setSample(0.04);
$logger->setSample(0.01);
$publish = true;
} else {
throw new \Exception('Invalid experimental logging provider');
@@ -862,6 +904,8 @@ App::error()
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
} else {
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
}
try {
@@ -872,14 +916,14 @@ App::error()
}
$log->setNamespace("http");
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('database', $dsn->getHost());
$log->addTag('method', $route->getMethod());
$log->addTag('url', $route->getPath());
$log->addTag('url', $request->getURI());
$log->addTag('verboseType', get_class($error));
$log->addTag('code', $error->getCode());
$log->addTag('projectId', $project->getId());
@@ -891,8 +935,14 @@ App::error()
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
/** @var Appwrite\SDK\Method $sdk */
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
}
$log->setAction($action);
$log->addTag('service', $action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
@@ -987,22 +1037,23 @@ App::get('/robots.txt')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($host === $mainDomain || $host === 'localhost') {
if (($host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
}
});
@@ -1014,22 +1065,23 @@ App::get('/humans.txt')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('isResourceBlocked')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked) {
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if ($host === $mainDomain || $host === 'localhost') {
if (($host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
}
});
@@ -1093,9 +1145,9 @@ App::get('/v1/ping')
->label('event', 'projects.[projectId].ping')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('queueForEvents')
->action(function (Response $response, Document $project, Database $dbForConsole, Event $queueForEvents) {
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) {
if ($project->isEmpty()) {
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
@@ -1107,8 +1159,8 @@ App::get('/v1/ping')
->setAttribute('pingCount', $pingCount)
->setAttribute('pingedAt', $pingedAt);
Authorization::skip(function () use ($dbForConsole, $project) {
$dbForConsole->updateDocument('projects', $project->getId(), $project);
Authorization::skip(function () use ($dbForPlatform, $project) {
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
});
$queueForEvents
+9 -9
View File
@@ -162,15 +162,15 @@ App::post('/v1/mock/api-key-unprefixed')
->label('docs', false)
->param('projectId', '', new UID(), 'Project ID.')
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, Response $response, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $projectId, Response $response, Database $dbForPlatform) {
$isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development';
if (!$isDevelopment) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
}
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
@@ -195,9 +195,9 @@ App::post('/v1/mock/api-key-unprefixed')
'secret' => \bin2hex(\random_bytes(128)),
]);
$key = $dbForConsole->createDocument('keys', $key);
$key = $dbForPlatform->createDocument('keys', $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -214,15 +214,15 @@ App::get('/v1/mock/github/callback')
->inject('gitHub')
->inject('project')
->inject('response')
->inject('dbForConsole')
->action(function (string $providerInstallationId, string $projectId, GitHub $github, Document $project, Response $response, Database $dbForConsole) {
->inject('dbForPlatform')
->action(function (string $providerInstallationId, string $projectId, GitHub $github, Document $project, Response $response, Database $dbForPlatform) {
$isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development';
if (!$isDevelopment) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
}
$project = $dbForConsole->getDocument('projects', $projectId);
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
@@ -256,7 +256,7 @@ App::get('/v1/mock/github/callback')
'personal' => false
]);
$installation = $dbForConsole->createDocument('installations', $installation);
$installation = $dbForPlatform->createDocument('installations', $installation);
}
$response->json([
+105 -60
View File
@@ -19,7 +19,6 @@ use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
@@ -59,7 +58,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
return $label;
};
$eventDatabaseListener = function (Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
$eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
@@ -75,17 +74,20 @@ $eventDatabaseListener = function (Document $document, Response $response, Event
->from($queueForEvents)
->trigger();
$queueForWebhooks
->from($queueForEvents)
->trigger();
if ($queueForEvents->getProject()->getId() === 'console') {
return;
/** Trigger webhooks events only if a project has them enabled */
if (!empty($project->getAttribute('webhooks'))) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
}
$queueForRealtime
->from($queueForEvents)
->trigger();
/** Trigger realtime events only for non console events */
if ($queueForEvents->getProject()->getId() !== 'console') {
$queueForRealtime
->from($queueForEvents)
->trigger();
}
};
$usageDatabaseListener = function (string $event, Document $document, Usage $queueForUsage) {
@@ -191,14 +193,16 @@ App::init()
->groups(['api'])
->inject('utopia')
->inject('request')
->inject('dbForConsole')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('queueForAudits')
->inject('project')
->inject('user')
->inject('session')
->inject('servers')
->inject('mode')
->inject('team')
->action(function (App $utopia, Request $request, Database $dbForConsole, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) {
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) {
$route = $utopia->getRoute();
if ($project->isEmpty()) {
@@ -250,9 +254,10 @@ App::init()
$user = new Document([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $project->getAttribute('name', 'Untitled'),
'name' => 'Dynamic Key',
]);
$role = Auth::USER_ROLE_APPS;
@@ -260,6 +265,8 @@ App::init()
Authorization::setRole(Auth::USER_ROLE_APPS);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
$queueForAudits->setUser($user);
}
} elseif ($keyType === API_KEY_STANDARD) {
// No underline means no prefix. Backwards compatibility.
@@ -271,9 +278,10 @@ App::init()
$user = new Document([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $project->getAttribute('name', 'Untitled'),
'name' => $key->getAttribute('name', 'UNKNOWN'),
]);
$role = Auth::USER_ROLE_APPS;
@@ -290,8 +298,8 @@ App::init()
$accessedAt = $key->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DateTime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
$sdkValidator = new WhiteList($servers, true);
@@ -304,10 +312,12 @@ App::init()
/** Update access time as well */
$key->setAttribute('accessedAt', Datetime::now());
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->purgeCachedDocument('projects', $project->getId());
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
$queueForAudits->setUser($user);
}
}
}
@@ -342,6 +352,33 @@ App::init()
Authorization::setRole($authRole);
}
/**
* Update project last activity
*/
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
/**
* Update user last activity
*/
if (!empty($user->getId())) {
$accessedAt = $user->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if (APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$dbForPlatform->updateDocument('users', $user->getId(), $user);
}
}
}
/** Do not allow access to disabled services */
/**
* @var ?\Appwrite\SDK\Method $method
@@ -408,8 +445,9 @@ App::init()
->inject('queueForBuilds')
->inject('queueForUsage')
->inject('dbForProject')
->inject('timelimit')
->inject('mode')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Connection $queue, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, string $mode) use ($usageDatabaseListener, $eventDatabaseListener) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Connection $queue, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Usage $queueForUsage, Database $dbForProject, callable $timelimit, string $mode) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
@@ -432,7 +470,7 @@ App::init()
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = new TimeLimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForProject);
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
@@ -460,7 +498,7 @@ App::init()
$abuse = new Abuse($timeLimit);
$remaining = $timeLimit->remaining();
$limit = $timeLimit->limit();
$time = (new \DateTime($timeLimit->time()))->getTimestamp() + $route->getLabel('abuse-time', 3600);
$time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
$closestLimit = $remaining;
@@ -494,9 +532,17 @@ App::init()
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($request->getIP())
->setHostname($request->getHostname())
->setEvent($route->getLabel('audits.event', ''))
->setProject($project)
->setUser($user);
->setProject($project);
/* If a session exists, use the user associated with the session */
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
}
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
@@ -515,6 +561,7 @@ App::init()
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForUsage))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForUsage))
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
$project,
$document,
$response,
$queueForEventsClone->from($queueForEvents),
@@ -658,9 +705,7 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->inject('mode')
->inject('dbForConsole')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, string $mode, Database $dbForConsole) use ($parseLabel) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, Usage $queueForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -669,10 +714,6 @@ App::shutdown()
$queueForEvents->setPayload($responsePayload);
}
$queueForWebhooks
->from($queueForEvents)
->trigger();
$queueForFunctions
->from($queueForEvents)
->trigger();
@@ -682,6 +723,17 @@ App::shutdown()
->from($queueForEvents)
->trigger();
}
/** Trigger webhooks events only if a project has them enabled
* A future optimisation is to only trigger webhooks if the webhook is "enabled"
* But it might have performance implications on the API due to the number of webhooks etc.
* Some profiling is needed to see if this is a problem.
*/
if (!empty($project->getAttribute('webhooks'))) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
}
}
$route = $utopia->getRoute();
@@ -699,10 +751,32 @@ App::shutdown()
}
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
/**
* User in the request is empty, and no user was set for auditing previously.
* This indicates:
* - No API Key was used.
* - No active session exists.
*
* Therefore, we consider this an anonymous request and create a relevant user.
*/
$user = new Document([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_GUEST,
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => 'Guest',
]);
$queueForAudits->setUser($user);
}
if (!empty($queueForAudits->getResource()) && !empty($queueForAudits->getUser()->getId())) {
if (!empty($queueForAudits->getResource()) && !$queueForAudits->getUser()->isEmpty()) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
@@ -780,8 +854,6 @@ App::shutdown()
}
}
if ($project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
$fileSize = 0;
@@ -800,33 +872,6 @@ App::shutdown()
->setProject($project)
->trigger();
}
/**
* Update project last activity
*/
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForConsole->updateDocument('projects', $project->getId(), $project));
}
}
/**
* Update user last activity
*/
if (!$user->isEmpty()) {
$accessedAt = $user->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if (APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$dbForConsole->updateDocument('users', $user->getId(), $user);
}
}
}
});
App::init()
+9 -8
View File
@@ -5,6 +5,7 @@ use Appwrite\Extend\Exception;
use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
@@ -57,44 +58,44 @@ App::init()
$auths = $project->getAttribute('auths', []);
switch ($route->getLabel('auth.type', '')) {
case 'emailPassword':
if (($auths['emailPassword'] ?? true) === false) {
case 'email-password':
if (($auths[Config::getParam('auth')['email-password']['key']] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email / Password authentication is disabled for this project');
}
break;
case 'magic-url':
if (($auths['usersAuthMagicURL'] ?? true) === false) {
if (($auths[Config::getParam('auth')['magic-url']['key']] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Magic URL authentication is disabled for this project');
}
break;
case 'anonymous':
if (($auths['anonymous'] ?? true) === false) {
if (($auths[Config::getParam('auth')['anonymous']['key']] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Anonymous authentication is disabled for this project');
}
break;
case 'phone':
if (($auths['phone'] ?? true) === false) {
if (($auths[Config::getParam('auth')['phone']['key']] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Phone authentication is disabled for this project');
}
break;
case 'invites':
if (($auths['invites'] ?? true) === false) {
if (($auths[Config::getParam('auth')['invites']['key']] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Invites authentication is disabled for this project');
}
break;
case 'jwt':
if (($auths['JWT'] ?? true) === false) {
if (($auths[Config::getParam('auth')['jwt']['key']] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'JWT authentication is disabled for this project');
}
break;
case 'email-otp':
if (($auths['emailOTP'] ?? true) === false) {
if (($auths[Config::getParam('auth')['email-otp']['key']] ?? true) === false) {
throw new Exception(Exception::USER_AUTH_METHOD_UNSUPPORTED, 'Email OTP authentication is disabled for this project');
}
break;
+231 -77
View File
@@ -9,16 +9,19 @@ use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Http\Server;
use Swoole\Process;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Swoole\Table;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
@@ -26,6 +29,12 @@ use Utopia\Pools\Group;
use Utopia\Swoole\Files;
use Utopia\System\System;
const DOMAIN_SYNC_TIMER = 30; // 30 seconds
$domains = new Table(1_000_000); // 1 million rows
$domains->column('value', Table::TYPE_INT, 1);
$domains->create();
$http = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
@@ -33,15 +42,17 @@ $http = new Server(
);
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$workerNumber = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$http
->set([
'worker_num' => $workerNumber,
'worker_num' => $totalWorkers,
'dispatch_func' => 'dispatch',
'open_http2_protocol' => true,
'http_compression' => false,
'package_max_length' => $payloadSize,
'buffer_output_size' => $payloadSize,
'task_worker_num' => 1, // required for the task to fetch domains background
]);
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
@@ -56,12 +67,97 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server, $workerId) {
Console::success('Reload completed...');
});
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
*
* Routes requests as 'safe' or 'risky' based on specific content patterns (like POST actions or certain domains)
* to optimize load distribution between the workers. Utilizes `$safeThreadsPercent` to manage risk by assigning
* riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary.
* doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func
*
* @param Server $server Swoole server instance.
* @param int $fd client ID
* @param int $type the type of data and its current state
* @param string|null $data Request content for categorization.
* @global int $totalThreads Total number of workers.
* @return int Chosen worker ID for the request.
*/
function dispatch(Server $server, int $fd, int $type, $data = null): int
{
global $totalWorkers, $domains;
// If data is not set we can send request to any worker
// first we try to pick idle worker, if not we randomly pick a worker
if ($data === null) {
for ($i = 0; $i < $totalWorkers; $i++) {
if ($server->getWorkerStatus($i) === SWOOLE_WORKER_IDLE) {
return $i;
}
}
return rand(0, $totalWorkers - 1);
}
$riskyWorkersPercent = intval(System::getEnv('_APP_RISKY_WORKERS_PERCENT', 80)) / 100; // Decimal form 0 to 1
// Each worker has numeric ID, starting from 0 and incrementing
// From 0 to riskyWorkers, we consider safe workers
// From riskyWorkers to totalWorkers, we consider risky workers
$riskyWorkers = (int) floor($totalWorkers * $riskyWorkersPercent); // Absolute amount of risky workers
$domain = '';
// max up to 3 as first line has request details and second line has host
$lines = explode("\n", $data, 3);
$request = $lines[0];
if (count($lines) > 1) {
$domain = trim(explode('Host: ', $lines[1])[1]);
}
// Sync executions are considered risky
$risky = false;
if (str_starts_with($request, 'POST') && str_contains($request, '/executions')) {
$risky = true;
} elseif (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS'))) {
$risky = true;
} elseif ($domains->get(md5($domain), 'value') === 1) {
// executions request coming from custom domain
$risky = true;
}
if ($risky) {
// If risky request, only consider risky workers
for ($j = $riskyWorkers; $j < $totalWorkers; $j++) {
/** Reference https://openswoole.com/docs/modules/swoole-server-getWorkerStatus#description */
if ($server->getWorkerStatus($j) === SWOOLE_WORKER_IDLE) {
// If idle worker found, give to him
return $j;
}
}
// If no idle workers, give to random risky worker
$worker = rand($riskyWorkers, $totalWorkers - 1);
Console::warning("swoole_dispatch: Risky branch: did not find a idle worker, picking random worker {$worker}");
return $worker;
}
// If safe request, give to any idle worker
// Its fine to pick risky worker here, because it's idle. Idle is never actually risky
for ($i = 0; $i < $totalWorkers; $i++) {
if ($server->getWorkerStatus($i) === SWOOLE_WORKER_IDLE) {
return $i;
}
}
// If no idle worker found, give to random safe worker
// We avoid risky workers here, as it could be in work - not idle. Thats exactly when they are risky.
$worker = rand(0, $riskyWorkers - 1);
Console::warning("swoole_dispatch: Non-risky branch: did not find a idle worker, picking random worker {$worker}");
return $worker;
}
include __DIR__ . '/controllers/general.php';
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $register) {
$app = new App('UTC');
$app->setCompression(true);
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
go(function () use ($register, $app) {
$pools = $register->get('pools');
@@ -76,8 +172,8 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
do {
try {
$attempts++;
$dbForConsole = $app->getResource('dbForConsole');
/** @var Utopia\Database\Database $dbForConsole */
$dbForPlatform = $app->getResource('dbForPlatform');
/** @var Utopia\Database\Database $dbForPlatform */
break; // leave the do-while if successful
} catch (\Throwable $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
@@ -91,22 +187,17 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
Console::success('[Setup] - Server database init started...');
try {
Console::success('[Setup] - Creating database: appwrite...');
$dbForConsole->create();
} catch (\Throwable $e) {
Console::success('[Setup] - Creating console database...');
$dbForPlatform->create();
} catch (Duplicate) {
Console::success('[Setup] - Skip: metadata table already exists');
}
if ($dbForConsole->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForConsole);
if ($dbForPlatform->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForPlatform);
$audit->setup();
}
if ($dbForConsole->getCollection(TimeLimit::COLLECTION)->isEmpty()) {
$adapter = new TimeLimit("", 0, 1, $dbForConsole);
$adapter->setup();
}
/** @var array $collections */
$collections = Config::getParam('collections', []);
$consoleCollections = $collections['console'];
@@ -114,45 +205,21 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$dbForConsole->getCollection($key)->isEmpty()) {
if (!$dbForPlatform->getCollection($key)->isEmpty()) {
continue;
}
Console::success('[Setup] - Creating collection: ' . $collection['$id'] . '...');
Console::success('[Setup] - Creating console collection: ' . $collection['$id'] . '...');
$attributes = [];
$indexes = [];
$attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']);
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
}
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
$dbForConsole->createCollection($key, $attributes, $indexes);
$dbForPlatform->createCollection($key, $attributes, $indexes);
}
if ($dbForConsole->getDocument('buckets', 'default')->isEmpty() && !$dbForConsole->exists($dbForConsole->getDatabase(), 'bucket_1')) {
if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() && !$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_1')) {
Console::success('[Setup] - Creating default bucket...');
$dbForConsole->createDocument('buckets', new Document([
$dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'name' => 'Default',
@@ -172,7 +239,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'search' => 'buckets Default',
]));
$bucket = $dbForConsole->getDocument('buckets', 'default');
$bucket = $dbForPlatform->getDocument('buckets', 'default');
Console::success('[Setup] - Creating files collection for default bucket...');
$files = $collections['buckets']['files'] ?? [];
@@ -180,34 +247,53 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
throw new Exception('Files collection is not configured.');
}
$attributes = [];
$indexes = [];
$attributes = \array_map(fn ($attribute) => new Document($attribute), $files['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $files['indexes']);
foreach ($files['attributes'] as $attribute) {
$attributes[] = new Document([
'$id' => ID::custom($attribute['$id']),
'type' => $attribute['type'],
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'array' => $attribute['array'],
'filters' => $attribute['filters'],
'default' => $attribute['default'] ?? null,
'format' => $attribute['format'] ?? ''
]);
$dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
}
$projectCollections = $collections['projects'];
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
$sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1);
$cache = $app->getResource('cache');
foreach ($sharedTablesV2 as $hostname) {
$adapter = $pools
->get($hostname)
->pop()
->getResource();
$dbForProject = (new Database($adapter, $cache))
->setDatabase('appwrite')
->setSharedTables(true)
->setTenant(null)
->setNamespace(System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', ''));
try {
Console::success('[Setup] - Creating project database: ' . $hostname . '...');
$dbForProject->create();
} catch (Duplicate) {
Console::success('[Setup] - Skip: metadata table already exists');
}
foreach ($files['indexes'] as $index) {
$indexes[] = new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]);
}
foreach ($projectCollections as $key => $collection) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$dbForProject->getCollection($key)->isEmpty()) {
continue;
}
$dbForConsole->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
$attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']);
Console::success('[Setup] - Creating project collection: ' . $collection['$id'] . '...');
$dbForProject->createCollection($key, $attributes, $indexes);
}
}
$pools->reclaim();
@@ -218,6 +304,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
Console::success('Server started successfully (max payload is ' . number_format($payloadSize) . ' bytes)');
Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}");
// Start the task that starts fetching custom domains
$http->task([], 0);
// listen ctrl + c
Process::signal(2, function () use ($http) {
Console::log('Stop by Ctrl+C');
@@ -225,7 +314,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
});
});
$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) {
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) {
App::setResource('swooleRequest', fn () => $swooleRequest);
App::setResource('swooleResponse', fn () => $swooleResponse);
@@ -245,7 +334,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
}
$app = new App('UTC');
$app->setCompression(true);
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
$pools = $register->get('pools');
@@ -274,10 +363,12 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
} else {
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
}
$log->setNamespace("http");
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($th->getMessage());
@@ -295,8 +386,16 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$log->addExtra('trace', $th->getTraceAsString());
$log->addExtra('roles', Authorization::getRoles());
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$sdk = $route->getLabel("sdk", false);
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
/** @var Appwrite\SDK\Method $sdk */
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
}
$log->setAction($action);
$log->addTag('service', $action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
@@ -335,4 +434,59 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
}
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on('Task', function () use ($register, $domains) {
$lastSyncUpdate = null;
$pools = $register->get('pools');
App::setResource('pools', fn () => $pools);
$app = new App('UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
Console::loop(function () use ($dbForPlatform, $domains, &$lastSyncUpdate) {
try {
$time = DateTime::now();
$limit = 1000;
$sum = $limit;
$latestDocument = null;
while ($sum === $limit) {
$queries = [Query::limit($limit)];
if ($latestDocument !== null) {
$queries[] = Query::cursorAfter($latestDocument);
}
if ($lastSyncUpdate != null) {
$queries[] = Query::greaterThanEqual('$updatedAt', $lastSyncUpdate);
}
$queries[] = Query::equal('resourceType', ['function']);
$results = [];
try {
$results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error($th->getMessage());
}
$sum = count($results);
foreach ($results as $document) {
$domain = $document->getAttribute('domain');
if (str_ends_with($domain, System::getEnv('_APP_DOMAIN_FUNCTIONS'))) {
continue;
}
$domains->set(md5($domain), ['value' => 1]);
}
$latestDocument = !empty(array_key_last($results)) ? $results[array_key_last($results)] : null;
}
$lastSyncUpdate = $time;
if ($sum > 0) {
Console::log("Sync domains tick: {$sum} domains were updated");
}
} catch (Throwable $th) {
Console::error($th->getMessage());
}
}, DOMAIN_SYNC_TIMER, 0, function ($error) {
Console::error($error);
});
});
$http->start();
+107 -48
View File
@@ -48,6 +48,7 @@ use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Database\PDOProxy;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Cache\Adapter\Sharding;
@@ -124,7 +125,7 @@ const APP_USER_ACCESS = 24 * 60 * 60; // 24 hours
const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4318;
const APP_VERSION_STABLE = '1.6.0';
const APP_VERSION_STABLE = '1.6.1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -202,6 +203,7 @@ const DELETE_TYPE_TOPIC = 'topic';
const DELETE_TYPE_TARGET = 'target';
const DELETE_TYPE_EXPIRED_TARGETS = 'invalid_targets';
const DELETE_TYPE_SESSION_TARGETS = 'session_targets';
const DELETE_TYPE_MAINTENANCE = 'maintenance';
// Message types
const MESSAGE_SEND_TYPE_INTERNAL = 'internal';
@@ -232,6 +234,11 @@ const API_KEY_DYNAMIC = 'dynamic';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
const METRIC_WEBHOOKS_SENT = 'webhooks.events.sent';
const METRIC_WEBHOOKS_FAILED = 'webhooks.events.failed';
const METRIC_WEBHOOK_ID_SENT = '{webhookInternalId}.webhooks.events.sent';
const METRIC_WEBHOOK_ID_FAILED = '{webhookInternalId}.webhooks.events.failed';
const METRIC_AUTH_METHOD_PHONE = 'auth.method.phone';
const METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE = METRIC_AUTH_METHOD_PHONE . '.{countryCode}';
@@ -254,9 +261,15 @@ const METRIC_DOCUMENTS = 'documents';
const METRIC_DATABASE_ID_DOCUMENTS = '{databaseInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS = '{databaseInternalId}.{collectionInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE = '{databaseInternalId}.{collectionInternalId}.databases.storage';
const METRIC_DATABASES_OPERATIONS_READS = 'databases.operations.reads';
const METRIC_DATABASE_ID_OPERATIONS_READS = '{databaseInternalId}.databases.operations.reads';
const METRIC_DATABASES_OPERATIONS_WRITES = 'databases.operations.writes';
const METRIC_DATABASE_ID_OPERATIONS_WRITES = '{databaseInternalId}.databases.operations.writes';
const METRIC_BUCKETS = 'buckets';
const METRIC_FILES = 'files';
const METRIC_FILES_STORAGE = 'files.storage';
const METRIC_FILES_TRANSFORMATIONS = 'files.transformations';
const METRIC_BUCKET_ID_FILES_TRANSFORMATIONS = '{bucketInternalId}.files.transformations';
const METRIC_BUCKET_ID_FILES = '{bucketInternalId}.files';
const METRIC_BUCKET_ID_FILES_STORAGE = '{bucketInternalId}.files.storage';
const METRIC_FUNCTIONS = 'functions';
@@ -722,12 +735,19 @@ Database::addFilter(
$data = \json_decode($message->getAttribute('data', []), true);
$providerType = $message->getAttribute('providerType', '');
if ($providerType === MESSAGE_TYPE_EMAIL) {
$searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]);
} elseif ($providerType === MESSAGE_TYPE_SMS) {
$searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]);
} else {
$searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]);
switch ($providerType) {
case MESSAGE_TYPE_EMAIL:
$searchValues[] = $data['subject'];
$searchValues[] = MESSAGE_TYPE_EMAIL;
break;
case MESSAGE_TYPE_SMS:
$searchValues[] = $data['content'];
$searchValues[] = MESSAGE_TYPE_SMS;
break;
case MESSAGE_TYPE_PUSH:
$searchValues[] = $data['title'] ?? '';
$searchValues[] = MESSAGE_TYPE_PUSH;
break;
}
$search = \implode(' ', \array_filter($searchValues));
@@ -751,7 +771,7 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_DATETIME, function () {
}, Database::VAR_DATETIME);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_ENUM, function ($attribute) {
$elements = $attribute['formatOptions']['elements'];
$elements = $attribute['formatOptions']['elements'] ?? [];
return new WhiteList($elements, true);
}, Database::VAR_STRING);
@@ -854,31 +874,37 @@ $register->set('pools', function () {
$connections = [
'console' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_CONSOLE', $fallbackForDB),
'dsns' => $fallbackForDB,
'multiple' => false,
'schemes' => ['mariadb', 'mysql'],
],
'database' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_PROJECT', $fallbackForDB),
'dsns' => $fallbackForDB,
'multiple' => true,
'schemes' => ['mariadb', 'mysql'],
],
'logs' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_LOGS', $fallbackForDB),
'multiple' => false,
'schemes' => ['mariadb', 'mysql'],
],
'queue' => [
'type' => 'queue',
'dsns' => System::getEnv('_APP_CONNECTIONS_QUEUE', $fallbackForRedis),
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
'pubsub' => [
'type' => 'pubsub',
'dsns' => System::getEnv('_APP_CONNECTIONS_PUBSUB', $fallbackForRedis),
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
'cache' => [
'type' => 'cache',
'dsns' => System::getEnv('_APP_CONNECTIONS_CACHE', $fallbackForRedis),
'dsns' => $fallbackForRedis,
'multiple' => true,
'schemes' => ['redis'],
],
@@ -890,7 +916,7 @@ $register->set('pools', function () {
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
if ($multiprocessing) {
$workerCount = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
} else {
$workerCount = 1;
}
@@ -950,12 +976,12 @@ $register->set('pools', function () {
});
},
'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) {
$redis = new Redis();
$redis = new \Redis();
@$redis->pconnect($dsnHost, (int)$dsnPort);
if ($dsnPass) {
$redis->auth($dsnPass);
}
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
},
@@ -1214,12 +1240,12 @@ App::setResource('clients', function ($request, $console, $project) {
return \array_unique($clients);
}, ['request', 'console', 'project']);
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForConsole) {
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) {
/** @var Appwrite\Utopia\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var string $mode */
Authorization::setDefaultStatus(true);
@@ -1268,13 +1294,13 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$user = new Document([]);
} else {
if ($project->getId() === 'console') {
$user = $dbForConsole->getDocument('users', Auth::$unique);
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} else {
$user = $dbForProject->getDocument('users', Auth::$unique);
}
}
} else {
$user = $dbForConsole->getDocument('users', Auth::$unique);
$user = $dbForPlatform->getDocument('users', Auth::$unique);
}
if (
@@ -1317,14 +1343,14 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
}
$dbForProject->setMetadata('user', $user->getId());
$dbForConsole->setMetadata('user', $user->getId());
$dbForPlatform->setMetadata('user', $user->getId());
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForConsole']);
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform']);
App::setResource('project', function ($dbForConsole, $request, $console) {
App::setResource('project', function ($dbForPlatform, $request, $console) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
@@ -1333,10 +1359,10 @@ App::setResource('project', function ($dbForConsole, $request, $console) {
return $console;
}
$project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
return $project;
}, ['dbForConsole', 'request', 'console']);
}, ['dbForPlatform', 'request', 'console']);
App::setResource('session', function (Document $user) {
if ($user->isEmpty()) {
@@ -1401,9 +1427,9 @@ App::setResource('console', function () {
]);
}, []);
App::setResource('dbForProject', function (Group $pools, Database $dbForConsole, Cache $cache, Document $project) {
App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -1426,14 +1452,9 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole,
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -1446,9 +1467,9 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForConsole,
}
return $database;
}, ['pools', 'dbForConsole', 'cache', 'project']);
}, ['pools', 'dbForPlatform', 'cache', 'project']);
App::setResource('dbForConsole', function (Group $pools, Cache $cache) {
App::setResource('dbForPlatform', function (Group $pools, Cache $cache) {
$dbAdapter = $pools
->get('console')
->pop()
@@ -1466,12 +1487,12 @@ App::setResource('dbForConsole', function (Group $pools, Cache $cache) {
return $database;
}, ['pools', 'cache']);
App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases) {
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -1488,7 +1509,9 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -1518,7 +1541,7 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForConsole,
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
}, ['pools', 'dbForPlatform', 'cache']);
App::setResource('cache', function (Group $pools) {
$list = Config::getParam('pools-cache', []);
@@ -1535,6 +1558,27 @@ App::setResource('cache', function (Group $pools) {
return new Cache(new Sharding($adapters));
}, ['pools']);
App::setResource('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int)$port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});
App::setResource('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
};
}, ['redis']);
App::setResource('deviceForLocal', function () {
return new Local();
});
@@ -1800,7 +1844,11 @@ App::setResource('plan', function (array $plan = []) {
return [];
});
App::setResource('team', function (Document $project, Database $dbForConsole, App $utopia, Request $request) {
App::setResource('smsRates', function () {
return [];
});
App::setResource('team', function (Document $project, Database $dbForPlatform, App $utopia, Request $request) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
@@ -1810,25 +1858,36 @@ App::setResource('team', function (Document $project, Database $dbForConsole, Ap
if (str_starts_with($path, '/v1/projects/:projectId')) {
$uri = $request->getURI();
$pid = explode('/', $uri)[3];
$p = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $pid));
$p = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $pid));
$teamInternalId = $p->getAttribute('teamInternalId', '');
} elseif ($path === '/v1/projects') {
$teamId = $request->getParam('teamId', '');
$team = Authorization::skip(fn () => $dbForConsole->getDocument('teams', $teamId));
$team = Authorization::skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
return $team;
}
}
$team = Authorization::skip(function () use ($dbForConsole, $teamInternalId) {
return $dbForConsole->findOne('teams', [
$team = Authorization::skip(function () use ($dbForPlatform, $teamInternalId) {
return $dbForPlatform->findOne('teams', [
Query::equal('$internalId', [$teamInternalId]),
]);
});
return $team;
}, ['project', 'dbForConsole', 'utopia', 'request']);
}, ['project', 'dbForPlatform', 'utopia', 'request']);
App::setResource(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
App::setResource('previewHostname', function (Request $request) {
if (App::isDevelopment()) {
$host = $request->getQuery('appwrite-hostname') ?? '';
if (!empty($host)) {
return $host;
}
}
return '';
}, ['request']);
+63 -13
View File
@@ -13,7 +13,7 @@ use Swoole\Runtime;
use Swoole\Table;
use Swoole\Timer;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\Database\TimeLimit;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\App;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -29,6 +29,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\System\System;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\WebSocket\Adapter;
use Utopia\WebSocket\Server;
@@ -92,7 +93,9 @@ if (!function_exists('getProjectDB')) {
$database = new Database($adapter, getCache());
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -135,6 +138,32 @@ if (!function_exists('getCache')) {
}
}
// Allows overriding
if (!function_exists('getRedis')) {
function getRedis(): \Redis
{
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int)$port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
}
}
if (!function_exists('getTimelimit')) {
function getTimelimit(): TimeLimitRedis
{
return new TimeLimitRedis("", 0, 1, getRedis());
}
}
if (!function_exists('getRealtime')) {
function getRealtime(): Realtime
{
@@ -142,6 +171,13 @@ if (!function_exists('getRealtime')) {
}
}
if (!function_exists('getTelemetry')) {
function getTelemetry(int $workerId): Utopia\Telemetry\Adapter
{
return new NoTelemetry();
}
}
$realtime = getRealtime();
/**
@@ -157,7 +193,7 @@ $stats->create();
$containerId = uniqid();
$statsDocument = null;
$workerNumber = swoole_cpu_num() * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$workerNumber = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$adapter = new Adapter\Swoole(port: System::getEnv('PORT', 80));
$adapter
@@ -174,7 +210,7 @@ $logError = function (Throwable $error, string $action) use ($register) {
$log = new Log();
$log->setNamespace("realtime");
$log->setServer(gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
@@ -274,6 +310,12 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
Console::success('Worker ' . $workerId . ' started successfully');
$telemetry = getTelemetry($workerId);
$register->set('telemetry', fn () => $telemetry);
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
$attempts = 0;
$start = time();
@@ -416,6 +458,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
);
if (($num = count($receivers)) > 0) {
$register->get('telemetry.messageSentCounter')->add($num);
$stats->incr($event['project'], 'messages', $num);
}
});
@@ -464,7 +507,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
$dbForProject = getProjectDB($project);
$timelimit = $app->getResource('timelimit');
$console = $app->getResource('console'); /** @var Document $console */
$user = $app->getResource('user'); /** @var Document $user */
@@ -473,12 +516,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
*
* Abuse limits are connecting 128 times per minute and ip address.
*/
$timeLimit = new TimeLimit('url:{url},ip:{ip}', 128, 60, $dbForProject);
$timeLimit
$timelimit = $timelimit('url:{url},ip:{ip}', 128, 60);
$timelimit
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getURI());
$abuse = new Abuse($timeLimit);
$abuse = new Abuse($timelimit);
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'enabled' && $abuse->check()) {
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
@@ -519,6 +562,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]
]));
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($project->getId(), [
'projectId' => $project->getId(),
'teamId' => $project->getAttribute('teamId')
@@ -573,7 +619,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
*
* Abuse limits are sending 32 times per minute and connection.
*/
$timeLimit = new TimeLimit('url:{url},connection:{connection}', 32, 60, $database);
$timeLimit = getTimelimit('url:{url},connection:{connection}', 32, 60);
$timeLimit
->setParam('{connection}', $connection)
@@ -592,9 +638,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
switch ($message['type']) {
/**
* This type is used to authenticate.
*/
case 'ping':
$server->send([$connection], json_encode([
'type' => 'pong'
]));
break;
case 'authentication':
if (!array_key_exists('session', $message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
@@ -652,9 +701,10 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
});
$server->onClose(function (int $connection) use ($realtime, $stats) {
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
}
$realtime->unsubscribe($connection);
+1 -1
View File
@@ -167,7 +167,7 @@ $image = $this->getParam('image', '');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: <?php echo $organization; ?>/console:5.0.12
image: <?php echo $organization; ?>/console:5.2.27
restart: unless-stopped
networks:
- appwrite
+98 -17
View File
@@ -2,6 +2,7 @@
require_once __DIR__ . '/init.php';
use Appwrite\Certificates\LetsEncrypt;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
@@ -16,7 +17,7 @@ use Appwrite\Event\Usage;
use Appwrite\Event\UsageDump;
use Appwrite\Platform\Appwrite;
use Swoole\Runtime;
use Utopia\App;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
@@ -41,7 +42,7 @@ Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
Server::setResource('register', fn () => $register);
Server::setResource('dbForConsole', function (Cache $cache, Registry $register) {
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register) {
$pools = $register->get('pools');
$database = $pools
->get('console')
@@ -54,7 +55,7 @@ Server::setResource('dbForConsole', function (Cache $cache, Registry $register)
return $adapter;
}, ['cache', 'register']);
Server::setResource('project', function (Message $message, Database $dbForConsole) {
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
@@ -62,12 +63,12 @@ Server::setResource('project', function (Message $message, Database $dbForConsol
return $project;
}
return $dbForConsole->getDocument('projects', $project->getId());
}, ['message', 'dbForConsole']);
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForConsole) {
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
$pools = $register->get('pools');
@@ -93,7 +94,9 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -106,14 +109,14 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
}
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForConsole']);
}, ['cache', 'register', 'message', 'project', 'dbForPlatform']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForConsole, $cache) {
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForConsole, $cache, &$databases): Database {
return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForConsole;
return $dbForPlatform;
}
try {
@@ -126,7 +129,9 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -150,7 +155,9 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
$databases[$dsn->getHost()] = $database;
if ($dsn->getHost() === System::getEnv('_APP_DATABASE_SHARED_TABLES', '')) {
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getInternalId())
@@ -164,10 +171,10 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForConso
return $database;
};
}, ['pools', 'dbForConsole', 'cache']);
}, ['pools', 'dbForPlatform', 'cache']);
Server::setResource('abuseRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400));
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400);
});
Server::setResource('auditRetention', function () {
@@ -194,6 +201,27 @@ Server::setResource('cache', function (Registry $register) {
return new Cache(new Sharding($adapters));
}, ['register']);
Server::setResource('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int)$port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});
Server::setResource('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
};
}, ['redis']);
Server::setResource('log', fn () => new Log());
Server::setResource('queueForUsage', function (Connection $queue) {
@@ -277,6 +305,59 @@ Server::setResource(
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
Server::setResource('certificates', function () {
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue a LetsEncrypt SSL certificate.');
}
return new LetsEncrypt($email);
});
Server::setResource('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
foreach ($extras as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
};
}, ['register', 'project']);
$pools = $register->get('pools');
$platform = new Appwrite();
@@ -335,7 +416,7 @@ $worker
if ($logger) {
$log->setNamespace("appwrite-worker");
$log->setServer(\gethostname());
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
+7 -7
View File
@@ -45,24 +45,23 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.16.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "0.43.0",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "0.43.0",
"utopia-php/audit": "0.47.*",
"utopia-php/cache": "0.11.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.53.27",
"utopia-php/database": "0.56.4",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.2.*",
"utopia-php/fetch": "0.3.*",
"utopia-php/image": "0.7.*",
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.12.*",
"utopia-php/messaging": "0.13.*",
"utopia-php/migration": "0.6.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/platform": "0.7.1",
"utopia-php/pools": "0.5.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.7.*",
@@ -70,6 +69,7 @@
"utopia-php/storage": "0.18.*",
"utopia-php/swoole": "0.8.*",
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.8.*",
"utopia-php/websocket": "0.1.*",
"matomo/device-detector": "6.1.*",
@@ -83,7 +83,7 @@
},
"require-dev": {
"ext-fileinfo": "*",
"appwrite/sdk-generator": "0.39.*",
"appwrite/sdk-generator": "0.39.30",
"phpunit/phpunit": "9.5.20",
"swoole/ide-helper": "5.1.2",
"textalk/websocket": "1.5.7",
Generated
+60 -111
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": "0b299e8aaf279f410c6d81c1ad3a5031",
"content-hash": "e2050a253319ce2c068e000ba5787d74",
"packages": [
{
"name": "adhocore/jwt",
@@ -3134,57 +3134,6 @@
},
"time": "2023-04-05T11:54:14+00:00"
},
{
"name": "utopia-php/abuse",
"version": "0.43.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/6346a3b4c5177a43160035a7289e30fdfb0790d6",
"reference": "6346a3b4c5177a43160035a7289e30fdfb0790d6",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-pdo": "*",
"ext-redis": "*",
"php": ">=8.0",
"utopia-php/database": "0.53.*"
},
"require-dev": {
"laravel/pint": "1.5.*",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^9.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\Abuse\\": "src/Abuse"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A simple abuse library to manage application usage limits",
"keywords": [
"Abuse",
"framework",
"php",
"upf",
"utopia"
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.43.0"
},
"time": "2024-08-30T05:17:23+00:00"
},
{
"name": "utopia-php/analytics",
"version": "0.10.2",
@@ -3233,21 +3182,21 @@
},
{
"name": "utopia-php/audit",
"version": "0.43.0",
"version": "0.47.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e"
"reference": "1ebd5784ba68645073426f2f04a67726a1bde4d7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e",
"reference": "cef22b5dc6a6d28fcd522f41c7bf7ded4a4dfd3e",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/1ebd5784ba68645073426f2f04a67726a1bde4d7",
"reference": "1ebd5784ba68645073426f2f04a67726a1bde4d7",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/database": "0.53.*"
"utopia-php/database": "0.56.*"
},
"require-dev": {
"laravel/pint": "1.5.*",
@@ -3274,9 +3223,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.43.0"
"source": "https://github.com/utopia-php/audit/tree/0.47.0"
},
"time": "2024-08-30T05:17:36+00:00"
"time": "2025-01-15T02:40:53+00:00"
},
{
"name": "utopia-php/cache",
@@ -3379,16 +3328,16 @@
},
{
"name": "utopia-php/compression",
"version": "0.1.2",
"version": "0.1.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/compression.git",
"reference": "6062f70596415f8d5de40a589367b0eb2a435f98"
"reference": "66f093557ba66d98245e562036182016c7dcfe8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/compression/zipball/6062f70596415f8d5de40a589367b0eb2a435f98",
"reference": "6062f70596415f8d5de40a589367b0eb2a435f98",
"url": "https://api.github.com/repos/utopia-php/compression/zipball/66f093557ba66d98245e562036182016c7dcfe8a",
"reference": "66f093557ba66d98245e562036182016c7dcfe8a",
"shasum": ""
},
"require": {
@@ -3419,9 +3368,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/compression/issues",
"source": "https://github.com/utopia-php/compression/tree/0.1.2"
"source": "https://github.com/utopia-php/compression/tree/0.1.3"
},
"time": "2024-11-08T14:59:54+00:00"
"time": "2025-01-15T15:15:51+00:00"
},
{
"name": "utopia-php/config",
@@ -3476,16 +3425,16 @@
},
{
"name": "utopia-php/database",
"version": "0.53.27",
"version": "0.56.4",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "d3a8cae6e743a6a1a5719b860762a03aabce678a"
"reference": "240478a60797124a885ceac40046fe47c22415b7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/d3a8cae6e743a6a1a5719b860762a03aabce678a",
"reference": "d3a8cae6e743a6a1a5719b860762a03aabce678a",
"url": "https://api.github.com/repos/utopia-php/database/zipball/240478a60797124a885ceac40046fe47c22415b7",
"reference": "240478a60797124a885ceac40046fe47c22415b7",
"shasum": ""
},
"require": {
@@ -3526,9 +3475,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.53.27"
"source": "https://github.com/utopia-php/database/tree/0.56.4"
},
"time": "2024-12-02T08:53:55+00:00"
"time": "2025-01-20T09:22:08+00:00"
},
{
"name": "utopia-php/domains",
@@ -3639,16 +3588,16 @@
},
{
"name": "utopia-php/fetch",
"version": "0.2.1",
"version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/fetch.git",
"reference": "1423c0ee3eef944d816ca6e31706895b585aea82"
"reference": "02b12c05aec13399dcc2da8d51f908e328ab63f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/1423c0ee3eef944d816ca6e31706895b585aea82",
"reference": "1423c0ee3eef944d816ca6e31706895b585aea82",
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/02b12c05aec13399dcc2da8d51f908e328ab63f4",
"reference": "02b12c05aec13399dcc2da8d51f908e328ab63f4",
"shasum": ""
},
"require": {
@@ -3672,22 +3621,22 @@
"description": "A simple library that provides an interface for making HTTP Requests.",
"support": {
"issues": "https://github.com/utopia-php/fetch/issues",
"source": "https://github.com/utopia-php/fetch/tree/0.2.1"
"source": "https://github.com/utopia-php/fetch/tree/0.3.0"
},
"time": "2024-03-18T11:50:59+00:00"
"time": "2025-01-17T06:11:10+00:00"
},
{
"name": "utopia-php/framework",
"version": "0.33.15",
"version": "0.33.16",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "83b0628900c2c53e8c3efbf069f3e13050295edc"
"reference": "e91d4c560d1b809e25faa63d564fef034363b50f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/83b0628900c2c53e8c3efbf069f3e13050295edc",
"reference": "83b0628900c2c53e8c3efbf069f3e13050295edc",
"url": "https://api.github.com/repos/utopia-php/http/zipball/e91d4c560d1b809e25faa63d564fef034363b50f",
"reference": "e91d4c560d1b809e25faa63d564fef034363b50f",
"shasum": ""
},
"require": {
@@ -3719,9 +3668,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.33.15"
"source": "https://github.com/utopia-php/http/tree/0.33.16"
},
"time": "2024-12-10T13:07:04+00:00"
"time": "2025-01-16T15:58:50+00:00"
},
{
"name": "utopia-php/image",
@@ -3878,16 +3827,16 @@
},
{
"name": "utopia-php/messaging",
"version": "0.12.2",
"version": "0.13.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
"reference": "f6790fba1fcee12163d51c65d2c226a7856295d9"
"reference": "0e3e57351fe4fe875ef3ab9a01a7fff5f022de90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/f6790fba1fcee12163d51c65d2c226a7856295d9",
"reference": "f6790fba1fcee12163d51c65d2c226a7856295d9",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/0e3e57351fe4fe875ef3ab9a01a7fff5f022de90",
"reference": "0e3e57351fe4fe875ef3ab9a01a7fff5f022de90",
"shasum": ""
},
"require": {
@@ -3898,9 +3847,9 @@
"phpmailer/phpmailer": "6.9.1"
},
"require-dev": {
"laravel/pint": "1.13.11",
"phpstan/phpstan": "1.10.58",
"phpunit/phpunit": "10.5.10"
"laravel/pint": "1.*",
"phpstan/phpstan": "1.*",
"phpunit/phpunit": "11.*"
},
"type": "library",
"autoload": {
@@ -3923,22 +3872,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
"source": "https://github.com/utopia-php/messaging/tree/0.12.2"
"source": "https://github.com/utopia-php/messaging/tree/0.13.0"
},
"time": "2024-10-22T01:02:20+00:00"
"time": "2024-12-05T08:36:07+00:00"
},
{
"name": "utopia-php/migration",
"version": "0.6.14",
"version": "0.6.15",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "59a19f09ded0ccab4c8cca35b1242c01e2b9cfd2"
"reference": "e849ec3e7ad38f5f5273ebb0132b112639cdf01c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/59a19f09ded0ccab4c8cca35b1242c01e2b9cfd2",
"reference": "59a19f09ded0ccab4c8cca35b1242c01e2b9cfd2",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/e849ec3e7ad38f5f5273ebb0132b112639cdf01c",
"reference": "e849ec3e7ad38f5f5273ebb0132b112639cdf01c",
"shasum": ""
},
"require": {
@@ -3946,7 +3895,7 @@
"ext-curl": "*",
"ext-openssl": "*",
"php": "8.3.*",
"utopia-php/database": "0.53.*",
"utopia-php/database": "0.56.*",
"utopia-php/dsn": "0.2.*",
"utopia-php/framework": "0.33.*",
"utopia-php/storage": "0.18.*"
@@ -3979,9 +3928,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/0.6.14"
"source": "https://github.com/utopia-php/migration/tree/0.6.15"
},
"time": "2025-01-08T01:07:25+00:00"
"time": "2025-01-15T04:55:08+00:00"
},
{
"name": "utopia-php/mongo",
@@ -4095,16 +4044,16 @@
},
{
"name": "utopia-php/platform",
"version": "0.7.2",
"version": "0.7.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "6f9243848f1c6466f6509fd01c7e18306a6d8caf"
"reference": "3433a0f1a54988f2a59c735f507745cb2c24638a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/6f9243848f1c6466f6509fd01c7e18306a6d8caf",
"reference": "6f9243848f1c6466f6509fd01c7e18306a6d8caf",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/3433a0f1a54988f2a59c735f507745cb2c24638a",
"reference": "3433a0f1a54988f2a59c735f507745cb2c24638a",
"shasum": ""
},
"require": {
@@ -4139,9 +4088,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
"source": "https://github.com/utopia-php/platform/tree/0.7.2"
"source": "https://github.com/utopia-php/platform/tree/0.7.1"
},
"time": "2025-01-15T05:56:26+00:00"
"time": "2024-10-22T10:27:49+00:00"
},
{
"name": "utopia-php/pools",
@@ -4807,16 +4756,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.39.29",
"version": "0.39.30",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "a9c3f6076ec162588dac7b0a741bc1a2c3d1a2b7"
"reference": "830198d501f51163514305befefb775106a7198b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/a9c3f6076ec162588dac7b0a741bc1a2c3d1a2b7",
"reference": "a9c3f6076ec162588dac7b0a741bc1a2c3d1a2b7",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/830198d501f51163514305befefb775106a7198b",
"reference": "830198d501f51163514305befefb775106a7198b",
"shasum": ""
},
"require": {
@@ -4852,9 +4801,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.39.29"
"source": "https://github.com/appwrite/sdk-generator/tree/0.39.30"
},
"time": "2025-01-07T05:28:35+00:00"
"time": "2025-01-20T06:10:03+00:00"
},
{
"name": "doctrine/annotations",
+7 -2
View File
@@ -193,11 +193,14 @@ services:
- _APP_EXPERIMENT_LOGGING_PROVIDER
- _APP_EXPERIMENT_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_DATABASE_SHARED_NAMESPACE
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:5.0.12
image: appwrite/console:5.2.27
restart: unless-stopped
networks:
- appwrite
@@ -382,6 +385,8 @@ services:
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_DATABASE_SHARED_TABLES
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_EMAIL_CERTIFICATES
appwrite-worker-databases:
entrypoint: worker-databases
@@ -864,7 +869,7 @@ services:
appwrite-assistant:
container_name: appwrite-assistant
image: appwrite/assistant:0.5.0
image: appwrite/assistant:0.7.0
networks:
- appwrite
environment:
@@ -12,5 +12,6 @@ mutation {
providerId
providerType
identifier
expired
}
}
@@ -33,6 +33,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -28,6 +28,7 @@ query {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -30,6 +30,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -30,6 +30,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -31,6 +31,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -30,6 +30,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -11,5 +11,6 @@ mutation {
providerId
providerType
identifier
expired
}
}
@@ -28,6 +28,7 @@ mutation {
providerId
providerType
identifier
expired
}
accessedAt
}
@@ -17,6 +17,7 @@ mutation {
providerId
providerType
identifier
expired
}
userId
userName
@@ -3,4 +3,5 @@ appwrite databases updateBooleanAttribute \
--collectionId <COLLECTION_ID> \
--key '' \
--required false \
--default false
--default false \
@@ -3,4 +3,5 @@ appwrite databases updateDatetimeAttribute \
--collectionId <COLLECTION_ID> \
--key '' \
--required false \
--default ''
--default '' \
@@ -3,4 +3,5 @@ appwrite databases updateEmailAttribute \
--collectionId <COLLECTION_ID> \
--key '' \
--required false \
--default email@example.com
--default email@example.com \
@@ -4,4 +4,5 @@ appwrite databases updateEnumAttribute \
--key '' \
--elements one two three \
--required false \
--default <DEFAULT>
--default <DEFAULT> \
@@ -5,4 +5,5 @@ appwrite databases updateFloatAttribute \
--required false \
--min null \
--max null \
--default null
--default null \
@@ -5,4 +5,5 @@ appwrite databases updateIntegerAttribute \
--required false \
--min null \
--max null \
--default null
--default null \
@@ -3,4 +3,5 @@ appwrite databases updateIpAttribute \
--collectionId <COLLECTION_ID> \
--key '' \
--required false \
--default ''
--default '' \
@@ -3,3 +3,4 @@ appwrite databases updateRelationshipAttribute \
--collectionId <COLLECTION_ID> \
--key '' \
@@ -3,4 +3,6 @@ appwrite databases updateStringAttribute \
--collectionId <COLLECTION_ID> \
--key '' \
--required false \
--default <DEFAULT>
--default <DEFAULT> \
@@ -3,4 +3,5 @@ appwrite databases updateUrlAttribute \
--collectionId <COLLECTION_ID> \
--key '' \
--required false \
--default https://example.com
--default https://example.com \
@@ -1,7 +1,10 @@
appwrite messaging createPush \
--messageId <MESSAGE_ID> \
--title <TITLE> \
--body <BODY> \
@@ -15,3 +15,6 @@ appwrite messaging updatePush \
@@ -0,0 +1,5 @@
appwrite projects updateMembershipsPrivacy \
--projectId <PROJECT_ID> \
--userName false \
--userEmail false \
--mfa false
@@ -12,7 +12,7 @@ const result = await databases.updateStringAttribute(
'', // key
false, // required
'<DEFAULT>', // default
null, // size (optional)
1, // size (optional)
'' // newKey (optional)
);
@@ -1,4 +1,4 @@
import { Client, Messaging } from "@appwrite.io/console";
import { Client, Messaging, MessagePriority } from "@appwrite.io/console";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
@@ -8,8 +8,8 @@ const messaging = new Messaging(client);
const result = await messaging.createPush(
'<MESSAGE_ID>', // messageId
'<TITLE>', // title
'<BODY>', // body
'<TITLE>', // title (optional)
'<BODY>', // body (optional)
[], // topics (optional)
[], // users (optional)
[], // targets (optional)
@@ -20,9 +20,12 @@ const result = await messaging.createPush(
'<SOUND>', // sound (optional)
'<COLOR>', // color (optional)
'<TAG>', // tag (optional)
'<BADGE>', // badge (optional)
null, // badge (optional)
false, // draft (optional)
'' // scheduledAt (optional)
'', // scheduledAt (optional)
false, // contentAvailable (optional)
false, // critical (optional)
MessagePriority.Normal // priority (optional)
);
console.log(result);
@@ -1,4 +1,4 @@
import { Client, Messaging } from "@appwrite.io/console";
import { Client, Messaging, MessagePriority } from "@appwrite.io/console";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
@@ -22,7 +22,10 @@ const result = await messaging.updatePush(
'<TAG>', // tag (optional)
null, // badge (optional)
false, // draft (optional)
'' // scheduledAt (optional)
'', // scheduledAt (optional)
false, // contentAvailable (optional)
false, // critical (optional)
MessagePriority.Normal // priority (optional)
);
console.log(result);
@@ -0,0 +1,16 @@
import { Client, Projects } from "@appwrite.io/console";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID
const projects = new Projects(client);
const result = await projects.updateMembershipsPrivacy(
'<PROJECT_ID>', // projectId
false, // userName
false, // userEmail
false // mfa
);
console.log(result);
@@ -13,6 +13,6 @@ AttributeString result = await databases.updateStringAttribute(
key: '',
xrequired: false,
xdefault: '<DEFAULT>',
size: 0, // (optional)
size: 1, // (optional)
newKey: '', // (optional)
);
@@ -9,8 +9,8 @@ Messaging messaging = Messaging(client);
Message result = await messaging.createPush(
messageId: '<MESSAGE_ID>',
title: '<TITLE>',
body: '<BODY>',
title: '<TITLE>', // (optional)
body: '<BODY>', // (optional)
topics: [], // (optional)
users: [], // (optional)
targets: [], // (optional)
@@ -21,7 +21,10 @@ Message result = await messaging.createPush(
sound: '<SOUND>', // (optional)
color: '<COLOR>', // (optional)
tag: '<TAG>', // (optional)
badge: '<BADGE>', // (optional)
badge: 0, // (optional)
draft: false, // (optional)
scheduledAt: '', // (optional)
contentAvailable: false, // (optional)
critical: false, // (optional)
priority: MessagePriority.normal, // (optional)
);
@@ -24,4 +24,7 @@ Message result = await messaging.updatePush(
badge: 0, // (optional)
draft: false, // (optional)
scheduledAt: '', // (optional)
contentAvailable: false, // (optional)
critical: false, // (optional)
priority: MessagePriority.normal, // (optional)
);
@@ -13,6 +13,6 @@ const response = await databases.updateStringAttribute(
'', // key
false, // required
'<DEFAULT>', // default
null, // size (optional)
1, // size (optional)
'' // newKey (optional)
);
@@ -1,4 +1,4 @@
import { Client, Messaging } from "https://deno.land/x/appwrite/mod.ts";
import { Client, Messaging, MessagePriority } from "https://deno.land/x/appwrite/mod.ts";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
@@ -9,8 +9,8 @@ const messaging = new Messaging(client);
const response = await messaging.createPush(
'<MESSAGE_ID>', // messageId
'<TITLE>', // title
'<BODY>', // body
'<TITLE>', // title (optional)
'<BODY>', // body (optional)
[], // topics (optional)
[], // users (optional)
[], // targets (optional)
@@ -21,7 +21,10 @@ const response = await messaging.createPush(
'<SOUND>', // sound (optional)
'<COLOR>', // color (optional)
'<TAG>', // tag (optional)
'<BADGE>', // badge (optional)
null, // badge (optional)
false, // draft (optional)
'' // scheduledAt (optional)
'', // scheduledAt (optional)
false, // contentAvailable (optional)
false, // critical (optional)
MessagePriority.Normal // priority (optional)
);
@@ -1,4 +1,4 @@
import { Client, Messaging } from "https://deno.land/x/appwrite/mod.ts";
import { Client, Messaging, MessagePriority } from "https://deno.land/x/appwrite/mod.ts";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
@@ -23,5 +23,8 @@ const response = await messaging.updatePush(
'<TAG>', // tag (optional)
null, // badge (optional)
false, // draft (optional)
'' // scheduledAt (optional)
'', // scheduledAt (optional)
false, // contentAvailable (optional)
false, // critical (optional)
MessagePriority.Normal // priority (optional)
);
@@ -15,6 +15,6 @@ AttributeString result = await databases.UpdateStringAttribute(
key: "",
required: false,
default: "<DEFAULT>",
size: 0, // optional
size: 1, // optional
newKey: "" // optional
);
@@ -1,4 +1,5 @@
using Appwrite;
using Appwrite.Enums;
using Appwrite.Models;
using Appwrite.Services;
@@ -11,8 +12,8 @@ Messaging messaging = new Messaging(client);
Message result = await messaging.CreatePush(
messageId: "<MESSAGE_ID>",
title: "<TITLE>",
body: "<BODY>",
title: "<TITLE>", // optional
body: "<BODY>", // optional
topics: new List<string>(), // optional
users: new List<string>(), // optional
targets: new List<string>(), // optional
@@ -23,7 +24,10 @@ Message result = await messaging.CreatePush(
sound: "<SOUND>", // optional
color: "<COLOR>", // optional
tag: "<TAG>", // optional
badge: "<BADGE>", // optional
badge: 0, // optional
draft: false, // optional
scheduledAt: "" // optional
scheduledAt: "", // optional
contentAvailable: false, // optional
critical: false, // optional
priority: MessagePriority.Normal // optional
);
@@ -1,4 +1,5 @@
using Appwrite;
using Appwrite.Enums;
using Appwrite.Models;
using Appwrite.Services;
@@ -25,5 +26,8 @@ Message result = await messaging.UpdatePush(
tag: "<TAG>", // optional
badge: 0, // optional
draft: false, // optional
scheduledAt: "" // optional
scheduledAt: "", // optional
contentAvailable: false, // optional
critical: false, // optional
priority: MessagePriority.Normal // optional
);
@@ -20,7 +20,7 @@ func main() {
"",
false,
"<DEFAULT>",
databases.WithUpdateStringAttributeSize(0),
databases.WithUpdateStringAttributeSize(1),
databases.WithUpdateStringAttributeNewKey(""),
)
@@ -16,8 +16,8 @@ func main() {
service := messaging.NewMessaging(client)
response, error := service.CreatePush(
"<MESSAGE_ID>",
"<TITLE>",
"<BODY>",
messaging.WithCreatePushTitle("<TITLE>"),
messaging.WithCreatePushBody("<BODY>"),
messaging.WithCreatePushTopics([]interface{}{}),
messaging.WithCreatePushUsers([]interface{}{}),
messaging.WithCreatePushTargets([]interface{}{}),
@@ -28,9 +28,12 @@ func main() {
messaging.WithCreatePushSound("<SOUND>"),
messaging.WithCreatePushColor("<COLOR>"),
messaging.WithCreatePushTag("<TAG>"),
messaging.WithCreatePushBadge("<BADGE>"),
messaging.WithCreatePushBadge(0),
messaging.WithCreatePushDraft(false),
messaging.WithCreatePushScheduledAt(""),
messaging.WithCreatePushContentAvailable(false),
messaging.WithCreatePushCritical(false),
messaging.WithCreatePushPriority("normal"),
)
if error != nil {
@@ -31,6 +31,9 @@ func main() {
messaging.WithUpdatePushBadge(0),
messaging.WithUpdatePushDraft(false),
messaging.WithUpdatePushScheduledAt(""),
messaging.WithUpdatePushContentAvailable(false),
messaging.WithUpdatePushCritical(false),
messaging.WithUpdatePushPriority("normal"),
)
if error != nil {

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