Compare commits

..
Author SHA1 Message Date
Jake Barnby d4ded4c61b Fixes for subkeys 2025-02-04 21:57:31 +13:00
Jake Barnby c9e379899b Still pass ID to handleDatabaseStorage 2025-02-04 18:44:36 +13:00
Jake Barnby 199df72219 Fix ID not changed per handleDatabase iteration 2025-02-04 18:37:56 +13:00
Jake Barnby e8bd6c998b Logging 2025-02-04 17:11:25 +13:00
Jake Barnby 0f9805cd9b No size caching 2025-02-03 21:51:47 +13:00
Jake Barnby ca36c12414 Add storage test case 2025-02-03 18:45:53 +13:00
Jake Barnby f5b3e4758c Add logging 2025-01-29 00:16:44 +13:00
131 changed files with 8052 additions and 5817 deletions
-3
View File
@@ -39,7 +39,6 @@ _APP_STORAGE_S3_ACCESS_KEY=
_APP_STORAGE_S3_SECRET=
_APP_STORAGE_S3_REGION=us-east-1
_APP_STORAGE_S3_BUCKET=
_APP_STORAGE_S3_ENDPOINT=
_APP_STORAGE_DO_SPACES_ACCESS_KEY=
_APP_STORAGE_DO_SPACES_SECRET=
_APP_STORAGE_DO_SPACES_REGION=us-east-1
@@ -87,7 +86,6 @@ _APP_MAINTENANCE_RETENTION_EXECUTION=1209600
_APP_MAINTENANCE_RETENTION_ABUSE=86400
_APP_MAINTENANCE_RETENTION_AUDIT=1209600
_APP_USAGE_AGGREGATION_INTERVAL=30
_APP_STATS_RESOURCES_INTERVAL=3600
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400
_APP_USAGE_STATS=enabled
@@ -112,4 +110,3 @@ _APP_MESSAGE_PUSH_TEST_DSN=
_APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
_APP_PROJECT_REGIONS=default
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
-5
View File
@@ -13,13 +13,8 @@ on:
schedule:
- cron: '0 16 * * 0'
permissions:
contents: read
jobs:
analyze:
permissions:
security-events: write
name: Analyze
runs-on: ubuntu-latest
+8 -83
View File
@@ -8,33 +8,9 @@ env:
IMAGE: appwrite-dev
CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }}
on: [ pull_request ]
on: [pull_request]
jobs:
check_database_changes:
name: Check if utopia-php/database changed
runs-on: ubuntu-latest
outputs:
database_changed: ${{ steps.check.outputs.database_changed }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Fetch base branch
run: git fetch origin ${{ github.event.pull_request.base.ref }}
- name: Check for utopia-php/database changes
id: check
run: |
if git diff origin/${{ github.event.pull_request.base.ref }} HEAD -- composer.lock | grep -q '"name": "utopia-php/database"'; then
echo "Database version changed, going to run all mode tests."
echo "database_changed=true" >> "$GITHUB_ENV"
echo "database_changed=true" >> "$GITHUB_OUTPUT"
else
echo "database_changed=false" >> "$GITHUB_ENV"
echo "database_changed=false" >> "$GITHUB_OUTPUT"
fi
setup:
name: Setup & Build Appwrite Image
runs-on: ubuntu-latest
@@ -127,62 +103,6 @@ jobs:
name: E2E Service Test
runs-on: ubuntu-latest
needs: setup
strategy:
fail-fast: false
matrix:
service: [
Account,
Avatars,
Console,
Databases,
Functions,
FunctionsSchedule,
GraphQL,
Health,
Locale,
Projects,
Realtime,
Storage,
Teams,
Users,
Webhooks,
VCS,
Messaging,
Migrations
]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
sleep 30
- name: Run ${{ matrix.service }} tests with Project table mode
run: |
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
export _APP_DATABASE_SHARED_TABLES_V1=
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
e2e_shared_mode_test:
name: E2E Shared Mode Service Test
runs-on: ubuntu-latest
needs: [ setup, check_database_changes ]
if: needs.check_database_changes.outputs.database_changed == 'true'
strategy:
fail-fast: false
matrix:
@@ -208,6 +128,7 @@ jobs:
Migrations
]
tables-mode: [
'Project',
'Shared V1',
'Shared V2',
]
@@ -239,8 +160,12 @@ jobs:
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 \
@@ -326,4 +251,4 @@ jobs:
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 -1
View File
@@ -16,4 +16,4 @@ dev/yasd_init.php
.phpunit.result.cache
Makefile
appwrite.json
/.zed/
/.zed/
+13 -13
View File
@@ -1,6 +1,6 @@
# Contributing
We would :heart: you to contribute to Appwrite and help make it better! We want contributing to Appwrite to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, and new docs, as well as updates and tweaks, blog posts, workshops, and more.
We would ❤️ you to contribute to Appwrite and help make it better! We want contributing to Appwrite to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, and new docs, as well as updates and tweaks, blog posts, workshops, and more.
## Here for Hacktoberfest?
If you're here to contribute during Hacktoberfest, we're so happy to see you here. Appwrite has been a long-time participant of Hacktoberfest and we welcome you, whatever your experience level. This year, we're **only taking contributions for issues tagged** `hacktoberfest`, so we can focus our resources to support your contributions.
@@ -9,13 +9,13 @@ You can [find issues using this query](https://github.com/search?q=org%3Aappwrit
## How to Start?
If you are worried or dont know where to start, check out the next section that explains what kind of help we could use and where you can get involved. You can send your questions to [@appwrite on Twitter](https://twitter.com/appwrite) or to anyone from the [Appwrite team on Discord](https://appwrite.io/discord). You can also submit an issue, and a maintainer can guide you!
If you are worried or dont know where to start, check out the next section that explains what kind of help we could use and where you can get involved. You can send your questions to [@appwrite](https://twitter.com/appwrite) on Twitter or to anyone from the [Appwrite team on Discord](https://appwrite.io/discord). You can also submit an issue, and a maintainer can guide you!
## Code of Conduct
Help us keep Appwrite open and inclusive. Please read and follow our [Code of Conduct](https://github.com/appwrite/.github/blob/main/CODE_OF_CONDUCT.md).
## Submit a Pull Request :rocket:
## Submit a Pull Request 🚀
Branch naming convention is as following
@@ -65,13 +65,13 @@ Now, go a step further by running the linter using the following command to manu
composer lint <your file path>
```
This will give you a list of errors to rectify. If you need more information on the errors, you can pass in additional command line arguments to get more verbose information. More lists of available arguments can be found [on PHP_Codesniffer usage Wiki](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage). A very useful command line argument is `--report=diff`. This will give you the expected changes by the linter for easy fixing of formatting issues.
This will give you a list of errors to rectify. If you need more information on the errors, you can pass in additional command line arguments to get more verbose information. More lists of available arguments can be found [here](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage). A very useful command line argument is `--report=diff`. This will give you the expected changes by the linter for easy fixing of formatting issues.
```bash
composer lint --report=diff <your file path>
```
5. Push changes to GitHub
5. Push changes to GitHub.
```
$ git push origin [name_of_your_new_branch]
@@ -301,7 +301,7 @@ Adding a new dependency should have vital value for the product with minimum pos
## Introducing New Features
We would :sparkling_heart: you to contribute to Appwrite, but we also want to ensure Appwrite is loyal to its vision and mission statement :pray:.
We would 💖 you to contribute to Appwrite, but we also want to ensure Appwrite is loyal to its vision and mission statement 🙏.
For us to find the right balance, please open an issue explaining your ideas before introducing a new pull request.
@@ -367,7 +367,7 @@ In file `app/controllers/shared/api.php` On the database listener, add to an exi
```php
case $document->getCollection() === 'teams':
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_TEAMS, $value); // per project
break;
```
@@ -379,10 +379,10 @@ In that case you need also to handle children removal using addReduce() method c
```php
case $document->getCollection() === 'buckets': //buckets
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_BUCKETS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
$queueForUsage
->addReduce($document);
}
break;
@@ -428,16 +428,16 @@ public function __construct()
->inject('dbForProject')
->inject('queueForFunctions')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('log')
->callback(fn (Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log) => $this->action($message, $dbForProject, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $log));
->callback(fn (Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Log $log) => $this->action($message, $dbForProject, $queueForFunctions, $queueForEvents, $queueForUsage, $log));
}
```
and then trigger the queue with the new metric like so:
```php
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_BUILDS, 1)
->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0))
->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000)
@@ -640,7 +640,7 @@ Pull requests are great, but there are many other ways you can help Appwrite.
### Blogging & Speaking
Blogging, speaking about, or creating tutorials about one of Appwrites many features are great ways to get the word out about Appwrite. Mention [@appwrite on Twitter](https://twitter.com/appwrite) and/or [email team@appwrite.io](mailto:team@appwrite.io) so we can give pointers and tips and help you spread the word by promoting your content on the different Appwrite communication channels. Please add your blog posts and videos of talks to our [Awesome Appwrite](https://github.com/appwrite/awesome-appwrite) repo on GitHub.
Blogging, speaking about, or creating tutorials about one of Appwrites many features are great ways to get the word out about Appwrite. Mention [@appwrite](https://twitter.com/appwrite) on Twitter and/or [email team@appwrite.io](mailto:team@appwrite.io) so we can give pointers and tips and help you spread the word by promoting your content on the different Appwrite communication channels. Please add your blog posts and videos of talks to our [Awesome Appwrite](https://github.com/appwrite/awesome-appwrite) repo on GitHub.
### Presenting at Meetups
+3 -5
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.10.1 AS final
FROM appwrite/base:0.9.5 AS final
LABEL maintainer="team@appwrite.io"
@@ -85,10 +85,8 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/worker-messaging && \
chmod +x /usr/local/bin/worker-migrations && \
chmod +x /usr/local/bin/worker-webhooks && \
chmod +x /usr/local/bin/worker-stats-usage && \
chmod +x /usr/local/bin/worker-stats-usage-dump && \
chmod +x /usr/local/bin/stats-resources && \
chmod +x /usr/local/bin/worker-stats-resources
chmod +x /usr/local/bin/worker-usage && \
chmod +x /usr/local/bin/worker-usage-dump
# Letsencrypt Permissions
RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
+22 -58
View File
@@ -1,8 +1,8 @@
> 好消息!Appwrite 云现已进入公开测试版!立即访问 cloud.appwrite.io 注册,体验无忧的托管服务。今天就加入我们的云端吧!:cloud: :tada:
> 好消息!Appwrite 云现已进入公开测试版!立即访问 cloud.appwrite.io 注册,体验无忧的托管服务。今天就加入我们的云端吧!☁️🎉
<br />
<p align="center">
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner with logo and slogan build like a team of hundreds""></a>
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite Logo"></a>
<br />
<br />
<b>适用于[Flutter/Vue/Angular/React/iOS/Android/* 等等平台 *]的完整后端服务</b>
@@ -36,8 +36,6 @@ Appwrite 可以提供给开发者用户验证,外部授权,用户数据读
内容:
- [开始](#开始)
- [安装](#安装)
- [Unix](#unix)
- [Windows](#windows)
@@ -56,9 +54,6 @@ Appwrite 可以提供给开发者用户验证,外部授权,用户数据读
- [订阅我们](#订阅我们)
- [版权说明](#版权说明)
## 开始
要轻松开始使用Appwrite,您可以[**免费注册Appwrite Cloud**](https://cloud.appwrite.io/)。在Appwrite Cloud公开测试版期间,您可以完全免费使用Appwrite,而且我们不会收集您的信用卡信息。
## 安装
Appwrite 的容器化服务器只需要一行指令就可以运行。您可以使用 docker-compose 在本地主机上运行 Appwrite,也可以在任何其他容器化工具(如 [Kubernetes](https://kubernetes.io/docs/home/)、[Docker Swarm](https://docs.docker.com/engine/swarm/) 或 [Rancher](https://rancher.com/docs/))上运行 Appwrite。
@@ -103,42 +98,7 @@ docker run -it --rm `
### 从旧版本升级
如果您从旧版本升级 Appwrite 服务器,则应在设置完成后使用 Appwrite 迁移工具。有关这方面的更多信息,请查看 [安装文档](https://appwrite.io/docs/self-hosting)。
## 一键配置
除了在本地运行 Appwrite,您还可以使用预配置的设置启动 Appwrite。这样可以让您快速启动并运行 Appwrite,而无需在本地计算机上安装 Docker。
请从以下提供商中选择一个:
<table border="0">
<tr>
<td align="center" width="100" height="100">
<a href="https://marketplace.digitalocean.com/apps/appwrite">
<img width="50" height="39" src="public/images/integrations/digitalocean-logo.svg" alt="DigitalOcean Logo" />
<br /><sub><b>DigitalOcean</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://gitpod.io/#https://github.com/appwrite/integration-for-gitpod">
<img width="50" height="39" src="public/images/integrations/gitpod-logo.svg" alt="Gitpod Logo" />
<br /><sub><b>Gitpod</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://www.linode.com/marketplace/apps/appwrite/appwrite/">
<img width="50" height="39" src="public/images/integrations/akamai-logo.svg" alt="Akamai Logo" />
<br /><sub><b>Akamai Compute</b></sub></a>
</a>
</td>
<td align="center" width="100" height="100">
<a href="https://aws.amazon.com/marketplace/pp/prodview-2hiaeo2px4md6">
<img width="50" height="39" src="public/images/integrations/aws-logo.svg" alt="AWS Logo" />
<br /><sub><b>AWS Marketplace</b></sub></a>
</a>
</td>
</tr>
</table>
如果您从旧版本升级 Appwrite 服务器,则应在设置完成后使用 Appwrite 迁移工具。有关这方面的更多信息,请查看 [安装文档](https://appwrite.io/docs/installation)。
## 入门
@@ -186,25 +146,29 @@ docker run -it --rm `
以下是当前支持的平台和语言列表。如果您想帮助我们为您选择的平台添加支持,您可以访问我们的 [SDK 生成器](https://github.com/appwrite/sdk-generator) 项目并查看我们的 [贡献指南](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md)。
#### 客户端
* :white_check_mark: &nbsp; [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护)
- &nbsp; [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护)
- &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护)
- &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) (由 Appwrite 团队维护)
- ✅ &nbsp; [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护)
- ✅ &nbsp; [React Native](https://github.com/appwrite/sdk-for-react-native) - **公测** (由 Appwrite 团队维护)
#### 服务器
* :white_check_mark: &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart) - (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno) - **公测** (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin) - **公测** (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
* :white_check_mark: &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护)
- &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护)
- &nbsp; [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwrite 团队维护)
- &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart) (由 Appwrite 团队维护)
- &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno) (由 Appwrite 团队维护)
- &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护)
- &nbsp; [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护)
- &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (由 Appwrite 团队维护)
- &nbsp; [Swift](https://github.com/appwrite/sdk-for-swift) (由 Appwrite 团队维护)
- ✅ &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护)
#### 开发者社区
* :white_check_mark: &nbsp; [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/))
* :white_check_mark: &nbsp; [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (维护者 [fenix-hub @GodotNuts](https://github.com/fenix-hub))
- ✅ &nbsp; [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/))
- ✅ &nbsp; [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (维护者 [fenix-hub @GodotNuts](https://github.com/fenix-hub))
找不到需要的的 SDK? - 欢迎通过发起 PR 来帮助我们完善 Appwrite 的软件生态环境 [SDK 生成器](https://github.com/appwrite/sdk-generator)!
+31 -35
View File
@@ -1,8 +1,8 @@
> Appwrite Init has concluded! You can check out all the latest announcements [on our Init website](https://appwrite.io/init) :rocket:
> Appwrite Init has concluded! You can check out all the latest announcements [on our Init website](https://appwrite.io/init) 🚀
<br />
<p align="center">
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "Build Like a Team of Hundreds"></a>
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite Logo"></a>
<br />
<br />
<b>Appwrite is a backend platform for developing Web, Mobile, and Flutter applications. Built with the open source community and optimized for developer experience in the coding languages you love.</b>
@@ -12,11 +12,11 @@
<!-- [![Build Status](https://img.shields.io/travis/com/appwrite/appwrite?style=flat-square)](https://travis-ci.com/appwrite/appwrite) -->
[![We're Hiring label](https://img.shields.io/static/v1?label=We're&message=Hiring&color=blue&style=flat-square)](https://appwrite.io/company/careers)
[![Hacktoberfest label](https://img.shields.io/static/v1?label=hacktoberfest&message=ready&color=191120&style=flat-square)](https://hacktoberfest.appwrite.io)
[![Discord label](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord?r=Github)
[![Build Status label](https://img.shields.io/github/actions/workflow/status/appwrite/appwrite/tests.yml?branch=master&label=tests&style=flat-square)](https://github.com/appwrite/appwrite/actions)
[![X Account label](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
[![We're Hiring](https://img.shields.io/static/v1?label=We're&message=Hiring&color=blue&style=flat-square)](https://appwrite.io/company/careers)
[![Hacktoberfest](https://img.shields.io/static/v1?label=hacktoberfest&message=ready&color=191120&style=flat-square)](https://hacktoberfest.appwrite.io)
[![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord?r=Github)
[![Build Status](https://img.shields.io/github/actions/workflow/status/appwrite/appwrite/tests.yml?branch=master&label=tests&style=flat-square)](https://github.com/appwrite/appwrite/actions)
[![X Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
<!-- [![Docker Pulls](https://img.shields.io/docker/pulls/appwrite/appwrite?color=f02e65&style=flat-square)](https://hub.docker.com/r/appwrite/appwrite) -->
<!-- [![Translate](https://img.shields.io/badge/translate-f02e65?style=flat-square)](docs/tutorials/add-translations.md) -->
@@ -37,14 +37,13 @@ Using Appwrite, you can easily integrate your app with user authentication and m
<br />
</p>
![Appwrite project dashboard showing various Appwrite features](public/images/github.png)
![Appwrite](public/images/github.png)
Find out more at: [https://appwrite.io](https://appwrite.io).
Find out more at: [https://appwrite.io](https://appwrite.io)
Table of Contents:
- [Getting Started](#getting-started)
- [Self-Hosting](#self-hosting)
- [Installation](#installation)
- [Unix](#unix)
- [Windows](#windows)
- [CMD](#cmd)
@@ -63,14 +62,11 @@ Table of Contents:
- [Follow Us](#follow-us)
- [License](#license)
## Getting Started
The easiest way to get started with Appwrite is by [signing up for Appwrite Cloud](https://cloud.appwrite.io/). While Appwrite Cloud is in public beta, you can build with Appwrite completely free, and we won't collect you credit card information.
## Self-Hosting
## Installation
Appwrite is designed to run in a containerized environment. Running your server is as easy as running one command from your terminal. You can either run Appwrite on your localhost using docker-compose or on any other container orchestration tool, such as [Kubernetes](https://kubernetes.io/docs/home/), [Docker Swarm](https://docs.docker.com/engine/swarm/), or [Rancher](https://rancher.com/docs/).
Before running the installation command, make sure you have [Docker](https://www.docker.com/products/docker-desktop) installed on your machine:
The easiest way to start running your Appwrite server is by running our docker-compose file. Before running the installation command, make sure you have [Docker](https://www.docker.com/products/docker-desktop) installed on your machine:
### Unix
@@ -110,7 +106,7 @@ For advanced production and custom installation, check out our Docker [environme
### Upgrade from an Older Version
If you are upgrading your Appwrite server from an older version, you should use the Appwrite migration tool once your setup is completed. For more information regarding this, check out the [Installation Docs](https://appwrite.io/docs/self-hosting).
If you are upgrading your Appwrite server from an older version, you should use the Appwrite migration tool once your setup is completed. For more information regarding this, check out the [Installation Docs](https://appwrite.io/docs/installation).
## One-Click Setups
@@ -196,34 +192,34 @@ Below is a list of currently supported platforms and languages. If you would lik
#### Client
- :white_check_mark: &nbsp; [Web](https://github.com/appwrite/sdk-for-web) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Android](https://github.com/appwrite/sdk-for-android) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [React Native](https://github.com/appwrite/sdk-for-react-native) - **Beta** (Maintained by the Appwrite Team)
- &nbsp; [Web](https://github.com/appwrite/sdk-for-web) (Maintained by the Appwrite Team)
- &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter) (Maintained by the Appwrite Team)
- &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) (Maintained by the Appwrite Team)
- &nbsp; [Android](https://github.com/appwrite/sdk-for-android) (Maintained by the Appwrite Team)
- &nbsp; [React Native](https://github.com/appwrite/sdk-for-react-native) - **Beta** (Maintained by the Appwrite Team)
#### Server
- :white_check_mark: &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [PHP](https://github.com/appwrite/sdk-for-php) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Python](https://github.com/appwrite/sdk-for-python) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team)
- &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node) (Maintained by the Appwrite Team)
- &nbsp; [PHP](https://github.com/appwrite/sdk-for-php) (Maintained by the Appwrite Team)
- &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart) (Maintained by the Appwrite Team)
- &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno) (Maintained by the Appwrite Team)
- &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby) (Maintained by the Appwrite Team)
- &nbsp; [Python](https://github.com/appwrite/sdk-for-python) (Maintained by the Appwrite Team)
- &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team)
- &nbsp; [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team)
- &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team)
#### Community
- :white_check_mark: &nbsp; [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (Maintained by [Michael Gangolf](https://github.com/m1ga/))
- :white_check_mark: &nbsp; [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (Maintained by [fenix-hub @GodotNuts](https://github.com/fenix-hub))
- &nbsp; [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (Maintained by [Michael Gangolf](https://github.com/m1ga/))
- &nbsp; [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (Maintained by [fenix-hub @GodotNuts](https://github.com/fenix-hub))
Looking for more SDKs? - Help us by contributing a pull request to our [SDK Generator](https://github.com/appwrite/sdk-generator)!
## Architecture
![Appwrite Architecture showing how Appwrite is built and the services and tools it uses](docs/specs/overview.drawio.svg)
![Appwrite Architecture](docs/specs/overview.drawio.svg)
Appwrite uses a microservices architecture that was designed for easy scaling and delegation of responsibilities. In addition, Appwrite supports multiple APIs, such as REST, WebSocket, and GraphQL to allow you to interact with your resources by leveraging your existing knowledge and protocols of choice.
@@ -233,7 +229,7 @@ The Appwrite API layer was designed to be extremely fast by leveraging in-memory
All code contributions, including those of people having commit access, must go through a pull request and be approved by a core developer before being merged. This is to ensure a proper review of all the code.
We truly :heart: pull requests! If you wish to help, you can learn more about how you can contribute to this project in the [contribution guide](CONTRIBUTING.md).
We truly ❤️ pull requests! If you wish to help, you can learn more about how you can contribute to this project in the [contribution guide](CONTRIBUTING.md).
## Security
+12 -53
View File
@@ -5,8 +5,6 @@ require_once __DIR__ . '/init.php';
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Func;
use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Utopia\Cache\Adapter\Sharding;
@@ -21,7 +19,7 @@ use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
use Utopia\Registry\Registry;
use Utopia\System\System;
@@ -162,57 +160,18 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
};
}, ['pools', 'dbForPlatform', 'cache']);
CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getInternalId());
return $database;
}
$dbAdapter = $pools
->get('logs')
->pop()
->getResource();
$database = new Database(
$dbAdapter,
$cache
);
$database
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
// set tenant
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getInternalId());
}
return $database;
};
}, ['pools', 'cache']);
CLI::setResource('queueForStatsUsage', function (Connection $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
CLI::setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
CLI::setResource('publisher', function (Group $pools) {
return $pools->get('publisher')->pop()->getResource();
CLI::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);
CLI::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
CLI::setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
CLI::setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
CLI::setResource('queueForFunctions', function (Connection $queue) {
return new Func($queue);
}, ['queue']);
CLI::setResource('queueForDeletes', function (Connection $queue) {
return new Delete($queue);
}, ['queue']);
CLI::setResource('queueForCertificates', function (Connection $queue) {
return new Certificate($queue);
}, ['queue']);
CLI::setResource('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
$logger = $register->get('logger');
+1 -2
View File
@@ -16,6 +16,5 @@ return [
'union-china-pay' => ['name' => 'Union China Pay', 'path' => __DIR__ . '/credit-cards/union-china-pay.png'],
'visa' => ['name' => 'Visa', 'path' => __DIR__ . '/credit-cards/visa.png'],
'mir' => ['name' => 'MIR', 'path' => __DIR__ . '/credit-cards/mir.png'],
'maestro' => ['name' => 'Maestro', 'path' => __DIR__ . '/credit-cards/maestro.png'],
'rupay' => ['name' => 'Rupay', 'path' => __DIR__ . '/credit-cards/rupay.png']
'maestro' => ['name' => 'Maestro', 'path' => __DIR__ . '/credit-cards/maestro.png']
];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

-2
View File
@@ -5,7 +5,6 @@ $common = include __DIR__ . '/collections/common.php';
$projects = include __DIR__ . '/collections/projects.php';
$databases = include __DIR__ . '/collections/databases.php';
$platform = include __DIR__ . '/collections/platform.php';
$logs = include __DIR__ . '/collections/logs.php';
// see - http.php#245
// $collections['buckets']['files'];
@@ -28,7 +27,6 @@ $collections = [
'databases' => $databases,
'projects' => array_merge($projects, $common),
'console' => array_merge($platform, $common),
'logs' => $logs,
];
return $collections;
-18
View File
@@ -2548,17 +2548,6 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => 'transformedAt',
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
@@ -2628,13 +2617,6 @@ return [
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_transformedAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['transformedAt'],
'lengths' => [],
'orders' => [],
]
]
],
];
-94
View File
@@ -1,94 +0,0 @@
<?php
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
$logsCollection = [];
$logsCollection['stats'] = [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('stats'),
'name' => 'stats',
'attributes' => [
[
'$id' => ID::custom('metric'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 255,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('region'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 255,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('value'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 8,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('time'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('period'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 4,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_time'),
'type' => Database::INDEX_KEY,
'attributes' => ['time'],
'lengths' => [],
'orders' => [Database::ORDER_DESC],
],
[
'$id' => ID::custom('_key_period_time'),
'type' => Database::INDEX_KEY,
'attributes' => ['period', 'time'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_metric_period_time'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['metric', 'period', 'time'],
'lengths' => [],
'orders' => [Database::ORDER_DESC],
],
],
];
return $logsCollection;
-33
View File
@@ -1185,39 +1185,6 @@ return [
'default' => false,
'array' => false,
],
[
'$id' => ID::custom('personalAccessToken'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => ID::custom('personalAccessTokenExpiry'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('personalRefreshToken'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['encrypt'],
],
],
'indexes' => [
+1 -1
View File
@@ -356,7 +356,7 @@ return [
],
Exception::TEAM_INVALID_SECRET => [
'name' => Exception::TEAM_INVALID_SECRET,
'description' => 'The team invitation secret is invalid. Please request a new invitation and try again.',
'description' => 'The team invitation secret is invalid. Please request a new invitation and try again.',
'code' => 401,
],
Exception::TEAM_MEMBERSHIP_MISMATCH => [
+302 -11
View File
@@ -58,6 +58,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -106,6 +109,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -190,6 +196,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -265,6 +274,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/identities",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -323,6 +335,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -385,6 +400,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -433,6 +451,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -498,6 +519,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -567,6 +591,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -632,6 +659,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -709,6 +739,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -776,6 +809,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -821,10 +857,10 @@
],
"description": "Complete the MFA challenge by providing the one-time password. Finish the process of MFA verification by providing the one-time password. To begin the flow, use [createMfaChallenge](\/docs\/references\/cloud\/client-web\/account#createMfaChallenge) method.",
"responses": {
"200": {
"description": "Session",
"204": {
"description": "No content",
"content": {
"application\/json": {
"": {
"schema": {
"$ref": "#\/components\/schemas\/session"
}
@@ -849,6 +885,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -924,6 +963,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -974,6 +1016,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1022,6 +1067,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1070,6 +1118,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1120,6 +1171,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1189,6 +1243,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1263,6 +1320,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1338,6 +1398,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/prefs",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1386,6 +1449,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/prefs",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1458,6 +1524,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1531,6 +1600,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1612,6 +1684,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/sessions",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1653,6 +1728,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1703,6 +1781,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1751,6 +1832,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1824,6 +1908,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1890,6 +1977,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2030,6 +2120,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2103,6 +2196,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2176,6 +2272,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/sessions",
"offline-key": "{sessionId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2236,6 +2335,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2289,6 +2391,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2351,6 +2456,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2371,7 +2479,7 @@
"tags": [
"account"
],
"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.",
"description": "",
"responses": {
"201": {
"description": "Target",
@@ -2391,7 +2499,7 @@
"type": "",
"deprecated": false,
"demo": "account\/create-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/account\/create-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -2400,6 +2508,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2449,7 +2560,7 @@
"tags": [
"account"
],
"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.",
"description": "",
"responses": {
"200": {
"description": "Target",
@@ -2469,7 +2580,7 @@
"type": "",
"deprecated": false,
"demo": "account\/update-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/account\/update-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -2478,6 +2589,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2526,10 +2640,17 @@
"tags": [
"account"
],
"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.",
"description": "",
"responses": {
"204": {
"description": "No content"
"description": "No content",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/target"
}
}
}
}
},
"x-appwrite": {
@@ -2539,7 +2660,7 @@
"type": "",
"deprecated": false,
"demo": "account\/delete-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/account\/delete-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -2548,6 +2669,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2609,6 +2733,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2690,6 +2817,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2766,6 +2896,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2909,6 +3042,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2982,6 +3118,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3049,6 +3188,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3127,6 +3269,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3175,6 +3320,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3244,6 +3392,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3369,6 +3520,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3498,6 +3652,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3555,6 +3712,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4042,6 +4202,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4123,6 +4286,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4214,6 +4380,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4312,6 +4481,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4396,6 +4568,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "{documentId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4502,6 +4677,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "{documentId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4596,6 +4774,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "{documentId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4694,6 +4875,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "{documentId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4777,6 +4961,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4862,6 +5049,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4976,6 +5166,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5049,6 +5242,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5100,6 +5296,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5151,6 +5350,9 @@
"server"
],
"packaging": false,
"offline-model": "\/localed",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5202,6 +5404,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/localeCode",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5253,6 +5458,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/continents",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5304,6 +5512,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/countries",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5355,6 +5566,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/countries\/eu",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5406,6 +5620,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/countries\/phones",
"offline-key": "",
"offline-response-key": "countryCode",
"auth": {
"Project": []
}
@@ -5457,6 +5674,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/currencies",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5508,6 +5728,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/languages",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5560,6 +5783,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5642,6 +5868,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5715,6 +5944,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5800,6 +6032,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5897,6 +6132,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5968,6 +6206,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6056,6 +6297,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6122,6 +6366,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6188,6 +6435,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6361,7 +6611,6 @@
"gif",
"png",
"webp",
"heic",
"avif"
],
"x-enum-name": "ImageFormat",
@@ -6404,6 +6653,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6477,6 +6729,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6552,6 +6807,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6636,6 +6894,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams",
"offline-key": "{teamId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6697,6 +6958,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams",
"offline-key": "{teamId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6770,6 +7034,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6833,6 +7100,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams\/{teamId}\/memberships",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6918,6 +7188,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7028,6 +7301,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams\/{teamId}\/memberships",
"offline-key": "{membershipId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7099,6 +7375,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7185,6 +7464,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7257,6 +7539,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7354,6 +7639,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams\/{teamId}\/prefs",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7414,6 +7702,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams\/{teamId}\/prefs",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -3383,7 +3383,7 @@
"parameters": [
{
"name": "code",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro, rupay.",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro.",
"required": true,
"schema": {
"type": "string",
@@ -3404,8 +3404,7 @@
"union-china-pay",
"visa",
"mir",
"maestro",
"rupay"
"maestro"
],
"x-enum-name": "CreditCard",
"x-enum-keys": [
@@ -3424,8 +3423,7 @@
"Union China Pay",
"Visa",
"MIR",
"Maestro",
"Rupay"
"Maestro"
]
},
"in": "path"
@@ -4367,7 +4365,7 @@
"tags": [
"databases"
],
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.\n",
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.",
"responses": {
"201": {
"description": "Document",
+83 -129
View File
@@ -3387,7 +3387,7 @@
"parameters": [
{
"name": "code",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro, rupay.",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro.",
"required": true,
"schema": {
"type": "string",
@@ -3408,8 +3408,7 @@
"union-china-pay",
"visa",
"mir",
"maestro",
"rupay"
"maestro"
],
"x-enum-name": "CreditCard",
"x-enum-keys": [
@@ -3428,8 +3427,7 @@
"Union China Pay",
"Visa",
"MIR",
"Maestro",
"Rupay"
"Maestro"
]
},
"in": "path"
@@ -7825,7 +7823,7 @@
"tags": [
"databases"
],
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.\n",
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.",
"responses": {
"201": {
"description": "Document",
@@ -11587,7 +11585,7 @@
},
"x-appwrite": {
"method": "getCertificate",
"weight": 133,
"weight": 134,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11694,7 +11692,7 @@
},
"x-appwrite": {
"method": "getPubSub",
"weight": 129,
"weight": 130,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11720,6 +11718,54 @@
]
}
},
"\/health\/queue": {
"get": {
"summary": "Get queue",
"operationId": "healthGetQueue",
"tags": [
"health"
],
"description": "Check the Appwrite queue messaging servers are up and connection is successful.",
"responses": {
"200": {
"description": "Health Status",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/healthStatus"
}
}
}
}
},
"x-appwrite": {
"method": "getQueue",
"weight": 129,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "health.read",
"platforms": [
"server"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": [],
"Key": []
}
]
}
},
"\/health\/queue\/builds": {
"get": {
"summary": "Get builds queue",
@@ -11742,7 +11788,7 @@
},
"x-appwrite": {
"method": "getQueueBuilds",
"weight": 135,
"weight": 136,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11803,7 +11849,7 @@
},
"x-appwrite": {
"method": "getQueueCertificates",
"weight": 134,
"weight": 135,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11864,7 +11910,7 @@
},
"x-appwrite": {
"method": "getQueueDatabases",
"weight": 136,
"weight": 137,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11936,7 +11982,7 @@
},
"x-appwrite": {
"method": "getQueueDeletes",
"weight": 137,
"weight": 138,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12035,9 +12081,8 @@
"v1-audits",
"v1-mails",
"v1-functions",
"v1-stats-resources",
"v1-stats-usage",
"v1-stats-usage-dump",
"v1-usage",
"v1-usage-dump",
"v1-webhooks",
"v1-certificates",
"v1-builds",
@@ -12085,7 +12130,7 @@
},
"x-appwrite": {
"method": "getQueueFunctions",
"weight": 141,
"weight": 142,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12146,7 +12191,7 @@
},
"x-appwrite": {
"method": "getQueueLogs",
"weight": 132,
"weight": 133,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12207,7 +12252,7 @@
},
"x-appwrite": {
"method": "getQueueMails",
"weight": 138,
"weight": 139,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12268,7 +12313,7 @@
},
"x-appwrite": {
"method": "getQueueMessaging",
"weight": 139,
"weight": 140,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12329,7 +12374,7 @@
},
"x-appwrite": {
"method": "getQueueMigrations",
"weight": 140,
"weight": 141,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12368,70 +12413,9 @@
]
}
},
"\/health\/queue\/stats-resources": {
"\/health\/queue\/usage": {
"get": {
"summary": "Get stats resources queue",
"operationId": "healthGetQueueStatsResources",
"tags": [
"health"
],
"description": "Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue.",
"responses": {
"200": {
"description": "Health Queue",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/healthQueue"
}
}
}
}
},
"x-appwrite": {
"method": "getQueueStatsResources",
"weight": 142,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue-stats-resources.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-resources.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "health.read",
"platforms": [
"server"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": [],
"Key": []
}
],
"parameters": [
{
"name": "threshold",
"description": "Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.",
"required": false,
"schema": {
"type": "integer",
"format": "int32",
"default": 5000
},
"in": "query"
}
]
}
},
"\/health\/queue\/stats-usage": {
"get": {
"summary": "Get stats usage queue",
"summary": "Get usage queue",
"operationId": "healthGetQueueUsage",
"tags": [
"health"
@@ -12456,7 +12440,7 @@
"type": "",
"deprecated": false,
"demo": "health\/get-queue-usage.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-usage.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -12490,10 +12474,10 @@
]
}
},
"\/health\/queue\/stats-usage-dump": {
"\/health\/queue\/usage-dump": {
"get": {
"summary": "Get usage dump queue",
"operationId": "healthGetQueueStatsUsageDump",
"operationId": "healthGetQueueUsageDump",
"tags": [
"health"
],
@@ -12511,13 +12495,13 @@
}
},
"x-appwrite": {
"method": "getQueueStatsUsageDump",
"method": "getQueueUsageDump",
"weight": 144,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue-stats-usage-dump.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-usage-dump.md",
"demo": "health\/get-queue-usage-dump.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage-dump.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -12573,7 +12557,7 @@
},
"x-appwrite": {
"method": "getQueueWebhooks",
"weight": 131,
"weight": 132,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12730,7 +12714,7 @@
},
"x-appwrite": {
"method": "getTime",
"weight": 130,
"weight": 131,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13389,7 +13373,7 @@
"tags": [
"messaging"
],
"description": "Update an email message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update an email message by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -13707,7 +13691,7 @@
"tags": [
"messaging"
],
"description": "Update a push notification by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update a push notification by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -13992,7 +13976,7 @@
"tags": [
"messaging"
],
"description": "Update an SMS message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update an SMS message by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -17625,17 +17609,17 @@
},
"endpoint": {
"type": "string",
"description": "Source Appwrite endpoint",
"description": "Source's Appwrite Endpoint",
"x-example": "https:\/\/example.com"
},
"projectId": {
"type": "string",
"description": "Source Project ID",
"description": "Source's Project ID",
"x-example": "<PROJECT_ID>"
},
"apiKey": {
"type": "string",
"description": "Source API Key",
"description": "Source's API Key",
"x-example": "<API_KEY>"
}
},
@@ -36068,20 +36052,6 @@
"$ref": "#\/components\/schemas\/metric"
},
"x-example": []
},
"imageTransformations": {
"type": "array",
"description": "Aggregated number of files transformations per period.",
"items": {
"$ref": "#\/components\/schemas\/metric"
},
"x-example": []
},
"imageTransformationsTotal": {
"type": "integer",
"description": "Total aggregated number of files transformations.",
"x-example": 0,
"format": "int32"
}
},
"required": [
@@ -36089,9 +36059,7 @@
"filesTotal",
"filesStorageTotal",
"files",
"storage",
"imageTransformations",
"imageTransformationsTotal"
"storage"
]
},
"usageFunctions": {
@@ -36629,18 +36597,6 @@
"$ref": "#\/components\/schemas\/metric"
},
"x-example": []
},
"imageTransformationsTotal": {
"type": "integer",
"description": "An array of aggregated number of image transformations.",
"x-example": 0,
"format": "int32"
},
"imageTransformations": {
"type": "integer",
"description": "Total aggregated number of image transformations.",
"x-example": 0,
"format": "int32"
}
},
"required": [
@@ -36672,9 +36628,7 @@
"authPhoneEstimate",
"authPhoneCountryBreakdown",
"databasesReads",
"databasesWrites",
"imageTransformationsTotal",
"imageTransformations"
"databasesWrites"
]
},
"headers": {
+79 -95
View File
@@ -3077,7 +3077,7 @@
"parameters": [
{
"name": "code",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro, rupay.",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro.",
"required": true,
"schema": {
"type": "string",
@@ -3098,8 +3098,7 @@
"union-china-pay",
"visa",
"mir",
"maestro",
"rupay"
"maestro"
],
"x-enum-name": "CreditCard",
"x-enum-keys": [
@@ -3118,8 +3117,7 @@
"Union China Pay",
"Visa",
"MIR",
"Maestro",
"Rupay"
"Maestro"
]
},
"in": "path"
@@ -7383,7 +7381,7 @@
"tags": [
"databases"
],
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.\n",
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.",
"responses": {
"201": {
"description": "Document",
@@ -10463,7 +10461,7 @@
},
"x-appwrite": {
"method": "getCertificate",
"weight": 133,
"weight": 134,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10572,7 +10570,7 @@
},
"x-appwrite": {
"method": "getPubSub",
"weight": 129,
"weight": 130,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10599,6 +10597,55 @@
]
}
},
"\/health\/queue": {
"get": {
"summary": "Get queue",
"operationId": "healthGetQueue",
"tags": [
"health"
],
"description": "Check the Appwrite queue messaging servers are up and connection is successful.",
"responses": {
"200": {
"description": "Health Status",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/healthStatus"
}
}
}
}
},
"x-appwrite": {
"method": "getQueue",
"weight": 129,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "health.read",
"platforms": [
"server"
],
"packaging": false,
"auth": {
"Project": [],
"Key": []
}
},
"security": [
{
"Project": [],
"Key": []
}
]
}
},
"\/health\/queue\/builds": {
"get": {
"summary": "Get builds queue",
@@ -10621,7 +10668,7 @@
},
"x-appwrite": {
"method": "getQueueBuilds",
"weight": 135,
"weight": 136,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10683,7 +10730,7 @@
},
"x-appwrite": {
"method": "getQueueCertificates",
"weight": 134,
"weight": 135,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10745,7 +10792,7 @@
},
"x-appwrite": {
"method": "getQueueDatabases",
"weight": 136,
"weight": 137,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10818,7 +10865,7 @@
},
"x-appwrite": {
"method": "getQueueDeletes",
"weight": 137,
"weight": 138,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10919,9 +10966,8 @@
"v1-audits",
"v1-mails",
"v1-functions",
"v1-stats-resources",
"v1-stats-usage",
"v1-stats-usage-dump",
"v1-usage",
"v1-usage-dump",
"v1-webhooks",
"v1-certificates",
"v1-builds",
@@ -10969,7 +11015,7 @@
},
"x-appwrite": {
"method": "getQueueFunctions",
"weight": 141,
"weight": 142,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11031,7 +11077,7 @@
},
"x-appwrite": {
"method": "getQueueLogs",
"weight": 132,
"weight": 133,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11093,7 +11139,7 @@
},
"x-appwrite": {
"method": "getQueueMails",
"weight": 138,
"weight": 139,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11155,7 +11201,7 @@
},
"x-appwrite": {
"method": "getQueueMessaging",
"weight": 139,
"weight": 140,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11217,7 +11263,7 @@
},
"x-appwrite": {
"method": "getQueueMigrations",
"weight": 140,
"weight": 141,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11257,71 +11303,9 @@
]
}
},
"\/health\/queue\/stats-resources": {
"\/health\/queue\/usage": {
"get": {
"summary": "Get stats resources queue",
"operationId": "healthGetQueueStatsResources",
"tags": [
"health"
],
"description": "Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue.",
"responses": {
"200": {
"description": "Health Queue",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/healthQueue"
}
}
}
}
},
"x-appwrite": {
"method": "getQueueStatsResources",
"weight": 142,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue-stats-resources.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-resources.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "health.read",
"platforms": [
"server"
],
"packaging": false,
"auth": {
"Project": [],
"Key": []
}
},
"security": [
{
"Project": [],
"Key": []
}
],
"parameters": [
{
"name": "threshold",
"description": "Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.",
"required": false,
"schema": {
"type": "integer",
"format": "int32",
"default": 5000
},
"in": "query"
}
]
}
},
"\/health\/queue\/stats-usage": {
"get": {
"summary": "Get stats usage queue",
"summary": "Get usage queue",
"operationId": "healthGetQueueUsage",
"tags": [
"health"
@@ -11346,7 +11330,7 @@
"type": "",
"deprecated": false,
"demo": "health\/get-queue-usage.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-usage.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -11381,10 +11365,10 @@
]
}
},
"\/health\/queue\/stats-usage-dump": {
"\/health\/queue\/usage-dump": {
"get": {
"summary": "Get usage dump queue",
"operationId": "healthGetQueueStatsUsageDump",
"operationId": "healthGetQueueUsageDump",
"tags": [
"health"
],
@@ -11402,13 +11386,13 @@
}
},
"x-appwrite": {
"method": "getQueueStatsUsageDump",
"method": "getQueueUsageDump",
"weight": 144,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue-stats-usage-dump.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-usage-dump.md",
"demo": "health\/get-queue-usage-dump.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage-dump.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -11465,7 +11449,7 @@
},
"x-appwrite": {
"method": "getQueueWebhooks",
"weight": 131,
"weight": 132,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11625,7 +11609,7 @@
},
"x-appwrite": {
"method": "getTime",
"weight": 130,
"weight": 131,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12303,7 +12287,7 @@
"tags": [
"messaging"
],
"description": "Update an email message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update an email message by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -12623,7 +12607,7 @@
"tags": [
"messaging"
],
"description": "Update a push notification by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update a push notification by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -12910,7 +12894,7 @@
"tags": [
"messaging"
],
"description": "Update an SMS message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update an SMS message by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
+300 -17
View File
@@ -102,6 +102,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -152,6 +155,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -242,6 +248,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -321,6 +330,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/identities",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -382,6 +394,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -444,6 +459,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -494,6 +512,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -560,6 +581,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -632,6 +656,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -697,6 +724,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -775,6 +805,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -842,6 +875,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -886,19 +922,14 @@
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"produces": [],
"tags": [
"account"
],
"description": "Complete the MFA challenge by providing the one-time password. Finish the process of MFA verification by providing the one-time password. To begin the flow, use [createMfaChallenge](\/docs\/references\/cloud\/client-web\/account#createMfaChallenge) method.",
"responses": {
"200": {
"description": "Session",
"schema": {
"$ref": "#\/definitions\/session"
}
"204": {
"description": "No content"
}
},
"x-appwrite": {
@@ -918,6 +949,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -997,6 +1031,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1049,6 +1086,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1099,6 +1139,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1149,6 +1192,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1201,6 +1247,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1273,6 +1322,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1351,6 +1403,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1430,6 +1485,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/prefs",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1480,6 +1538,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/prefs",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1555,6 +1616,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1632,6 +1696,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1718,6 +1785,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/sessions",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1763,6 +1833,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1815,6 +1888,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1865,6 +1941,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -1942,6 +2021,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2016,6 +2098,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2151,6 +2236,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2228,6 +2316,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2305,6 +2396,9 @@
"server"
],
"packaging": false,
"offline-model": "\/account\/sessions",
"offline-key": "{sessionId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2365,6 +2459,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2420,6 +2517,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2482,6 +2582,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2508,7 +2611,7 @@
"tags": [
"account"
],
"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.",
"description": "",
"responses": {
"201": {
"description": "Target",
@@ -2524,7 +2627,7 @@
"type": "",
"deprecated": false,
"demo": "account\/create-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/account\/create-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -2533,6 +2636,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2591,7 +2697,7 @@
"tags": [
"account"
],
"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.",
"description": "",
"responses": {
"200": {
"description": "Target",
@@ -2607,7 +2713,7 @@
"type": "",
"deprecated": false,
"demo": "account\/update-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/account\/update-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -2616,6 +2722,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2661,11 +2770,13 @@
"consumes": [
"application\/json"
],
"produces": [],
"produces": [
"application\/json"
],
"tags": [
"account"
],
"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.",
"description": "",
"responses": {
"204": {
"description": "No content"
@@ -2678,7 +2789,7 @@
"type": "",
"deprecated": false,
"demo": "account\/delete-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/account\/delete-push-target.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -2687,6 +2798,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2748,6 +2862,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2834,6 +2951,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -2920,6 +3040,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3058,6 +3181,9 @@
"client"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3135,6 +3261,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3205,6 +3334,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3287,6 +3419,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3337,6 +3472,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3417,6 +3555,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3543,6 +3684,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3673,6 +3817,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -3737,6 +3884,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4225,6 +4375,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4309,6 +4462,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4401,6 +4557,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4493,6 +4652,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4574,6 +4736,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "{documentId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4679,6 +4844,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "{documentId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4768,6 +4936,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "{documentId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4864,6 +5035,9 @@
"server"
],
"packaging": false,
"offline-model": "\/databases\/{databaseId}\/collections\/{collectionId}\/documents",
"offline-key": "{documentId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -4943,6 +5117,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5025,6 +5202,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5143,6 +5323,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5214,6 +5397,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5287,6 +5473,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5360,6 +5549,9 @@
"server"
],
"packaging": false,
"offline-model": "\/localed",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5413,6 +5605,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/localeCode",
"offline-key": "current",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5466,6 +5661,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/continents",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5519,6 +5717,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/countries",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5572,6 +5773,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/countries\/eu",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5625,6 +5829,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/countries\/phones",
"offline-key": "",
"offline-response-key": "countryCode",
"auth": {
"Project": []
}
@@ -5678,6 +5885,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/currencies",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5731,6 +5941,9 @@
"server"
],
"packaging": false,
"offline-model": "\/locale\/languages",
"offline-key": "",
"offline-response-key": "code",
"auth": {
"Project": []
}
@@ -5785,6 +5998,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5840,7 +6056,9 @@
"consumes": [
"application\/json"
],
"produces": [],
"produces": [
"application\/json"
],
"tags": [
"messaging"
],
@@ -5869,6 +6087,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -5940,6 +6161,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6022,6 +6246,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6113,6 +6340,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6182,6 +6412,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6270,6 +6503,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6341,6 +6577,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6412,6 +6651,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6560,7 +6802,6 @@
"gif",
"png",
"webp",
"heic",
"avif"
],
"x-enum-name": "ImageFormat",
@@ -6611,6 +6852,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6682,6 +6926,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6756,6 +7003,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6847,6 +7097,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams",
"offline-key": "{teamId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6908,6 +7161,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams",
"offline-key": "{teamId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -6982,6 +7238,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7045,6 +7304,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams\/{teamId}\/memberships",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7127,6 +7389,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7241,6 +7506,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams\/{teamId}\/memberships",
"offline-key": "{membershipId}",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7310,6 +7578,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7395,6 +7666,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7465,6 +7739,9 @@
"server"
],
"packaging": false,
"offline-model": "",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7560,6 +7837,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams\/{teamId}\/prefs",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
@@ -7620,6 +7900,9 @@
"server"
],
"packaging": false,
"offline-model": "\/teams\/{teamId}\/prefs",
"offline-key": "",
"offline-response-key": "$id",
"auth": {
"Project": []
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -6
View File
@@ -3557,7 +3557,7 @@
"parameters": [
{
"name": "code",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro, rupay.",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro.",
"required": true,
"type": "string",
"x-example": "amex",
@@ -3577,8 +3577,7 @@
"union-china-pay",
"visa",
"mir",
"maestro",
"rupay"
"maestro"
],
"x-enum-name": "CreditCard",
"x-enum-keys": [
@@ -3597,8 +3596,7 @@
"Union China Pay",
"Visa",
"MIR",
"Maestro",
"Rupay"
"Maestro"
],
"in": "path"
},
@@ -4549,7 +4547,7 @@
"tags": [
"databases"
],
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.\n",
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.",
"responses": {
"201": {
"description": "Document",
+85 -130
View File
@@ -3577,7 +3577,7 @@
"parameters": [
{
"name": "code",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro, rupay.",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro.",
"required": true,
"type": "string",
"x-example": "amex",
@@ -3597,8 +3597,7 @@
"union-china-pay",
"visa",
"mir",
"maestro",
"rupay"
"maestro"
],
"x-enum-name": "CreditCard",
"x-enum-keys": [
@@ -3617,8 +3616,7 @@
"Union China Pay",
"Visa",
"MIR",
"Maestro",
"Rupay"
"Maestro"
],
"in": "path"
},
@@ -8014,7 +8012,7 @@
"tags": [
"databases"
],
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.\n",
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.",
"responses": {
"201": {
"description": "Document",
@@ -11812,7 +11810,7 @@
},
"x-appwrite": {
"method": "getCertificate",
"weight": 133,
"weight": 134,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11921,7 +11919,7 @@
},
"x-appwrite": {
"method": "getPubSub",
"weight": 129,
"weight": 130,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11947,6 +11945,56 @@
]
}
},
"\/health\/queue": {
"get": {
"summary": "Get queue",
"operationId": "healthGetQueue",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"health"
],
"description": "Check the Appwrite queue messaging servers are up and connection is successful.",
"responses": {
"200": {
"description": "Health Status",
"schema": {
"$ref": "#\/definitions\/healthStatus"
}
}
},
"x-appwrite": {
"method": "getQueue",
"weight": 129,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "health.read",
"platforms": [
"server"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": [],
"Key": []
}
]
}
},
"\/health\/queue\/builds": {
"get": {
"summary": "Get builds queue",
@@ -11971,7 +12019,7 @@
},
"x-appwrite": {
"method": "getQueueBuilds",
"weight": 135,
"weight": 136,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12032,7 +12080,7 @@
},
"x-appwrite": {
"method": "getQueueCertificates",
"weight": 134,
"weight": 135,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12093,7 +12141,7 @@
},
"x-appwrite": {
"method": "getQueueDatabases",
"weight": 136,
"weight": 137,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12163,7 +12211,7 @@
},
"x-appwrite": {
"method": "getQueueDeletes",
"weight": 137,
"weight": 138,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12261,9 +12309,8 @@
"v1-audits",
"v1-mails",
"v1-functions",
"v1-stats-resources",
"v1-stats-usage",
"v1-stats-usage-dump",
"v1-usage",
"v1-usage-dump",
"v1-webhooks",
"v1-certificates",
"v1-builds",
@@ -12310,7 +12357,7 @@
},
"x-appwrite": {
"method": "getQueueFunctions",
"weight": 141,
"weight": 142,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12371,7 +12418,7 @@
},
"x-appwrite": {
"method": "getQueueLogs",
"weight": 132,
"weight": 133,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12432,7 +12479,7 @@
},
"x-appwrite": {
"method": "getQueueMails",
"weight": 138,
"weight": 139,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12493,7 +12540,7 @@
},
"x-appwrite": {
"method": "getQueueMessaging",
"weight": 139,
"weight": 140,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12554,7 +12601,7 @@
},
"x-appwrite": {
"method": "getQueueMigrations",
"weight": 140,
"weight": 141,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12591,70 +12638,9 @@
]
}
},
"\/health\/queue\/stats-resources": {
"\/health\/queue\/usage": {
"get": {
"summary": "Get stats resources queue",
"operationId": "healthGetQueueStatsResources",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"health"
],
"description": "Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue.",
"responses": {
"200": {
"description": "Health Queue",
"schema": {
"$ref": "#\/definitions\/healthQueue"
}
}
},
"x-appwrite": {
"method": "getQueueStatsResources",
"weight": 142,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue-stats-resources.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-resources.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "health.read",
"platforms": [
"server"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": [],
"Key": []
}
],
"parameters": [
{
"name": "threshold",
"description": "Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.",
"required": false,
"type": "integer",
"format": "int32",
"default": 5000,
"in": "query"
}
]
}
},
"\/health\/queue\/stats-usage": {
"get": {
"summary": "Get stats usage queue",
"summary": "Get usage queue",
"operationId": "healthGetQueueUsage",
"consumes": [
"application\/json"
@@ -12681,7 +12667,7 @@
"type": "",
"deprecated": false,
"demo": "health\/get-queue-usage.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-usage.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -12713,10 +12699,10 @@
]
}
},
"\/health\/queue\/stats-usage-dump": {
"\/health\/queue\/usage-dump": {
"get": {
"summary": "Get usage dump queue",
"operationId": "healthGetQueueStatsUsageDump",
"operationId": "healthGetQueueUsageDump",
"consumes": [
"application\/json"
],
@@ -12736,13 +12722,13 @@
}
},
"x-appwrite": {
"method": "getQueueStatsUsageDump",
"method": "getQueueUsageDump",
"weight": 144,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue-stats-usage-dump.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-usage-dump.md",
"demo": "health\/get-queue-usage-dump.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage-dump.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -12798,7 +12784,7 @@
},
"x-appwrite": {
"method": "getQueueWebhooks",
"weight": 131,
"weight": 132,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12959,7 +12945,7 @@
},
"x-appwrite": {
"method": "getTime",
"weight": 130,
"weight": 131,
"cookies": false,
"type": "",
"deprecated": false,
@@ -13653,7 +13639,7 @@
"tags": [
"messaging"
],
"description": "Update an email message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update an email message by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -14001,7 +13987,7 @@
"tags": [
"messaging"
],
"description": "Update a push notification by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update a push notification by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -14311,7 +14297,7 @@
"tags": [
"messaging"
],
"description": "Update an SMS message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update an SMS message by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -18084,19 +18070,19 @@
},
"endpoint": {
"type": "string",
"description": "Source Appwrite endpoint",
"description": "Source's Appwrite Endpoint",
"default": null,
"x-example": "https:\/\/example.com"
},
"projectId": {
"type": "string",
"description": "Source Project ID",
"description": "Source's Project ID",
"default": null,
"x-example": "<PROJECT_ID>"
},
"apiKey": {
"type": "string",
"description": "Source API Key",
"description": "Source's API Key",
"default": null,
"x-example": "<API_KEY>"
}
@@ -36622,21 +36608,6 @@
"$ref": "#\/definitions\/metric"
},
"x-example": []
},
"imageTransformations": {
"type": "array",
"description": "Aggregated number of files transformations per period.",
"items": {
"type": "object",
"$ref": "#\/definitions\/metric"
},
"x-example": []
},
"imageTransformationsTotal": {
"type": "integer",
"description": "Total aggregated number of files transformations.",
"x-example": 0,
"format": "int32"
}
},
"required": [
@@ -36644,9 +36615,7 @@
"filesTotal",
"filesStorageTotal",
"files",
"storage",
"imageTransformations",
"imageTransformationsTotal"
"storage"
]
},
"usageFunctions": {
@@ -37216,18 +37185,6 @@
"$ref": "#\/definitions\/metric"
},
"x-example": []
},
"imageTransformationsTotal": {
"type": "integer",
"description": "An array of aggregated number of image transformations.",
"x-example": 0,
"format": "int32"
},
"imageTransformations": {
"type": "integer",
"description": "Total aggregated number of image transformations.",
"x-example": 0,
"format": "int32"
}
},
"required": [
@@ -37259,9 +37216,7 @@
"authPhoneEstimate",
"authPhoneCountryBreakdown",
"databasesReads",
"databasesWrites",
"imageTransformationsTotal",
"imageTransformations"
"databasesWrites"
]
},
"headers": {
+81 -95
View File
@@ -3261,7 +3261,7 @@
"parameters": [
{
"name": "code",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro, rupay.",
"description": "Credit Card Code. Possible values: amex, argencard, cabal, cencosud, diners, discover, elo, hipercard, jcb, mastercard, naranja, targeta-shopping, union-china-pay, visa, mir, maestro.",
"required": true,
"type": "string",
"x-example": "amex",
@@ -3281,8 +3281,7 @@
"union-china-pay",
"visa",
"mir",
"maestro",
"rupay"
"maestro"
],
"x-enum-name": "CreditCard",
"x-enum-keys": [
@@ -3301,8 +3300,7 @@
"Union China Pay",
"Visa",
"MIR",
"Maestro",
"Rupay"
"Maestro"
],
"in": "path"
},
@@ -7554,7 +7552,7 @@
"tags": [
"databases"
],
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.\n",
"description": "Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https:\/\/appwrite.io\/docs\/server\/databases#databasesCreateCollection) API or directly from your database console.",
"responses": {
"201": {
"description": "Document",
@@ -10691,7 +10689,7 @@
},
"x-appwrite": {
"method": "getCertificate",
"weight": 133,
"weight": 134,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10802,7 +10800,7 @@
},
"x-appwrite": {
"method": "getPubSub",
"weight": 129,
"weight": 130,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10829,6 +10827,57 @@
]
}
},
"\/health\/queue": {
"get": {
"summary": "Get queue",
"operationId": "healthGetQueue",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"health"
],
"description": "Check the Appwrite queue messaging servers are up and connection is successful.",
"responses": {
"200": {
"description": "Health Status",
"schema": {
"$ref": "#\/definitions\/healthStatus"
}
}
},
"x-appwrite": {
"method": "getQueue",
"weight": 129,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "health.read",
"platforms": [
"server"
],
"packaging": false,
"auth": {
"Project": [],
"Key": []
}
},
"security": [
{
"Project": [],
"Key": []
}
]
}
},
"\/health\/queue\/builds": {
"get": {
"summary": "Get builds queue",
@@ -10853,7 +10902,7 @@
},
"x-appwrite": {
"method": "getQueueBuilds",
"weight": 135,
"weight": 136,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10915,7 +10964,7 @@
},
"x-appwrite": {
"method": "getQueueCertificates",
"weight": 134,
"weight": 135,
"cookies": false,
"type": "",
"deprecated": false,
@@ -10977,7 +11026,7 @@
},
"x-appwrite": {
"method": "getQueueDatabases",
"weight": 136,
"weight": 137,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11048,7 +11097,7 @@
},
"x-appwrite": {
"method": "getQueueDeletes",
"weight": 137,
"weight": 138,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11148,9 +11197,8 @@
"v1-audits",
"v1-mails",
"v1-functions",
"v1-stats-resources",
"v1-stats-usage",
"v1-stats-usage-dump",
"v1-usage",
"v1-usage-dump",
"v1-webhooks",
"v1-certificates",
"v1-builds",
@@ -11197,7 +11245,7 @@
},
"x-appwrite": {
"method": "getQueueFunctions",
"weight": 141,
"weight": 142,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11259,7 +11307,7 @@
},
"x-appwrite": {
"method": "getQueueLogs",
"weight": 132,
"weight": 133,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11321,7 +11369,7 @@
},
"x-appwrite": {
"method": "getQueueMails",
"weight": 138,
"weight": 139,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11383,7 +11431,7 @@
},
"x-appwrite": {
"method": "getQueueMessaging",
"weight": 139,
"weight": 140,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11445,7 +11493,7 @@
},
"x-appwrite": {
"method": "getQueueMigrations",
"weight": 140,
"weight": 141,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11483,71 +11531,9 @@
]
}
},
"\/health\/queue\/stats-resources": {
"\/health\/queue\/usage": {
"get": {
"summary": "Get stats resources queue",
"operationId": "healthGetQueueStatsResources",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"health"
],
"description": "Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue.",
"responses": {
"200": {
"description": "Health Queue",
"schema": {
"$ref": "#\/definitions\/healthQueue"
}
}
},
"x-appwrite": {
"method": "getQueueStatsResources",
"weight": 142,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue-stats-resources.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-resources.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
"scope": "health.read",
"platforms": [
"server"
],
"packaging": false,
"auth": {
"Project": [],
"Key": []
}
},
"security": [
{
"Project": [],
"Key": []
}
],
"parameters": [
{
"name": "threshold",
"description": "Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.",
"required": false,
"type": "integer",
"format": "int32",
"default": 5000,
"in": "query"
}
]
}
},
"\/health\/queue\/stats-usage": {
"get": {
"summary": "Get stats usage queue",
"summary": "Get usage queue",
"operationId": "healthGetQueueUsage",
"consumes": [
"application\/json"
@@ -11574,7 +11560,7 @@
"type": "",
"deprecated": false,
"demo": "health\/get-queue-usage.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-usage.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -11607,10 +11593,10 @@
]
}
},
"\/health\/queue\/stats-usage-dump": {
"\/health\/queue\/usage-dump": {
"get": {
"summary": "Get usage dump queue",
"operationId": "healthGetQueueStatsUsageDump",
"operationId": "healthGetQueueUsageDump",
"consumes": [
"application\/json"
],
@@ -11630,13 +11616,13 @@
}
},
"x-appwrite": {
"method": "getQueueStatsUsageDump",
"method": "getQueueUsageDump",
"weight": 144,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "health\/get-queue-stats-usage-dump.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-stats-usage-dump.md",
"demo": "health\/get-queue-usage-dump.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage-dump.md",
"rate-limit": 0,
"rate-time": 3600,
"rate-key": "url:{url},ip:{ip}",
@@ -11693,7 +11679,7 @@
},
"x-appwrite": {
"method": "getQueueWebhooks",
"weight": 131,
"weight": 132,
"cookies": false,
"type": "",
"deprecated": false,
@@ -11857,7 +11843,7 @@
},
"x-appwrite": {
"method": "getTime",
"weight": 130,
"weight": 131,
"cookies": false,
"type": "",
"deprecated": false,
@@ -12570,7 +12556,7 @@
"tags": [
"messaging"
],
"description": "Update an email message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update an email message by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -12920,7 +12906,7 @@
"tags": [
"messaging"
],
"description": "Update a push notification by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update a push notification by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
@@ -13232,7 +13218,7 @@
"tags": [
"messaging"
],
"description": "Update an SMS message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.\n",
"description": "Update an SMS message by its unique ID.\n",
"responses": {
"200": {
"description": "Message",
-1
View File
@@ -7,5 +7,4 @@ return [
"gif" => "image/gif",
"png" => "image/png",
"heic" => "image/heic",
"webp" => "image/webp",
];
+4 -12
View File
@@ -572,7 +572,7 @@ return [
],
[
'name' => '_APP_STORAGE_S3_ACCESS_KEY',
'description' => 'S3 storage access key. Required when the storage adapter is set to S3. You can get your access key from your S3 storage provider',
'description' => 'AWS S3 storage access key. Required when the storage adapter is set to S3. You can get your access key from your AWS console',
'introduction' => '0.13.0',
'default' => '',
'required' => false,
@@ -580,7 +580,7 @@ return [
],
[
'name' => '_APP_STORAGE_S3_SECRET',
'description' => 'S3 storage secret key. Required when the storage adapter is set to S3. You can get your secret key from your S3 storage provider.',
'description' => 'AWS S3 storage secret key. Required when the storage adapter is set to S3. You can get your secret key from your AWS console.',
'introduction' => '0.13.0',
'default' => '',
'required' => false,
@@ -588,7 +588,7 @@ return [
],
[
'name' => '_APP_STORAGE_S3_REGION',
'description' => 'S3 storage region. Required when storage adapter is set to S3. You can find your region info for your bucket from your S3 storage provider.',
'description' => 'AWS S3 storage region. Required when storage adapter is set to S3. You can find your region info for your bucket from AWS console.',
'introduction' => '0.13.0',
'default' => 'us-east-1',
'required' => false,
@@ -596,20 +596,12 @@ return [
],
[
'name' => '_APP_STORAGE_S3_BUCKET',
'description' => 'S3 storage bucket. Required when storage adapter is set to S3. You can create buckets in your S3 storage provider.',
'description' => 'AWS S3 storage bucket. Required when storage adapter is set to S3. You can create buckets in your AWS console.',
'introduction' => '0.13.0',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_S3_ENDPOINT',
'description' => 'S3 storage endpoint. Required when using S3 storage providers other than AWS.',
'introduction' => '0.16.2',
'default' => '',
'required' => false,
'question' => '',
],
[
'name' => '_APP_STORAGE_DO_SPACES_ACCESS_KEY',
'description' => 'DigitalOcean spaces access key. Required when the storage adapter is set to DOSpaces. You can get your access key from your DigitalOcean console.',
+19 -21
View File
@@ -17,7 +17,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
@@ -2400,7 +2400,7 @@ App::put('/v1/account/sessions/phone')
App::post('/v1/account/tokens/phone')
->alias('/v1/account/sessions/phone')
->desc('Create phone token')
->groups(['api', 'account', 'auth'])
->groups(['api', 'account'])
->label('scope', 'sessions.write')
->label('auth.type', 'phone')
->label('audits.event', 'session.create')
@@ -2432,9 +2432,9 @@ App::post('/v1/account/tokens/phone')
->inject('queueForMessaging')
->inject('locale')
->inject('timelimit')
->inject('queueForStatsUsage')
->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, StatsUsage $queueForStatsUsage, array $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');
}
@@ -2583,11 +2583,11 @@ App::post('/v1/account/tokens/phone')
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForStatsUsage
$queueForUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
}
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
@@ -2715,15 +2715,13 @@ App::get('/v1/account/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new EventAudit($dbForProject);
$logs = $audit->getLogsByUser($user->getInternalId(), $queries);
$logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset);
$output = [];
@@ -2752,7 +2750,7 @@ App::get('/v1/account/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByUser($user->getInternalId(), $queries),
'total' => $audit->countLogsByUser($user->getInternalId()),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -3680,9 +3678,9 @@ App::post('/v1/account/verification/phone')
->inject('project')
->inject('locale')
->inject('timelimit')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('plan')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $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');
}
@@ -3777,11 +3775,11 @@ App::post('/v1/account/verification/phone')
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForStatsUsage
$queueForUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
}
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
@@ -4312,9 +4310,9 @@ App::post('/v1/account/mfa/challenge')
->inject('queueForMessaging')
->inject('queueForMails')
->inject('timelimit')
->inject('queueForStatsUsage')
->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, StatsUsage $queueForStatsUsage, array $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();
@@ -4385,11 +4383,11 @@ App::post('/v1/account/mfa/challenge')
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForStatsUsage
$queueForUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
}
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
+63 -73
View File
@@ -4,7 +4,7 @@ use Appwrite\Auth\Auth;
use Appwrite\Detector\Detector;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\SDK\AuthType;
@@ -23,12 +23,10 @@ use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Index as IndexException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Query as QueryException;
@@ -243,8 +241,8 @@ function updateAttribute(
string $filter = null,
string|bool|int|float $default = null,
bool $required = null,
int|float|null $min = null,
int|float|null $max = null,
int|float $min = null,
int|float $max = null,
array $elements = null,
array $options = [],
string $newKey = null,
@@ -300,9 +298,6 @@ function updateAttribute(
switch ($attribute->getAttribute('format')) {
case APP_DATABASE_ATTRIBUTE_INT_RANGE:
case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE:
$min ??= $attribute->getAttribute('formatOptions')['min'];
$max ??= $attribute->getAttribute('formatOptions')['max'];
if ($min > $max) {
throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value');
}
@@ -398,8 +393,6 @@ function updateAttribute(
throw new Exception(Exception::ATTRIBUTE_NOT_FOUND);
} catch (LimitException) {
throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED);
} catch (IndexException $e) {
throw new Exception(Exception::INDEX_INVALID, $e->getMessage());
}
}
@@ -483,8 +476,8 @@ App::post('/v1/databases')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) {
->inject('queueForUsage')
->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents, Usage $queueForUsage) {
$databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId;
@@ -534,7 +527,7 @@ App::post('/v1/databases')
}
$queueForEvents->setParam('databaseId', $database->getId());
$queueForStatsUsage->addMetric(str_replace(['{databaseInternalId}'], [$database->getInternalId()], METRIC_DATABASE_ID_STORAGE), 1); // per database
$queueForUsage->addMetric(str_replace(['{databaseInternalId}'], [$database->getInternalId()], METRIC_DATABASE_ID_STORAGE), 1); // per database
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -673,15 +666,13 @@ App::get('/v1/databases/:databaseId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'database/' . $databaseId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
@@ -729,7 +720,7 @@ App::get('/v1/databases/:databaseId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -806,8 +797,8 @@ App::delete('/v1/databases/:databaseId')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->action(function (string $databaseId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) {
->inject('queueForUsage')
->action(function (string $databaseId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage) {
$database = $dbForProject->getDocument('databases', $databaseId);
@@ -830,7 +821,7 @@ App::delete('/v1/databases/:databaseId')
->setParam('databaseId', $database->getId())
->setPayload($response->output($database, Response::MODEL_DATABASE));
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_DATABASES_STORAGE, 1); // Global, deletion forces full recalculation
$response->noContent();
@@ -1063,21 +1054,14 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$queries = Query::parseQueries($queries);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'database/' . $databaseId . '/collection/' . $collectionId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
@@ -1125,7 +1109,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -1563,8 +1547,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) {
// Ensure attribute default is within range
$min ??= PHP_INT_MIN;
$max ??= PHP_INT_MAX;
$min = \is_null($min) ? PHP_INT_MIN : $min;
$max = \is_null($max) ? PHP_INT_MAX : $max;
if ($min > $max) {
throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value');
@@ -1640,8 +1624,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float'
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) {
// Ensure attribute default is within range
$min ??= -PHP_FLOAT_MAX;
$max ??= PHP_FLOAT_MAX;
$min = \is_null($min) ? -PHP_FLOAT_MAX : $min;
$max = \is_null($max) ? PHP_FLOAT_MAX : $max;
if ($min > $max) {
throw new Exception(Exception::ATTRIBUTE_VALUE_INVALID, 'Minimum value must be lesser than maximum value');
@@ -2352,8 +2336,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/integ
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new Integer(), 'Minimum value to enforce on new documents', true)
->param('max', null, new Integer(), 'Maximum value to enforce on new documents', true)
->param('min', null, new Integer(), 'Minimum value to enforce on new documents')
->param('max', null, new Integer(), 'Maximum value to enforce on new documents')
->param('default', null, new Nullable(new Integer()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New attribute key.', true)
->inject('response')
@@ -2411,8 +2395,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/float
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents', true)
->param('max', null, new FloatValidator(), 'Maximum value to enforce on new documents', true)
->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents')
->param('max', null, new FloatValidator(), 'Maximum value to enforce on new documents')
->param('default', null, new Nullable(new FloatValidator()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New attribute key.', true)
->inject('response')
@@ -2634,8 +2618,8 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) {
->inject('queueForUsage')
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage) {
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -2732,7 +2716,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
->setContext('database', $db)
->setPayload($response->output($attribute, $model));
$queueForStatsUsage
$queueForUsage
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$db->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection
$response->noContent();
@@ -2838,7 +2822,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key'));
if ($attributeIndex === false) {
throw new Exception(Exception::ATTRIBUTE_UNKNOWN, 'Unknown attribute: ' . $attribute . '. Verify the attribute name or create the attribute.');
throw new Exception(Exception::ATTRIBUTE_UNKNOWN, 'Unknown attribute: ' . $attribute);
}
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
@@ -3150,8 +3134,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
->inject('dbForProject')
->inject('user')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage) {
->inject('queueForUsage')
->inject('mode')
->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Usage $queueForUsage, string $mode) {
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@@ -3354,7 +3339,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
$processDocument($collection, $document);
$queueForStatsUsage
$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
@@ -3406,8 +3391,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) {
->inject('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());
@@ -3519,9 +3505,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
$processDocument($collection, $document);
}
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
@@ -3583,8 +3570,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) {
->inject('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());
@@ -3660,9 +3648,10 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$processDocument($collection, $document);
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
@@ -3722,15 +3711,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'database/' . $databaseId . '/collection/' . $collectionId . '/document/' . $document->getId();
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
@@ -3778,7 +3765,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -3817,8 +3804,9 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) {
->inject('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
@@ -3958,9 +3946,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
$setCollection($collection, $newDocument);
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $operations)
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations)
;
$response->addHeader('X-Debug-Operations', $operations);
@@ -4069,8 +4058,9 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) {
->inject('queueForUsage')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, Usage $queueForUsage, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
@@ -4139,7 +4129,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
$processDocument($collection, $document);
$queueForStatsUsage
$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
+43 -25
View File
@@ -6,14 +6,13 @@ use Appwrite\Event\Build;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Usage;
use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\Validator\Headers;
use Appwrite\Functions\Validator\RuntimeSpecification;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Platform\Tasks\ScheduleExecutions;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -195,12 +194,9 @@ App::post('/v1/functions')
->inject('user')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
->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, callable $timelimit, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Database $dbForPlatform, 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
@@ -390,29 +386,51 @@ App::post('/v1/functions')
]))
);
/** Trigger Webhook */
$ruleModel = new Rule();
$ruleCreate =
$queueForEvents
->setProject($project)
->setEvent('rules.[ruleId].create')
->setParam('ruleId', $rule->getId())
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())));
->setClass(Event::WEBHOOK_CLASS_NAME)
->setQueue(Event::WEBHOOK_QUEUE_NAME);
/** Trigger Webhook */
$queueForWebhooks
->from($ruleCreate)
$ruleCreate
->setProject($project)
->setEvent('rules.[ruleId].create')
->setParam('ruleId', $rule->getId())
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())))
->trigger();
/** Trigger Functions */
$queueForFunctions
->from($ruleCreate)
$ruleCreate
->setClass(Event::FUNCTIONS_CLASS_NAME)
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
->trigger();
/** Trigger Realtime Events */
$queueForRealtime
->from($ruleCreate)
->setSubscribers(['console', $project->getId()])
->trigger();
/** Trigger realtime event */
$allEvents = Event::generateEvents('rules.[ruleId].create', [
'ruleId' => $rule->getId(),
]);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $rule,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $rule->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
Realtime::send(
projectId: $project->getId(),
payload: $rule->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
$queueForEvents->setParam('functionId', $function->getId());
@@ -1259,9 +1277,9 @@ App::post('/v1/functions/:functionId/deployments')
model: Response::MODEL_DEPLOYMENT,
)
],
requestType: 'multipart/form-data',
type: MethodType::UPLOAD,
packaging: true,
requestType: 'multipart/form-data',
))
->param('functionId', '', new UID(), 'Function ID.')
->param('entrypoint', null, new Text(1028), 'Entrypoint File.', true)
@@ -1882,10 +1900,10 @@ App::post('/v1/functions/:functionId/executions')
->inject('dbForPlatform')
->inject('user')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->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 $dbForPlatform, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, 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)) {
@@ -2212,7 +2230,7 @@ App::post('/v1/functions/:functionId/executions')
throw $th;
}
} finally {
$queueForStatsUsage
$queueForUsage
->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
+125 -82
View File
@@ -13,8 +13,8 @@ use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Domains\Validator\PublicDomain;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
use Utopia\Registry\Registry;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
@@ -188,6 +188,69 @@ App::get('/v1/health/cache')
]), Response::MODEL_HEALTH_STATUS_LIST);
});
App::get('/v1/health/queue')
->desc('Get queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
auth: [AuthType::KEY],
namespace: 'health',
name: 'getQueue',
description: '/docs/references/health/get-queue.md',
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_STATUS,
)
],
contentType: ContentType::JSON
))
->inject('response')
->inject('pools')
->action(function (Response $response, Group $pools) {
$output = [];
$configs = [
'Queue' => Config::getParam('pools-queue'),
];
foreach ($configs as $key => $config) {
foreach ($config as $database) {
$checkStart = \microtime(true);
try {
/** @var Connection $adapter */
$adapter = $pools->get($database)->pop()->getResource();
if ($adapter->ping()) {
$output[] = new Document([
'name' => $key . " ($database)",
'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) / 1000)
]);
} else {
$output[] = new Document([
'name' => $key . " ($database)",
'status' => 'fail',
'ping' => \round((\microtime(true) - $checkStart) / 1000)
]);
}
} catch (\Throwable $th) {
$output[] = new Document([
'name' => $key . " ($database)",
'status' => 'fail',
'ping' => \round((\microtime(true) - $checkStart) / 1000)
]);
}
}
}
$response->dynamic(new Document([
'statuses' => $output,
'total' => count($output),
]), Response::MODEL_HEALTH_STATUS_LIST);
});
App::get('/v1/health/pubsub')
->desc('Get pubsub')
->groups(['api', 'health'])
@@ -333,12 +396,13 @@ App::get('/v1/health/queue/webhooks')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::WEBHOOK_QUEUE_NAME));
$client = new Client(Event::WEBHOOK_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -365,12 +429,13 @@ App::get('/v1/health/queue/logs')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::AUDITS_QUEUE_NAME));
$client = new Client(Event::AUDITS_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -453,12 +518,13 @@ App::get('/v1/health/queue/certificates')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::CERTIFICATES_QUEUE_NAME));
$client = new Client(Event::CERTIFICATES_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -485,12 +551,13 @@ App::get('/v1/health/queue/builds')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::BUILDS_QUEUE_NAME));
$client = new Client(Event::BUILDS_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -518,12 +585,13 @@ App::get('/v1/health/queue/databases')
))
->param('name', 'database_db_main', new Text(256), 'Queue name for which to check the queue size', true)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (string $name, int|string $threshold, Publisher $publisher, Response $response) {
->action(function (string $name, int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue($name));
$client = new Client($name, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -550,12 +618,13 @@ App::get('/v1/health/queue/deletes')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::DELETE_QUEUE_NAME));
$client = new Client(Event::DELETE_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -582,12 +651,13 @@ App::get('/v1/health/queue/mails')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::MAILS_QUEUE_NAME));
$client = new Client(Event::MAILS_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -614,12 +684,13 @@ App::get('/v1/health/queue/messaging')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::MESSAGING_QUEUE_NAME));
$client = new Client(Event::MESSAGING_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -646,12 +717,13 @@ App::get('/v1/health/queue/migrations')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::MIGRATIONS_QUEUE_NAME));
$client = new Client(Event::MIGRATIONS_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -678,12 +750,13 @@ App::get('/v1/health/queue/functions')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::FUNCTIONS_QUEUE_NAME));
$client = new Client(Event::FUNCTIONS_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -692,47 +765,15 @@ App::get('/v1/health/queue/functions')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/stats-resources')
->desc('Get stats resources queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
auth: [AuthType::KEY],
namespace: 'health',
name: 'getQueueStatsResources',
description: '/docs/references/health/get-queue-stats-resources.md',
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_HEALTH_QUEUE,
)
],
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::STATS_RESOURCES_QUEUE_NAME));
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
});
App::get('/v1/health/queue/stats-usage')
->desc('Get stats usage queue')
App::get('/v1/health/queue/usage')
->desc('Get usage queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
auth: [AuthType::KEY],
namespace: 'health',
name: 'getQueueUsage',
description: '/docs/references/health/get-queue-stats-usage.md',
description: '/docs/references/health/get-queue-usage.md',
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -742,12 +783,13 @@ App::get('/v1/health/queue/stats-usage')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::STATS_USAGE_QUEUE_NAME));
$client = new Client(Event::USAGE_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -756,15 +798,15 @@ App::get('/v1/health/queue/stats-usage')
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
});
App::get('/v1/health/queue/stats-usage-dump')
App::get('/v1/health/queue/usage-dump')
->desc('Get usage dump queue')
->groups(['api', 'health'])
->label('scope', 'health.read')
->label('sdk', new Method(
auth: [AuthType::KEY],
namespace: 'health',
name: 'getQueueStatsUsageDump',
description: '/docs/references/health/get-queue-stats-usage-dump.md',
name: 'getQueueUsageDump',
description: '/docs/references/health/get-queue-usage-dump.md',
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -774,12 +816,13 @@ App::get('/v1/health/queue/stats-usage-dump')
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('queue')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::STATS_USAGE_DUMP_QUEUE_NAME));
$client = new Client(Event::USAGE_DUMP_QUEUE_NAME, $queue);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -952,9 +995,8 @@ App::get('/v1/health/queue/failed/:name')
Event::AUDITS_QUEUE_NAME,
Event::MAILS_QUEUE_NAME,
Event::FUNCTIONS_QUEUE_NAME,
Event::STATS_RESOURCES_QUEUE_NAME,
Event::STATS_USAGE_QUEUE_NAME,
Event::STATS_USAGE_DUMP_QUEUE_NAME,
Event::USAGE_QUEUE_NAME,
Event::USAGE_DUMP_QUEUE_NAME,
Event::WEBHOOK_QUEUE_NAME,
Event::CERTIFICATES_QUEUE_NAME,
Event::BUILDS_QUEUE_NAME,
@@ -963,11 +1005,12 @@ App::get('/v1/health/queue/failed/:name')
]), 'The name of the queue')
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('response')
->inject('publisher')
->action(function (string $name, int|string $threshold, Response $response, Publisher $publisher) {
->inject('queue')
->action(function (string $name, int|string $threshold, Response $response, Connection $queue) {
$threshold = \intval($threshold);
$failed = $publisher->getQueueSize(new Queue($name), failedJobs: true);
$client = new Client($name, $queue);
$failed = $client->countFailedJobs();
if ($failed >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue failed jobs threshold hit. Current size is {$failed} and threshold is {$threshold}.");
+20 -28
View File
@@ -994,15 +994,13 @@ App::get('/v1/messaging/providers/:providerId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'provider/' . $providerId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
foreach ($logs as $i => &$log) {
@@ -1049,7 +1047,7 @@ App::get('/v1/messaging/providers/:providerId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -2224,15 +2222,13 @@ App::get('/v1/messaging/topics/:topicId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'topic/' . $topicId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
@@ -2280,7 +2276,7 @@ App::get('/v1/messaging/topics/:topicId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -2636,15 +2632,13 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'subscriber/' . $subscriberId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
@@ -2692,7 +2686,7 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
@@ -3403,15 +3397,13 @@ App::get('/v1/messaging/messages/:messageId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'message/' . $messageId;
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
@@ -3459,7 +3451,7 @@ App::get('/v1/messaging/messages/:messageId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
+3 -3
View File
@@ -48,9 +48,9 @@ App::post('/v1/migrations/appwrite')
]
))
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
->param('endpoint', '', new URL(), 'Source Appwrite endpoint')
->param('projectId', '', new UID(), 'Source Project ID')
->param('apiKey', '', new Text(512), 'Source API Key')
->param('endpoint', '', new URL(), "Source's Appwrite Endpoint")
->param('projectId', '', new UID(), "Source's Project ID")
->param('apiKey', '', new Text(512), "Source's API Key")
->inject('response')
->inject('dbForProject')
->inject('project')
+4 -16
View File
@@ -40,18 +40,14 @@ App::get('/v1/project/usage')
->param('endDate', '', new DateTimeValidator(), 'End date for the usage')
->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('getLogsDB')
->inject('smsRates')
->action(function (string $startDate, string $endDate, string $period, Response $response, Document $project, Database $dbForProject, callable $getLogsDB, array $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);
$lastDay = (new DateTime($endDate))->format($format);
$dbForLogs = call_user_func($getLogsDB, $project);
$metrics = [
'total' => [
METRIC_EXECUTIONS,
@@ -67,7 +63,6 @@ App::get('/v1/project/usage')
METRIC_BUILDS_STORAGE,
METRIC_DATABASES_OPERATIONS_READS,
METRIC_DATABASES_OPERATIONS_WRITES,
METRIC_FILES_IMAGES_TRANSFORMED,
],
'period' => [
METRIC_NETWORK_REQUESTS,
@@ -80,7 +75,6 @@ App::get('/v1/project/usage')
METRIC_BUILDS_MB_SECONDS,
METRIC_DATABASES_OPERATIONS_READS,
METRIC_DATABASES_OPERATIONS_WRITES,
METRIC_FILES_IMAGES_TRANSFORMED,
]
];
@@ -99,11 +93,9 @@ App::get('/v1/project/usage')
'1d' => 'Y-m-d\T00:00:00.000P',
};
Authorization::skip(function () use ($dbForProject, $dbForLogs, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, $limit, &$total, &$stats) {
foreach ($metrics['total'] as $metric) {
$db = ($metric === METRIC_FILES_IMAGES_TRANSFORMED) ? $dbForLogs : $dbForProject;
$result = $db->findOne('stats', [
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
@@ -111,9 +103,7 @@ App::get('/v1/project/usage')
}
foreach ($metrics['period'] as $metric) {
$db = ($metric === METRIC_FILES_IMAGES_TRANSFORMED) ? $dbForLogs : $dbForProject;
$results = $db->find('stats', [
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::greaterThanEqual('time', $firstDay),
@@ -373,8 +363,6 @@ App::get('/v1/project/usage')
'authPhoneTotal' => $authPhoneTotal,
'authPhoneEstimate' => $authPhoneEstimate,
'authPhoneCountryBreakdown' => $authPhoneCountryBreakdown,
'imageTransformations' => $usage[METRIC_FILES_IMAGES_TRANSFORMED],
'imageTransformationsTotal' => $total[METRIC_FILES_IMAGES_TRANSFORMED],
]), Response::MODEL_USAGE_PROJECT);
});
+17 -25
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;
@@ -20,7 +21,6 @@ use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\NotFound as NotFoundException;
@@ -934,11 +934,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->param('rotation', 0, new Range(-360, 360), 'Preview image rotation in degrees. Pass an integer between -360 and 360.', true)
->param('background', '', new HexColor(), 'Preview image background color. Only works with transparent images (png). Use a valid HEX color, no # is needed for prefix.', true)
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true)
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
->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, Response $response, Database $dbForProject, 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,19 +1070,16 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
//Do not update transformedAt if it's a console user
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
}
}
$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)
->file($data);
->file($data)
;
unset($image);
});
@@ -1871,12 +1872,9 @@ App::get('/v1/storage/:bucketId/usage')
->param('bucketId', '', new UID(), 'Bucket ID.')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('getLogsDB')
->action(function (string $bucketId, string $range, Response $response, Document $project, Database $dbForProject, callable $getLogsDB) {
->action(function (string $bucketId, string $range, Response $response, Database $dbForProject) {
$dbForLogs = call_user_func($getLogsDB, $project);
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@@ -1889,16 +1887,12 @@ App::get('/v1/storage/:bucketId/usage')
$metrics = [
str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES),
str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE),
str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED),
];
Authorization::skip(function () use ($dbForProject, $dbForLogs, $bucket, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$db = ($metric === str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED))
? $dbForLogs
: $dbForProject;
$result = $db->findOne('stats', [
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
@@ -1906,7 +1900,7 @@ App::get('/v1/storage/:bucketId/usage')
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $db->find('stats', [
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
@@ -1947,7 +1941,5 @@ App::get('/v1/storage/:bucketId/usage')
'filesStorageTotal' => $usage[$metrics[1]]['total'],
'files' => $usage[$metrics[0]]['data'],
'storage' => $usage[$metrics[1]]['data'],
'imageTransformations' => $usage[$metrics[2]]['data'],
'imageTransformationsTotal' => $usage[$metrics[2]]['total'],
]), Response::MODEL_USAGE_BUCKETS);
});
+22 -23
View File
@@ -8,7 +8,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Workers\Deletes;
@@ -466,15 +466,15 @@ App::post('/v1/teams/:teamId/memberships')
->inject('queueForMessaging')
->inject('queueForEvents')
->inject('timelimit')
->inject('queueForStatsUsage')
->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, StatsUsage $queueForStatsUsage, array $plan) {
$isAppUser = Auth::isAppUser(Authorization::getRoles());
->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());
$url = htmlentities($url);
if (empty($url)) {
if (!$isAppUser && !$isPrivilegedUser) {
if (!$isAPIKey && !$isPrivilegedUser) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'URL is required');
}
}
@@ -482,13 +482,15 @@ App::post('/v1/teams/:teamId/memberships')
if (empty($userId) && empty($email) && empty($phone)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'At least one of userId, email, or phone is required');
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
if (!$isPrivilegedUser && !$isAppUser && empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED);
}
$email = \strtolower($email);
$name = empty($name) ? $email : $name;
$name = (empty($name)) ? $email : $name;
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@@ -507,7 +509,7 @@ App::post('/v1/teams/:teamId/memberships')
}
$email = $invitee->getAttribute('email', '');
$phone = $invitee->getAttribute('phone', '');
$name = $invitee->getAttribute('name', '') ?: $name;
$name = empty($name) ? $invitee->getAttribute('name', '') : $name;
} elseif (!empty($email)) {
$invitee = $dbForProject->findOne('users', [Query::equal('email', [$email])]); // Get user by email address
if (!$invitee->isEmpty() && !empty($phone) && $invitee->getAttribute('phone', '') !== $phone) {
@@ -588,8 +590,9 @@ App::post('/v1/teams/:teamId/memberships')
Query::equal('teamInternalId', [$team->getInternalId()]),
]);
$secret = Auth::tokenGenerator();
if ($membership->isEmpty()) {
$secret = Auth::tokenGenerator();
$membershipId = ID::unique();
$membership = new Document([
'$id' => $membershipId,
@@ -617,8 +620,7 @@ App::post('/v1/teams/:teamId/memberships')
$dbForProject->createDocument('memberships', $membership);
Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1));
} elseif ($membership->getAttribute('confirm') === false) {
$membership->setAttribute('secret', Auth::hash($secret));
} else {
$membership->setAttribute('invited', DateTime::now());
if ($isPrivilegedUser || $isAppUser) {
@@ -629,15 +631,14 @@ App::post('/v1/teams/:teamId/memberships')
$membership = ($isPrivilegedUser || $isAppUser) ?
Authorization::skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) :
$dbForProject->updateDocument('memberships', $membership->getId(), $membership);
} else {
throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED);
}
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, 'teamName' => $team->getAttribute('name')]);
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['membershipId' => $membership->getId(), 'userId' => $invitee->getId(), 'secret' => $secret, 'teamId' => $teamId]);
$url = Template::unParseURL($url);
if (!empty($email)) {
$projectName = $project->isEmpty() ? 'Console' : $project->getAttribute('name', '[APP-NAME]');
@@ -715,7 +716,7 @@ App::post('/v1/teams/:teamId/memberships')
->setSubject($subject)
->setBody($body)
->setRecipient($invitee->getAttribute('email'))
->setName($invitee->getAttribute('name', ''))
->setName($invitee->getAttribute('name'))
->setVariables($emailVariables)
->trigger();
@@ -758,11 +759,11 @@ App::post('/v1/teams/:teamId/memberships')
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$queueForStatsUsage
$queueForUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
}
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
@@ -1362,15 +1363,13 @@ App::get('/v1/teams/:teamId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$resource = 'team/' . $team->getId();
$logs = $audit->getLogsByResource($resource, $queries);
$logs = $audit->getLogsByResource($resource, $limit, $offset);
$output = [];
@@ -1417,7 +1416,7 @@ App::get('/v1/teams/:teamId/logs')
}
}
$response->dynamic(new Document([
'total' => $audit->countLogsByResource($resource, $queries),
'total' => $audit->countLogsByResource($resource),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
+5 -7
View File
@@ -862,15 +862,13 @@ App::get('/v1/users/:userId/logs')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$grouped = Query::groupByType($queries);
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
$offset = $grouped['offset'] ?? 0;
$audit = new Audit($dbForProject);
$logs = $audit->getLogsByUser($user->getInternalId(), $queries);
$logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset);
$output = [];
@@ -917,7 +915,7 @@ App::get('/v1/users/:userId/logs')
}
$response->dynamic(new Document([
'total' => $audit->countLogsByUser($user->getInternalId(), $queries),
'total' => $audit->countLogsByUser($user->getInternalId()),
'logs' => $output,
]), Response::MODEL_LOG_LIST);
});
+22 -35
View File
@@ -7,7 +7,7 @@ use Appwrite\Auth\Auth;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Validator\Origin;
use Appwrite\SDK\AuthType;
@@ -50,7 +50,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname)
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');
@@ -434,7 +434,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
@@ -499,13 +499,13 @@ App::init()
->inject('localeCodes')
->inject('clients')
->inject('geodb')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('queueForEvents')
->inject('queueForCertificates')
->inject('queueForFunctions')
->inject('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, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $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
*/
@@ -513,7 +513,7 @@ App::init()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
return;
}
}
@@ -732,13 +732,12 @@ App::options()
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('isResourceBlocked')
->inject('previewHostname')
->inject('project')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project) {
->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
*/
@@ -746,7 +745,7 @@ App::options()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) {
return;
}
}
@@ -761,16 +760,6 @@ App::options()
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
->noContent();
/** OPTIONS requests in utopia do not execute shutdown handlers, as a result we need to track the OPTIONS requests explicitly
* @see https://github.com/utopia-php/http/blob/0.33.16/src/App.php#L825-L855
*/
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize())
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->setProject($project)
->trigger();
});
App::error()
@@ -781,8 +770,8 @@ App::error()
->inject('project')
->inject('logger')
->inject('log')
->inject('queueForStatsUsage')
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage) {
->inject('queueForUsage')
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Usage $queueForUsage) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
@@ -885,10 +874,7 @@ App::error()
}
}
/**
* If its not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
*/
if (!$publish && $project->getId() !== 'console') {
if ($publish && $project->getId() !== 'console') {
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
@@ -896,17 +882,18 @@ App::error()
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
}
$queueForStatsUsage
$queueForUsage
->setProject($project)
->trigger();
}
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */
@@ -1053,12 +1040,12 @@ App::get('/robots.txt')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('isResourceBlocked')
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $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', '');
@@ -1066,7 +1053,7 @@ App::get('/robots.txt')
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
}
});
@@ -1081,12 +1068,12 @@ App::get('/humans.txt')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('queueForFunctions')
->inject('geodb')
->inject('isResourceBlocked')
->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $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', '');
@@ -1094,7 +1081,7 @@ App::get('/humans.txt')
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname);
}
});
@@ -1161,7 +1148,7 @@ App::get('/v1/ping')
->inject('dbForPlatform')
->inject('queueForEvents')
->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) {
if ($project->isEmpty() || $project->getId() === 'console') {
if ($project->isEmpty()) {
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
}
+142 -101
View File
@@ -1,7 +1,8 @@
<?php
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
@@ -11,11 +12,10 @@ use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Usage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\SDK\Method;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
@@ -28,7 +28,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
use Utopia\System\System;
use Utopia\Validator\WhiteList;
@@ -90,7 +90,7 @@ $eventDatabaseListener = function (Document $project, Document $document, Respon
}
};
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
$usageDatabaseListener = function (string $event, Document $document, Usage $queueForUsage) {
$value = 1;
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$value = -1;
@@ -98,72 +98,80 @@ $usageDatabaseListener = function (string $event, Document $document, StatsUsage
switch (true) {
case $document->getCollection() === 'teams':
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
$queueForUsage
->addMetric(METRIC_TEAMS, $value); // per project
break;
case $document->getCollection() === 'users':
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
$queueForUsage
->addMetric(METRIC_USERS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
$queueForUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'sessions': // sessions
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
$queueForUsage
->addMetric(METRIC_SESSIONS, $value); //per project
break;
case $document->getCollection() === 'databases': // databases
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
$queueForUsage
->addMetric(METRIC_DATABASES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
$queueForUsage
->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_COLLECTIONS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value)
;
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage->addReduce($document);
$queueForUsage
->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_DOCUMENTS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
break;
case $document->getCollection() === 'buckets': //buckets
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_BUCKETS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
$queueForUsage
->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'bucket_'): // files
$parts = explode('_', $document->getCollection());
$bucketInternalId = $parts[1];
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_FILES, $value) // per project
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
break;
case $document->getCollection() === 'functions':
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_FUNCTIONS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$queueForStatsUsage
$queueForUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'deployments':
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$document->getAttribute('resourceType'), $document->getAttribute('resourceInternalId')], METRIC_FUNCTION_ID_DEPLOYMENTS), $value) // per function
@@ -187,79 +195,117 @@ App::init()
->inject('servers')
->inject('mode')
->inject('team')
->inject('apiKey')
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
->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()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
/** Default role */
$roles = Config::getParam('roles', []);
$role = $user->isEmpty()
$role = ($user->isEmpty())
? Role::guests()->toString()
: Role::users()->toString();
/** Allowed Scopes for the role */
$scopes = $roles[$role]['scopes'];
$apiKey = $request->getHeader('x-appwrite-key', '');
// API Key authentication
if (!empty($apiKey)) {
// Do not allow API key and session to be set at the same time
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
if ($apiKey->isExpired()) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
// Remove after migration
if (!\str_contains($apiKey, '_')) {
$keyType = API_KEY_STANDARD;
$authKey = $apiKey;
} else {
[ $keyType, $authKey ] = \explode('_', $apiKey, 2);
}
$role = $apiKey->getRole();
$scopes = $apiKey->getScopes();
if ($keyType === API_KEY_DYNAMIC) {
// Dynamic key
// Disable authorization checks for API keys
Authorization::setDefaultStatus(false);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
if ($apiKey->getRole() === Auth::USER_ROLE_APPS) {
$user = new Document([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
]);
try {
$payload = $jwtObj->decode($authKey);
} catch (JWTException $error) {
throw new Exception(Exception::API_KEY_EXPIRED);
}
$queueForAudits->setUser($user);
}
$projectId = $payload['projectId'] ?? '';
$tokenScopes = $payload['scopes'] ?? [];
if ($apiKey->getType() === API_KEY_STANDARD) {
$dbKey = $project->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
// JWT includes project ID for better security
if ($projectId === $project->getId()) {
$user = new Document([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => 'Dynamic Key',
]);
if ($dbKey) {
$accessedAt = $dbKey->getAttribute('accessedAt', '');
$role = Auth::USER_ROLE_APPS;
$scopes = \array_merge($roles[$role]['scopes'], $tokenScopes);
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.
// Regular key
// Check if given key match project API keys
$key = $project->find('secret', $apiKey, 'keys');
if ($key) {
$user = new Document([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $key->getAttribute('name', 'UNKNOWN'),
]);
$role = Auth::USER_ROLE_APPS;
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
}
Authorization::setRole(Auth::USER_ROLE_APPS);
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
$accessedAt = $key->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$dbKey->setAttribute('accessedAt', DateTime::now());
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
$key->setAttribute('accessedAt', DateTime::now());
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
$sdkValidator = new WhiteList($servers, true);
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
if ($sdkValidator->isValid($sdk)) {
$sdks = $dbKey->getAttribute('sdks', []);
$sdks = $key->getAttribute('sdks', []);
if (!in_array($sdk, $sdks)) {
$sdks[] = $sdk;
$dbKey->setAttribute('sdks', $sdks);
array_push($sdks, $sdk);
$key->setAttribute('sdks', $sdks);
/** Update access time as well */
$dbKey->setAttribute('accessedAt', Datetime::now());
$dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey);
$key->setAttribute('accessedAt', Datetime::now());
$dbForPlatform->updateDocument('keys', $key->getId(), $key);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
@@ -267,7 +313,8 @@ App::init()
$queueForAudits->setUser($user);
}
}
} // Admin User Authentication
}
// Admin User Authentication
elseif (($project->getId() === 'console' && !$team->isEmpty() && !$user->isEmpty()) || ($project->getId() !== 'console' && !$user->isEmpty() && $mode === APP_MODE_ADMIN)) {
$teamId = $team->getId();
$adminRoles = [];
@@ -283,7 +330,7 @@ App::init()
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$scopes = []; // Reset scope if admin
$scopes = []; // reset scope if admin
foreach ($adminRoles as $role) {
$scopes = \array_merge($scopes, $roles[$role]['scopes']);
}
@@ -298,7 +345,9 @@ App::init()
Authorization::setRole($authRole);
}
// Update project last activity
/**
* 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) {
@@ -307,7 +356,9 @@ App::init()
}
}
// Update user last activity
/**
* Update user last activity
*/
if (!empty($user->getId())) {
$accessedAt = $user->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
@@ -321,18 +372,18 @@ App::init()
}
}
/** Do not allow access to disabled services */
/**
* @var ?Method $method
* @var ?\Appwrite\SDK\Method $method
*/
$method = $route->getLabel('sdk', false);
if (\is_array($method)) {
if (is_array($method)) {
$method = $method[0];
}
if (!empty($method)) {
$namespace = $method->getNamespace();
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$namespace]
@@ -342,13 +393,13 @@ App::init()
}
}
// Do now allow access if scope is not allowed
/** Do now allow access if scope is not allowed */
$scope = $route->getLabel('scope', 'none');
if (!\in_array($scope, $scopes)) {
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')');
}
// Do not allow access to blocked accounts
/** Do not allow access to blocked accounts */
if (false === $user->getAttribute('status')) { // Account is blocked
throw new Exception(Exception::USER_BLOCKED);
}
@@ -378,19 +429,18 @@ App::init()
->inject('response')
->inject('project')
->inject('user')
->inject('publisher')
->inject('queue')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForAudits')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('dbForProject')
->inject('timelimit')
->inject('mode')
->inject('apiKey')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, string $mode, ?Key $apiKey) 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();
@@ -421,7 +471,7 @@ App::init()
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
$timeLimitArray[] = $timeLimit;
}
@@ -487,12 +537,6 @@ App::init()
$queueForAudits->setUser($userClone);
}
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
foreach ($apiKey->getDisabledMetrics() as $key) {
$queueForStatsUsage->disableMetric($key);
}
}
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
$queueForBuilds->setProject($project);
@@ -500,14 +544,14 @@ App::init()
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
$queueForFunctions = new Func($publisher);
$queueForWebhooks = new Webhook($publisher);
$queueForEventsClone = new Event($queue);
$queueForFunctions = new Func($queue);
$queueForWebhooks = new Webhook($queue);
$queueForRealtime = new Realtime();
$dbForProject
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_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,
@@ -534,9 +578,12 @@ App::init()
if ($type === 'bucket') {
$bucketId = $parts[1] ?? null;
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
@@ -560,14 +607,6 @@ App::init()
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
//Do not update transformedAt if it's a console user
if (!Auth::isPrivilegedUser(Authorization::getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
}
}
}
$response
@@ -580,7 +619,8 @@ App::init()
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Pragma', 'no-cache')
->addHeader('Expires', '0')
->addHeader('X-Appwrite-Cache', 'miss');
->addHeader('X-Appwrite-Cache', 'miss')
;
}
}
});
@@ -648,7 +688,7 @@ App::shutdown()
->inject('user')
->inject('queueForEvents')
->inject('queueForAudits')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
@@ -657,7 +697,7 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) 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();
@@ -741,7 +781,6 @@ App::shutdown()
foreach ($queueForEvents->getParams() as $key => $value) {
$queueForAudits->setParam($key, $value);
}
$queueForAudits->trigger();
}
@@ -761,7 +800,9 @@ App::shutdown()
$queueForMessaging->trigger();
}
// Cache label
/**
* Cache label
*/
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$resource = $resourceType = null;
@@ -813,13 +854,13 @@ App::shutdown()
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
}
$queueForStatsUsage
$queueForUsage
->setProject($project)
->trigger();
}
+80 -135
View File
@@ -17,6 +17,7 @@ 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;
@@ -155,79 +156,6 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
include __DIR__ . '/controllers/general.php';
function createDatabase(App $app, string $resourceKey, string $dbName, array $collections, mixed $pools, callable $extraSetup = null): void
{
$max = 10;
$sleep = 1;
$attempts = 0;
do {
try {
$attempts++;
$resource = $app->getResource($resourceKey);
/* @var $database Database */
$database = is_callable($resource) ? $resource() : $resource;
break; // exit loop on success
} catch (\Exception $e) {
Console::warning(" └── Database not ready. Retrying connection ({$attempts})...");
$pools->reclaim();
if ($attempts >= $max) {
throw new \Exception(' └── Failed to connect to database: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
Console::success("[Setup] - $dbName database init started...");
// Attempt to create the database
try {
Console::info(" └── Creating database: $dbName...");
$database->create();
} catch (\Exception $e) {
Console::info(" └── Skip: metadata table already exists");
}
// Process collections
foreach ($collections as $key => $collection) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$database->getCollection($key)->isEmpty()) {
continue;
}
Console::info(" └── Creating collection: {$collection['$id']}...");
$attributes = array_map(fn ($attr) => new Document([
'$id' => ID::custom($attr['$id']),
'type' => $attr['type'],
'size' => $attr['size'],
'required' => $attr['required'],
'signed' => $attr['signed'],
'array' => $attr['array'],
'filters' => $attr['filters'],
'default' => $attr['default'] ?? null,
'format' => $attr['format'] ?? ''
]), $collection['attributes']);
$indexes = array_map(fn ($index) => new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]), $collection['indexes']);
$database->createCollection($key, $attributes, $indexes);
}
if ($extraSetup) {
$extraSetup($database);
}
}
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $register) {
$app = new App('UTC');
@@ -236,73 +164,94 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
/** @var Group $pools */
App::setResource('pools', fn () => $pools);
// wait for database to be ready
$attempts = 0;
$max = 10;
$sleep = 1;
do {
try {
$attempts++;
$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})...");
if ($attempts >= $max) {
throw new \Exception('Failed to connect to database: ' . $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < $max);
Console::success('[Setup] - Server database init started...');
try {
Console::success('[Setup] - Creating console database...');
$dbForPlatform->create();
} catch (Duplicate) {
Console::success('[Setup] - Skip: metadata table already exists');
}
if ($dbForPlatform->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForPlatform);
$audit->setup();
}
/** @var array $collections */
$collections = Config::getParam('collections', []);
// create logs database first, `getLogsDB` is a callable.
createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call.
createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections) {
if ($dbForPlatform->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForPlatform);
$audit->setup();
$consoleCollections = $collections['console'];
foreach ($consoleCollections as $key => $collection) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
if (!$dbForPlatform->getCollection($key)->isEmpty()) {
continue;
}
if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() &&
!$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_1')) {
Console::info(" └── Creating default bucket...");
$dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'name' => 'Default',
'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0),
'allowedFileExtensions' => [],
'enabled' => true,
'compression' => 'gzip',
'encryption' => true,
'antivirus' => true,
'fileSecurity' => true,
'$permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'search' => 'buckets Default',
]));
Console::success('[Setup] - Creating console collection: ' . $collection['$id'] . '...');
$bucket = $dbForPlatform->getDocument('buckets', 'default');
$attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']);
Console::info(" └── Creating files collection for default bucket...");
$files = $collections['buckets']['files'] ?? [];
if (empty($files)) {
throw new Exception('Files collection is not configured.');
}
$dbForPlatform->createCollection($key, $attributes, $indexes);
}
$attributes = array_map(fn ($attr) => new Document([
'$id' => ID::custom($attr['$id']),
'type' => $attr['type'],
'size' => $attr['size'],
'required' => $attr['required'],
'signed' => $attr['signed'],
'array' => $attr['array'],
'filters' => $attr['filters'],
'default' => $attr['default'] ?? null,
'format' => $attr['format'] ?? ''
]), $files['attributes']);
if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() && !$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_1')) {
Console::success('[Setup] - Creating default bucket...');
$dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('default'),
'$collection' => ID::custom('buckets'),
'name' => 'Default',
'maximumFileSize' => (int) System::getEnv('_APP_STORAGE_LIMIT', 0), // 10MB
'allowedFileExtensions' => [],
'enabled' => true,
'compression' => 'gzip',
'encryption' => true,
'antivirus' => true,
'fileSecurity' => true,
'$permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'search' => 'buckets Default',
]));
$indexes = array_map(fn ($index) => new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]), $files['indexes']);
$bucket = $dbForPlatform->getDocument('buckets', 'default');
$dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
Console::success('[Setup] - Creating files collection for default bucket...');
$files = $collections['buckets']['files'] ?? [];
if (empty($files)) {
throw new Exception('Files collection is not configured.');
}
});
$attributes = \array_map(fn ($attribute) => new Document($attribute), $files['attributes']);
$indexes = \array_map(fn (array $index) => new Document($index), $files['indexes']);
$dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
}
$projectCollections = $collections['projects'];
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
@@ -330,11 +279,6 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
Console::success('[Setup] - Skip: metadata table already exists');
}
if ($dbForProject->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($dbForProject);
$audit->setup();
}
foreach ($projectCollections as $key => $collection) {
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
@@ -353,6 +297,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
}
$pools->reclaim();
Console::success('[Setup] - Server database init completed...');
});
+58 -122
View File
@@ -21,7 +21,6 @@ if (\file_exists(__DIR__ . '/../vendor/autoload.php')) {
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
@@ -33,7 +32,7 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Usage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\Specification;
@@ -78,6 +77,7 @@ use Utopia\Logger\Logger;
use Utopia\Pools\Group;
use Utopia\Pools\Pool;
use Utopia\Queue;
use Utopia\Queue\Connection;
use Utopia\Registry\Registry;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Backblaze;
@@ -122,7 +122,6 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return
const APP_KEY_ACCESS = 24 * 60 * 60; // 24 hours
const APP_USER_ACCESS = 24 * 60 * 60; // 24 hours
const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_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.1';
@@ -238,6 +237,8 @@ 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}';
const METRIC_MESSAGES = 'messages';
@@ -268,8 +269,6 @@ 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_FILES_IMAGES_TRANSFORMED = 'files.imagesTransformed';
const METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED = '{bucketInternalId}.files.imagesTransformed';
const METRIC_BUCKET_ID_FILES = '{bucketInternalId}.files';
const METRIC_BUCKET_ID_FILES_STORAGE = '{bucketInternalId}.files.storage';
const METRIC_FUNCTIONS = 'functions';
@@ -302,20 +301,6 @@ const METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS = '{functionInternalId}.execution
const METRIC_NETWORK_REQUESTS = 'network.requests';
const METRIC_NETWORK_INBOUND = 'network.inbound';
const METRIC_NETWORK_OUTBOUND = 'network.outbound';
const METRIC_MAU = 'users.mau';
const METRIC_DAU = 'users.dau';
const METRIC_WAU = 'users.wau';
const METRIC_WEBHOOKS = 'webhooks';
const METRIC_PLATFORMS = 'platforms';
const METRIC_PROVIDERS = 'providers';
const METRIC_TOPICS = 'topics';
const METRIC_TARGETS = 'targets';
const METRIC_PROVIDER_TYPE_TARGETS = '{providerType}.targets';
const METRIC_KEYS = 'keys';
const METRIC_RESOURCE_TYPE_ID_BUILDS = '{resourceType}.{resourceInternalId}.builds';
const METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE = '{resourceType}.{resourceInternalId}.builds.storage';
const METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS = '{resourceType}.{resourceInternalId}.deployments';
const METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE = '{resourceType}.{resourceInternalId}.deployments.storage';
// Resource types
@@ -904,14 +889,8 @@ $register->set('pools', function () {
'multiple' => false,
'schemes' => ['mariadb', 'mysql'],
],
'publisher' => [
'type' => 'publisher',
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
'consumer' => [
'type' => 'consumer',
'queue' => [
'type' => 'queue',
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
@@ -1019,26 +998,31 @@ $register->set('pools', function () {
};
$adapter->setDatabase($dsn->getPath());
return $adapter;
break;
case 'pubsub':
return match ($dsn->getScheme()) {
$adapter = match ($dsn->getScheme()) {
'redis' => new PubSub($resource()),
default => null
};
case 'publisher':
case 'consumer':
return match ($dsn->getScheme()) {
'redis' => new Queue\Broker\Redis(new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort())),
break;
case 'queue':
$adapter = match ($dsn->getScheme()) {
'redis' => new Queue\Connection\Redis($dsn->getHost(), $dsn->getPort()),
default => null
};
break;
case 'cache':
return match ($dsn->getScheme()) {
$adapter = match ($dsn->getScheme()) {
'redis' => new RedisCache($resource()),
default => null
};
break;
default:
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation.");
}
return $adapter;
});
$group->add($pool);
@@ -1161,51 +1145,48 @@ App::setResource('localeCodes', function () {
});
// Queues
App::setResource('publisher', function (Group $pools) {
return $pools->get('publisher')->pop()->getResource();
App::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);
App::setResource('consumer', function (Group $pools) {
return $pools->get('consumer')->pop()->getResource();
}, ['pools']);
App::setResource('queueForMessaging', function (Queue\Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
App::setResource('queueForMails', function (Queue\Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
App::setResource('queueForBuilds', function (Queue\Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
App::setResource('queueForDatabase', function (Queue\Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
App::setResource('queueForDeletes', function (Queue\Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
App::setResource('queueForEvents', function (Queue\Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
App::setResource('queueForWebhooks', function (Queue\Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
App::setResource('queueForMessaging', function (Connection $queue) {
return new Messaging($queue);
}, ['queue']);
App::setResource('queueForMails', function (Connection $queue) {
return new Mail($queue);
}, ['queue']);
App::setResource('queueForBuilds', function (Connection $queue) {
return new Build($queue);
}, ['queue']);
App::setResource('queueForDatabase', function (Connection $queue) {
return new EventDatabase($queue);
}, ['queue']);
App::setResource('queueForDeletes', function (Connection $queue) {
return new Delete($queue);
}, ['queue']);
App::setResource('queueForEvents', function (Connection $queue) {
return new Event($queue);
}, ['queue']);
App::setResource('queueForWebhooks', function (Connection $queue) {
return new Webhook($queue);
}, ['queue']);
App::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
App::setResource('queueForStatsUsage', function (Queue\Publisher $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
App::setResource('queueForAudits', function (Queue\Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
App::setResource('queueForFunctions', function (Queue\Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
App::setResource('queueForCertificates', function (Queue\Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
App::setResource('queueForMigrations', function (Queue\Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
App::setResource('queueForAudits', function (Connection $queue) {
return new Audit($queue);
}, ['queue']);
App::setResource('queueForFunctions', function (Connection $queue) {
return new Func($queue);
}, ['queue']);
App::setResource('queueForUsage', function (Connection $queue) {
return new Usage($queue);
}, ['queue']);
App::setResource('queueForCertificates', function (Connection $queue) {
return new Certificate($queue);
}, ['queue']);
App::setResource('queueForMigrations', function (Connection $queue) {
return new Migration($queue);
}, ['queue']);
App::setResource('clients', function ($request, $console, $project) {
$console->setAttribute('platforms', [ // Always allow current host
'$collection' => ID::custom('platforms'),
@@ -1561,39 +1542,6 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform
};
}, ['pools', 'dbForPlatform', 'cache']);
App::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getInternalId());
return $database;
}
$dbAdapter = $pools
->get('logs')
->pop()
->getResource();
$database = new Database(
$dbAdapter,
$cache
);
$database
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
// set tenant
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getInternalId());
}
return $database;
};
}, ['pools', 'cache']);
App::setResource('cache', function (Group $pools) {
$list = Config::getParam('pools-cache', []);
$adapters = [];
@@ -1657,7 +1605,6 @@ function getDevice(string $root, string $connection = ''): Device
$accessSecret = '';
$bucket = '';
$region = '';
$url = App::getEnv('_APP_STORAGE_S3_ENDPOINT', '');
try {
$dsn = new DSN($connection);
@@ -1672,7 +1619,7 @@ function getDevice(string $root, string $connection = ''): Device
switch ($device) {
case Storage::DEVICE_S3:
return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl, $url);
return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl);
case STORAGE::DEVICE_DO_SPACES:
$device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl);
$device->setHttpVersion(S3::HTTP_VERSION_1_1);
@@ -1698,8 +1645,7 @@ function getDevice(string $root, string $connection = ''): Device
$s3Region = System::getEnv('_APP_STORAGE_S3_REGION', '');
$s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', '');
$s3Acl = 'private';
$s3EndpointUrl = App::getEnv('_APP_STORAGE_S3_ENDPOINT', '');
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl, $s3EndpointUrl);
return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
case Storage::DEVICE_DO_SPACES:
$doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', '');
$doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', '');
@@ -1944,13 +1890,3 @@ App::setResource('previewHostname', function (Request $request) {
return '';
}, ['request']);
App::setResource('apiKey', function (Request $request, Document $project): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
return null;
}
return Key::decode($project, $key);
}, ['request', 'project']);
+6 -70
View File
@@ -116,7 +116,6 @@ $image = $this->getParam('image', '');
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_S3_ENDPOINT
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
@@ -319,7 +318,6 @@ $image = $this->getParam('image', '');
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_S3_ENDPOINT
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
@@ -415,7 +413,6 @@ $image = $this->getParam('image', '');
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_S3_ENDPOINT
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
@@ -571,7 +568,6 @@ $image = $this->getParam('image', '');
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_S3_ENDPOINT
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
@@ -653,69 +649,10 @@ $image = $this->getParam('image', '');
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY
- _APP_MAINTENANCE_RETENTION_SCHEDULES
appwrite-task-stats-resources:
appwrite-worker-usage:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
container_name: appwrite-task-stats-resources
entrypoint: stats-resources
<<: *x-logging
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
- _APP_STATS_RESOURCES_INTERVAL
appwrite-worker-stats-resources:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-stats-resources
container_name: appwrite-worker-stats-resources
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_STATS_RESOURCES_INTERVAL
appwrite-worker-stats-usage:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-stats-usage
container_name: appwrite-worker-stats-usage
entrypoint: worker-usage
container_name: appwrite-worker-usage
<<: *x-logging
restart: unless-stopped
networks:
@@ -740,11 +677,11 @@ $image = $this->getParam('image', '');
- _APP_LOGGING_CONFIG
- _APP_USAGE_AGGREGATION_INTERVAL
appwrite-worker-stats-usage-dump:
appwrite-worker-usage-dump:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-stats-usage-dump
entrypoint: worker-usage-dump
<<: *x-logging
container_name: appwrite-worker-stats-usage-dump
container_name: appwrite-worker-usage-dump
restart: unless-stopped
networks:
- appwrite
@@ -885,7 +822,6 @@ $image = $this->getParam('image', '');
- OPR_EXECUTOR_STORAGE_S3_SECRET=$_APP_STORAGE_S3_SECRET
- OPR_EXECUTOR_STORAGE_S3_REGION=$_APP_STORAGE_S3_REGION
- OPR_EXECUTOR_STORAGE_S3_BUCKET=$_APP_STORAGE_S3_BUCKET
- OPR_EXECUTOR_STORAGE_S3_ENDPOINT=$_APP_STORAGE_S3_ENDPOINT
- OPR_EXECUTOR_STORAGE_DO_SPACES_ACCESS_KEY=$_APP_STORAGE_DO_SPACES_ACCESS_KEY
- OPR_EXECUTOR_STORAGE_DO_SPACES_SECRET=$_APP_STORAGE_DO_SPACES_SECRET
- OPR_EXECUTOR_STORAGE_DO_SPACES_REGION=$_APP_STORAGE_DO_SPACES_REGION
+44 -93
View File
@@ -13,12 +13,8 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\StatsUsageDump;
/** remove */
/** /remove */
use Appwrite\Event\Webhook;
use Appwrite\Event\Usage;
use Appwrite\Event\UsageDump;
use Appwrite\Platform\Appwrite;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
@@ -35,8 +31,8 @@ use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Connection;
use Utopia\Queue\Message;
use Utopia\Queue\Publisher;
use Utopia\Queue\Server;
use Utopia\Registry\Registry;
use Utopia\System\System;
@@ -177,39 +173,6 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
};
}, ['pools', 'dbForPlatform', 'cache']);
Server::setResource('getLogsDB', function (Group $pools, Cache $cache) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getInternalId());
return $database;
}
$dbAdapter = $pools
->get('logs')
->pop()
->getResource();
$database = new Database(
$dbAdapter,
$cache
);
$database
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
// set tenant
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getInternalId());
}
return $database;
};
}, ['pools', 'cache']);
Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400);
});
@@ -261,69 +224,57 @@ Server::setResource('timelimit', function (\Redis $redis) {
Server::setResource('log', fn () => new Log());
Server::setResource('publisher', function (Group $pools) {
return $pools->get('publisher')->pop()->getResource();
Server::setResource('queueForUsage', function (Connection $queue) {
return new Usage($queue);
}, ['queue']);
Server::setResource('queueForUsageDump', function (Connection $queue) {
return new UsageDump($queue);
}, ['queue']);
Server::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);
Server::setResource('consumer', function (Group $pools) {
return $pools->get('consumer')->pop()->getResource();
}, ['pools']);
Server::setResource('queueForDatabase', function (Connection $queue) {
return new EventDatabase($queue);
}, ['queue']);
Server::setResource('queueForStatsUsage', function (Publisher $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
Server::setResource('queueForMessaging', function (Connection $queue) {
return new Messaging($queue);
}, ['queue']);
Server::setResource('queueForStatsUsageDump', function (Publisher $publisher) {
return new StatsUsageDump($publisher);
}, ['publisher']);
Server::setResource('queueForMails', function (Connection $queue) {
return new Mail($queue);
}, ['queue']);
Server::setResource('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
Server::setResource('queueForBuilds', function (Connection $queue) {
return new Build($queue);
}, ['queue']);
Server::setResource('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
Server::setResource('queueForDeletes', function (Connection $queue) {
return new Delete($queue);
}, ['queue']);
Server::setResource('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
Server::setResource('queueForEvents', function (Connection $queue) {
return new Event($queue);
}, ['queue']);
Server::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
Server::setResource('queueForAudits', function (Connection $queue) {
return new Audit($queue);
}, ['queue']);
Server::setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
Server::setResource('queueForFunctions', function (Connection $queue) {
return new Func($queue);
}, ['queue']);
Server::setResource('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
Server::setResource('queueForCertificates', function (Connection $queue) {
return new Certificate($queue);
}, ['queue']);
Server::setResource('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
Server::setResource('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
Server::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Server::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
Server::setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
Server::setResource('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
Server::setResource('queueForMigrations', function (Connection $queue) {
return new Migration($queue);
}, ['queue']);
Server::setResource('logger', function (Registry $register) {
return $register->get('logger');
@@ -435,7 +386,7 @@ try {
*/
$platform->init(Service::TYPE_WORKER, [
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
'connection' => $pools->get('consumer')->pop()->getResource(),
'connection' => $pools->get('queue')->pop()->getResource(),
'workerName' => strtolower($workerName) ?? null,
'queueName' => $queueName
]);
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
php /usr/src/code/app/cli.php stats-resources $@
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
php /usr/src/code/app/worker.php stats-resources $@
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
php /usr/src/code/app/worker.php stats-usage $@
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
php /usr/src/code/app/worker.php stats-usage-dump $@
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/worker.php usage $@
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/worker.php usage-dump $@
+13 -13
View File
@@ -45,27 +45,27 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.16.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "0.51.*",
"utopia-php/abuse": "0.48.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "0.54.0",
"utopia-php/audit": "0.48.*",
"utopia-php/cache": "0.11.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.60.*",
"utopia-php/database": "0.57.*",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.17",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.3.*",
"utopia-php/image": "0.8.*",
"utopia-php/image": "0.7.*",
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.16.*",
"utopia-php/messaging": "0.13.*",
"utopia-php/migration": "0.6.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.3",
"utopia-php/platform": "0.7.1",
"utopia-php/pools": "0.5.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.8.*",
"utopia-php/queue": "0.7.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "0.18.*",
"utopia-php/swoole": "0.8.*",
@@ -84,12 +84,12 @@
},
"require-dev": {
"ext-fileinfo": "*",
"appwrite/sdk-generator": "0.40.*",
"phpunit/phpunit": "9.*",
"appwrite/sdk-generator": "0.39.30",
"phpunit/phpunit": "9.5.20",
"swoole/ide-helper": "5.1.2",
"textalk/websocket": "1.5.*",
"laravel/pint": "1.*",
"phpbench/phpbench": "1.*"
"textalk/websocket": "1.5.7",
"laravel/pint": "^1.14",
"phpbench/phpbench": "^1.2"
},
"provide": {
"ext-phpiredis": "*"
Generated
+565 -462
View File
File diff suppressed because it is too large Load Diff
+7 -75
View File
@@ -140,7 +140,6 @@ services:
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_S3_ENDPOINT
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
@@ -366,7 +365,6 @@ services:
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_S3_ENDPOINT
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
@@ -468,7 +466,6 @@ services:
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_S3_ENDPOINT
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
@@ -634,7 +631,6 @@ services:
- _APP_STORAGE_S3_SECRET
- _APP_STORAGE_S3_REGION
- _APP_STORAGE_S3_BUCKET
- _APP_STORAGE_S3_ENDPOINT
- _APP_STORAGE_DO_SPACES_ACCESS_KEY
- _APP_STORAGE_DO_SPACES_SECRET
- _APP_STORAGE_DO_SPACES_REGION
@@ -726,72 +722,10 @@ services:
- _APP_MAINTENANCE_DELAY
- _APP_DATABASE_SHARED_TABLES
appwrite-task-stats-resources:
container_name: appwrite-task-stats-resources
entrypoint: stats-resources
appwrite-worker-usage:
entrypoint: worker-usage
<<: *x-logging
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
- _APP_STATS_RESOURCES_INTERVAL
appwrite-worker-stats-resources:
entrypoint: worker-stats-resources
<<: *x-logging
container_name: appwrite-worker-stats-resources
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-stats-usage:
entrypoint: worker-stats-usage
<<: *x-logging
container_name: appwrite-worker-stats-usage
container_name: appwrite-worker-usage
image: appwrite-dev
networks:
- appwrite
@@ -819,10 +753,10 @@ services:
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-stats-usage-dump:
entrypoint: worker-stats-usage-dump
appwrite-worker-usage-dump:
entrypoint: worker-usage-dump
<<: *x-logging
container_name: appwrite-worker-stats-usage-dump
container_name: appwrite-worker-usage-dump
image: appwrite-dev
networks:
- appwrite
@@ -849,8 +783,7 @@ services:
- _APP_LOGGING_CONFIG
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_DATABASE_SHARED_TABLES
- _APP_STATS_USAGE_DUAL_WRITING_DBS
appwrite-task-scheduler-functions:
entrypoint: schedule-functions
<<: *x-logging
@@ -975,7 +908,6 @@ services:
- OPR_EXECUTOR_STORAGE_S3_SECRET=$_APP_STORAGE_S3_SECRET
- OPR_EXECUTOR_STORAGE_S3_REGION=$_APP_STORAGE_S3_REGION
- OPR_EXECUTOR_STORAGE_S3_BUCKET=$_APP_STORAGE_S3_BUCKET
- OPR_EXECUTOR_STORAGE_S3_ENDPOINT=$_APP_STORAGE_S3_ENDPOINT
- OPR_EXECUTOR_STORAGE_DO_SPACES_ACCESS_KEY=$_APP_STORAGE_DO_SPACES_ACCESS_KEY
- OPR_EXECUTOR_STORAGE_DO_SPACES_SECRET=$_APP_STORAGE_DO_SPACES_SECRET
- OPR_EXECUTOR_STORAGE_DO_SPACES_REGION=$_APP_STORAGE_DO_SPACES_REGION
+1 -1
View File
@@ -1 +1 @@
Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection) API or directly from your database console.
Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection) API or directly from your database console.
@@ -1 +0,0 @@
Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue.
+1 -1
View File
@@ -1 +1 @@
Update an email message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.
Update an email message by its unique ID.
+1 -1
View File
@@ -1 +1 @@
Update a push notification by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.
Update a push notification by its unique ID.
+1 -1
View File
@@ -1 +1 @@
Update an SMS message by its unique ID. This endpoint only works on messages that are in draft status. Messages that are already processing, sent, or failed cannot be updated.
Update an SMS message by its unique ID.
+2 -2
View File
@@ -175,9 +175,9 @@ Please mention in your documentation what resources or API docs you used to impl
## 3. Test your provider
After you finish adding your new provider to Appwrite, you should be able to see it in your Appwrite console. Navigate to 'Project > Users > Providers' and check your new provider's settings form.
After you finished adding your new provider to Appwrite, you should be able to see it in your Appwrite console. Navigate to 'Project > Users > Providers' and check your new provider's settings form.
> To start the Appwrite console from the source code, you can simply run `docker compose up -d'.
> To start Appwrite console from the source code, you can simply run `docker compose up -d'.
Add credentials and check both a successful and a failed login (where the user denies integration on the provider page).
-158
View File
@@ -1,158 +0,0 @@
<?php
namespace Appwrite\Auth;
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Utopia\Config\Config;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\System\System;
class Key
{
public function __construct(
protected string $projectId,
protected string $type,
protected string $role,
protected array $scopes,
protected string $name,
protected bool $expired = false,
protected array $disabledMetrics = [],
) {
}
public function getProjectId(): string
{
return $this->projectId;
}
public function getType(): string
{
return $this->type;
}
public function getRole(): string
{
return $this->role;
}
public function getScopes(): array
{
return $this->scopes;
}
public function getName(): string
{
return $this->name;
}
public function isExpired(): bool
{
return $this->expired;
}
public function getDisabledMetrics(): array
{
return $this->disabledMetrics;
}
/**
* Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name.
* Can be a stored API key or a dynamic key (JWT).
*
* @param Document $project
* @param string $key
* @return Key
* @throws Exception
*/
public static function decode(
Document $project,
string $key
): Key {
if (\str_contains($key, '_')) {
[$type, $secret] = \explode('_', $key, 2);
} else {
$type = API_KEY_STANDARD;
$secret = $key;
}
$role = Auth::USER_ROLE_APPS;
$roles = Config::getParam('roles', []);
$scopes = $roles[Auth::USER_ROLE_APPS]['scopes'] ?? [];
$expired = false;
$guestKey = new Key(
$project->getId(),
$type,
Auth::USER_ROLE_GUESTS,
$roles[Auth::USER_ROLE_GUESTS]['scopes'] ?? [],
'UNKNOWN'
);
switch ($type) {
case API_KEY_DYNAMIC:
$jwtObj = new JWT(
key: System::getEnv('_APP_OPENSSL_KEY_V1'),
algo: 'HS256',
maxAge: 86400,
leeway: 0
);
try {
$payload = $jwtObj->decode($secret);
} catch (JWTException) {
$expired = true;
}
$name = $payload['name'] ?? 'Dynamic Key';
$projectId = $payload['projectId'] ?? '';
$disabledMetrics = $payload['disabledMetrics'] ?? [];
$scopes = \array_merge($payload['scopes'] ?? [], $scopes);
if ($projectId !== $project->getId()) {
return $guestKey;
}
return new Key(
$projectId,
$type,
$role,
$scopes,
$name,
$expired,
$disabledMetrics
);
case API_KEY_STANDARD:
$key = $project->find(
key: 'secret',
find: $key,
subject: 'keys'
);
if (!$key) {
return $guestKey;
}
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
$expired = true;
}
$name = $key->getAttribute('name', 'UNKNOWN');
$scopes = \array_merge($key->getAttribute('scopes', []), $scopes);
return new Key(
$project->getId(),
$type,
$role,
$scopes,
$name,
$expired
);
default:
return $guestKey;
}
}
}
+14 -13
View File
@@ -20,9 +20,10 @@ class Slack extends OAuth2
* @var array
*/
protected array $scopes = [
'openid',
'email',
'profile'
'identity.avatar',
'identity.basic',
'identity.email',
'identity.team'
];
/**
@@ -34,15 +35,14 @@ class Slack extends OAuth2
}
/**
* @link https://api.slack.com/authentication/oauth-v2
*
* @return string
*/
public function getLoginURL(): string
{
return 'https://slack.com/oauth/v2/authorize?' . \http_build_query([
// https://api.slack.com/docs/oauth#step_1_-_sending_users_to_authorize_and_or_install
return 'https://slack.com/oauth/authorize?' . \http_build_query([
'client_id' => $this->appID,
'user_scope' => \implode(' ', $this->getScopes()),
'scope' => \implode(' ', $this->getScopes()),
'redirect_uri' => $this->callback,
'state' => \json_encode($this->state)
]);
@@ -56,15 +56,16 @@ class Slack extends OAuth2
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
// https://api.slack.com/docs/oauth#step_3_-_exchanging_a_verification_code_for_an_access_token
$this->tokens = \json_decode($this->request(
'GET',
'https://slack.com/api/oauth.v2.access?' . \http_build_query([
'https://slack.com/api/oauth.access?' . \http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'code' => $code,
'redirect_uri' => $this->callback
])
), true)['authed_user'] ?? [];
), true);
}
return $this->tokens;
@@ -79,13 +80,13 @@ class Slack extends OAuth2
{
$this->tokens = \json_decode($this->request(
'GET',
'https://slack.com/api/oauth.v2.access?' . \http_build_query([
'https://slack.com/api/oauth.access?' . \http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
), true)['authed_user'] ?? [];
), true);
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
@@ -160,9 +161,9 @@ class Slack extends OAuth2
if (empty($this->user)) {
$user = $this->request(
'GET',
'https://slack.com/api/users.identity',
['Authorization: Bearer ' . \urlencode($accessToken)]
'https://slack.com/api/users.identity?token=' . \urlencode($accessToken)
);
$this->user = \json_decode($user, true);
}
+3 -3
View File
@@ -2,7 +2,7 @@
namespace Appwrite\Event;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Audit extends Event
{
@@ -12,9 +12,9 @@ class Audit extends Event
protected string $ip = '';
protected string $hostname = '';
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::AUDITS_QUEUE_NAME)
+3 -3
View File
@@ -3,7 +3,7 @@
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Build extends Event
{
@@ -12,9 +12,9 @@ class Build extends Event
protected ?Document $deployment = null;
protected ?Document $template = null;
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::BUILDS_QUEUE_NAME)
+3 -3
View File
@@ -3,16 +3,16 @@
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Certificate extends Event
{
protected bool $skipRenewCheck = false;
protected ?Document $domain = null;
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::CERTIFICATES_QUEUE_NAME)
+3 -3
View File
@@ -4,7 +4,7 @@ namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\DSN\DSN;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Database extends Event
{
@@ -13,9 +13,9 @@ class Database extends Event
protected ?Document $collection = null;
protected ?Document $document = null;
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this->setClass(Event::DATABASE_CLASS_NAME);
}
+3 -3
View File
@@ -3,7 +3,7 @@
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Delete extends Event
{
@@ -15,9 +15,9 @@ class Delete extends Event
protected ?string $hourlyUsageRetentionDatetime = null;
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::DELETE_QUEUE_NAME)
+7 -17
View File
@@ -4,8 +4,8 @@ namespace Appwrite\Event;
use InvalidArgumentException;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
class Event
{
@@ -24,22 +24,11 @@ class Event
public const FUNCTIONS_QUEUE_NAME = 'v1-functions';
public const FUNCTIONS_CLASS_NAME = 'FunctionsV1';
/** remove */
public const USAGE_QUEUE_NAME = 'v1-usage';
public const USAGE_CLASS_NAME = 'UsageV1';
public const USAGE_DUMP_QUEUE_NAME = 'v1-usage-dump';
public const USAGE_DUMP_CLASS_NAME = 'UsageDumpV1';
/** /remove */
public const STATS_RESOURCES_QUEUE_NAME = 'v1-stats-resources';
public const STATS_RESOURCES_CLASS_NAME = 'StatsResourcesV1';
public const STATS_USAGE_QUEUE_NAME = 'v1-stats-usage';
public const STATS_USAGE_CLASS_NAME = 'StatsUsageV1';
public const STATS_USAGE_DUMP_QUEUE_NAME = 'v1-stats-usage-dump';
public const STATS_USAGE_DUMP_CLASS_NAME = 'StatsUsageDumpV1';
public const WEBHOOK_QUEUE_NAME = 'v1-webhooks';
public const WEBHOOK_CLASS_NAME = 'WebhooksV1';
@@ -69,10 +58,10 @@ class Event
protected bool $paused = false;
/**
* @param Publisher $publisher
* @param Connection $connection
* @return void
*/
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
}
@@ -356,11 +345,12 @@ class Event
}
/** The getter is required since events like Databases need to override the queue name depending on the project */
$queue = new Queue($this->getQueue());
$client = new Client($this->getQueue(), $this->connection);
// Merge the base payload with any trimmed values
$payload = array_merge($this->preparePayload(), $this->trimPayload());
return $this->publisher->enqueue($queue, $payload);
return $client->enqueue($payload);
}
/**
+3 -3
View File
@@ -3,7 +3,7 @@
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Func extends Event
{
@@ -19,9 +19,9 @@ class Func extends Event
protected ?Document $function = null;
protected ?Document $execution = null;
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::FUNCTIONS_QUEUE_NAME)
+3 -3
View File
@@ -2,7 +2,7 @@
namespace Appwrite\Event;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Mail extends Event
{
@@ -15,9 +15,9 @@ class Mail extends Event
protected string $bodyTemplate = '';
protected array $attachment = [];
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::MAILS_QUEUE_NAME)
+3 -3
View File
@@ -3,7 +3,7 @@
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Messaging extends Event
{
@@ -14,9 +14,9 @@ class Messaging extends Event
protected ?string $scheduledAt = null;
protected ?string $providerType = null;
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::MESSAGING_QUEUE_NAME)
+3 -3
View File
@@ -3,16 +3,16 @@
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Migration extends Event
{
protected string $type = '';
protected ?Document $migration = null;
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::MIGRATIONS_QUEUE_NAME)
+11 -46
View File
@@ -7,17 +7,10 @@ use Utopia\Database\Document;
class Realtime extends Event
{
protected array $subscribers = [];
public function __construct()
{
}
/**
* Get Realtime payload for this event.
*
* @return array
*/
public function getRealtimePayload(): array
{
$payload = [];
@@ -31,28 +24,6 @@ class Realtime extends Event
return $payload;
}
/**
* Set subscribers for this realtime event.
*
* @param array $subscribers
* @return array
*/
public function setSubscribers(array $subscribers): self
{
$this->subscribers = $subscribers;
return $this;
}
/**
* Get subscribers for this realtime event.
*
* @return array
*/
public function getSubscribers(): array
{
return $this->subscribers;
}
/**
* Execute Event.
*
@@ -82,23 +53,17 @@ class Realtime extends Event
bucket: $bucket,
);
$projectIds = !empty($this->getSubscribers())
? $this->getSubscribers()
: [$target['projectId'] ?? $this->getProject()->getId()];
foreach ($projectIds as $projectId) {
RealtimeAdapter::send(
projectId: $projectId,
payload: $this->getRealtimePayload(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
options: [
'permissionsChanged' => $target['permissionsChanged'],
'userId' => $this->getParam('userId')
]
);
}
RealtimeAdapter::send(
projectId: $target['projectId'] ?? $this->getProject()->getId(),
payload: $this->getRealtimePayload(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
options: [
'permissionsChanged' => $target['permissionsChanged'],
'userId' => $this->getParam('userId')
]
);
return true;
}
-29
View File
@@ -1,29 +0,0 @@
<?php
namespace Appwrite\Event;
use Utopia\Queue\Publisher;
class StatsResources extends Event
{
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(Event::STATS_RESOURCES_QUEUE_NAME)
->setClass(Event::STATS_RESOURCES_CLASS_NAME);
}
/**
* Prepare the payload for the usage event.
*
* @return array
*/
protected function preparePayload(): array
{
return [
'project' => $this->project
];
}
}
-86
View File
@@ -1,86 +0,0 @@
<?php
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
class StatsUsage extends Event
{
protected array $metrics = [];
protected array $reduce = [];
protected array $disabled = [];
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(Event::STATS_USAGE_QUEUE_NAME)
->setClass(Event::STATS_USAGE_CLASS_NAME);
}
/**
* Add reduce.
*
* @param Document $document
* @return self
*/
public function addReduce(Document $document): self
{
$this->reduce[] = $document;
return $this;
}
/**
* Add metric.
*
* @param string $key
* @param int $value
* @return self
*/
public function addMetric(string $key, int $value): self
{
$this->metrics[] = [
'key' => $key,
'value' => $value,
];
return $this;
}
/**
* Set disabled metrics.
*
* @param string $key
* @return self
*/
public function disableMetric(string $key): self
{
$this->disabled[] = $key;
return $this;
}
/**
* Prepare the payload for the event
*
* @return array
*/
protected function preparePayload(): array
{
return [
'project' => $this->getProject(),
'reduce' => $this->reduce,
'metrics' => \array_filter($this->metrics, function ($metric) {
foreach ($this->disabled as $disabledMetric) {
if (\str_ends_with($metric['key'], $disabledMetric)) {
return false;
}
}
return true;
}),
];
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Connection;
class Usage extends Event
{
protected array $metrics = [];
protected array $reduce = [];
public function __construct(protected Connection $connection)
{
parent::__construct($connection);
$this
->setQueue(Event::USAGE_QUEUE_NAME)
->setClass(Event::USAGE_CLASS_NAME);
}
/**
* Add reduce.
*
* @param Document $document
* @return self
*/
public function addReduce(Document $document): self
{
$this->reduce[] = $document;
return $this;
}
/**
* Add metric.
*
* @param string $key
* @param int $value
* @return self
*/
public function addMetric(string $key, int $value): self
{
$this->metrics[] = [
'key' => $key,
'value' => $value,
];
return $this;
}
/**
* Prepare the payload for the usage event.
*
* @return array
*/
protected function preparePayload(): array
{
return [
'project' => $this->project,
'reduce' => $this->reduce,
'metrics' => $this->metrics,
];
}
/**
* Sends metrics to the usage worker.
*
* @return string|bool
*/
public function trigger(): string|bool
{
parent::trigger();
$this->metrics = [];
return true;
}
}
@@ -2,19 +2,19 @@
namespace Appwrite\Event;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class StatsUsageDump extends Event
class UsageDump extends Event
{
protected array $stats;
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::STATS_USAGE_DUMP_QUEUE_NAME)
->setClass(Event::STATS_USAGE_DUMP_CLASS_NAME);
->setQueue(Event::USAGE_DUMP_QUEUE_NAME)
->setClass(Event::USAGE_DUMP_CLASS_NAME);
}
/**
+3 -3
View File
@@ -2,13 +2,13 @@
namespace Appwrite\Event;
use Utopia\Queue\Publisher;
use Utopia\Queue\Connection;
class Webhook extends Event
{
public function __construct(protected Publisher $publisher)
public function __construct(protected Connection $connection)
{
parent::__construct($publisher);
parent::__construct($connection);
$this
->setQueue(Event::WEBHOOK_QUEUE_NAME)
-1
View File
@@ -92,7 +92,6 @@ abstract class Migration
'1.5.11' => 'V20',
'1.6.0' => 'V21',
'1.6.1' => 'V21',
'1.6.2' => 'V22',
];
/**
-83
View File
@@ -1,83 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Exception;
use Throwable;
use Utopia\CLI\Console;
use Utopia\Database\Database;
class V22 extends Migration
{
/**
* @throws Throwable
*/
public function execute(): void
{
/**
* Disable SubQueries for Performance.
*/
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables', 'subQueryChallenges', 'subQueryProjectVariables', 'subQueryTargets', 'subQueryTopicTargets'] as $name) {
Database::addFilter(
$name,
fn () => null,
fn () => []
);
}
Console::info('Migrating Collections');
$this->migrateCollections();
}
/**
* Migrate Collections.
*
* @return void
* @throws Exception|Throwable
*/
private function migrateCollections(): void
{
$internalProjectId = $this->project->getInternalId();
$collectionType = match ($internalProjectId) {
'console' => 'console',
default => 'projects',
};
$collections = $this->collections[$collectionType];
foreach ($collections as $collection) {
$id = $collection['$id'];
Console::log("Migrating Collection \"{$id}\"");
$this->projectDB->setNamespace("_$internalProjectId");
switch ($id) {
case 'installations':
// Create personalAccessToken attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'personalAccessToken');
} catch (Throwable $th) {
Console::warning("'personalAccessToken' from {$id}: {$th->getMessage()}");
}
// Create personalAccessTokenExpiry attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'personalAccessTokenExpiry');
} catch (Throwable $th) {
Console::warning("'personalAccessTokenExpiry' from {$id}: {$th->getMessage()}");
}
// Create personalRefreshToken attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'personalRefreshToken');
} catch (Throwable $th) {
Console::warning("'personalRefreshToken' from {$id}: {$th->getMessage()}");
}
break;
}
usleep(50000);
}
}
}
-90
View File
@@ -1,90 +0,0 @@
<?php
namespace Appwrite\Platform;
use Swoole\Coroutine as Co;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Platform\Action as UtopiaAction;
class Action extends UtopiaAction
{
/**
* Log Error Callback
*
* @var callable
*/
protected mixed $logError;
/**
* Foreach Document
* Call provided callback for each document in the collection
*
* @param string $projectId
* @param string $collection
* @param array $queries
* @param callable $callback
*
* @return void
*/
protected function foreachDocument(Database $database, string $collection, array $queries = [], callable $callback = null, int $limit = 1000, bool $concurrent = false): void
{
$results = [];
$sum = $limit;
$latestDocument = null;
while ($sum === $limit) {
$newQueries = $queries;
try {
if ($latestDocument !== null) {
array_unshift($newQueries, Query::cursorAfter($latestDocument));
}
$newQueries[] = Query::limit($limit);
$database->disableValidation();
$results = $database->find($collection, $newQueries);
$database->enableValidation();
} catch (\Exception $e) {
if (!empty($this->logError)) {
call_user_func_array($this->logError, [$e, "CLI", "fetch_documents_namespace_{$database->getNamespace()}_collection{$collection}"]);
}
}
if (empty($results)) {
return;
}
$sum = count($results);
if ($concurrent) {
$callables = [];
$errors = [];
foreach ($results as $document) {
if (is_callable($callback)) {
$callables[] = Co\go(function () use ($document, $callback, &$errors) {
try {
$callback($document);
} catch (\Throwable $error) {
$errors[] = $error;
}
});
}
}
Co::join($callables);
if (!empty($errors)) {
throw new \Error("Errors found in concurrent foreachDocument: " . \json_encode($errors));
}
} else {
foreach ($results as $document) {
if (is_callable($callback)) {
$callback($document);
}
}
}
$latestDocument = $results[array_key_last($results)];
}
}
}
+2 -2
View File
@@ -6,6 +6,7 @@ use Appwrite\Platform\Tasks\Doctor;
use Appwrite\Platform\Tasks\Install;
use Appwrite\Platform\Tasks\Maintenance;
use Appwrite\Platform\Tasks\Migrate;
use Appwrite\Platform\Tasks\QueueCount;
use Appwrite\Platform\Tasks\QueueRetry;
use Appwrite\Platform\Tasks\ScheduleExecutions;
use Appwrite\Platform\Tasks\ScheduleFunctions;
@@ -13,7 +14,6 @@ use Appwrite\Platform\Tasks\ScheduleMessages;
use Appwrite\Platform\Tasks\SDKs;
use Appwrite\Platform\Tasks\Specs;
use Appwrite\Platform\Tasks\SSL;
use Appwrite\Platform\Tasks\StatsResources;
use Appwrite\Platform\Tasks\Upgrade;
use Appwrite\Platform\Tasks\Vars;
use Appwrite\Platform\Tasks\Version;
@@ -29,6 +29,7 @@ class Tasks extends Service
->addAction(Install::getName(), new Install())
->addAction(Maintenance::getName(), new Maintenance())
->addAction(Migrate::getName(), new Migrate())
->addAction(QueueCount::getName(), new QueueCount())
->addAction(QueueRetry::getName(), new QueueRetry())
->addAction(SDKs::getName(), new SDKs())
->addAction(SSL::getName(), new SSL())
@@ -39,7 +40,6 @@ class Tasks extends Service
->addAction(Upgrade::getName(), new Upgrade())
->addAction(Vars::getName(), new Vars())
->addAction(Version::getName(), new Version())
->addAction(StatsResources::getName(), new StatsResources())
;
}
}
+5 -6
View File
@@ -11,9 +11,8 @@ use Appwrite\Platform\Workers\Functions;
use Appwrite\Platform\Workers\Mails;
use Appwrite\Platform\Workers\Messaging;
use Appwrite\Platform\Workers\Migrations;
use Appwrite\Platform\Workers\StatsResources;
use Appwrite\Platform\Workers\StatsUsage;
use Appwrite\Platform\Workers\StatsUsageDump;
use Appwrite\Platform\Workers\Usage;
use Appwrite\Platform\Workers\UsageDump;
use Appwrite\Platform\Workers\Webhooks;
use Utopia\Platform\Service;
@@ -32,10 +31,10 @@ class Workers extends Service
->addAction(Mails::getName(), new Mails())
->addAction(Messaging::getName(), new Messaging())
->addAction(Webhooks::getName(), new Webhooks())
->addAction(StatsUsageDump::getName(), new StatsUsageDump())
->addAction(StatsUsage::getName(), new StatsUsage())
->addAction(UsageDump::getName(), new UsageDump())
->addAction(Usage::getName(), new Usage())
->addAction(Migrations::getName(), new Migrations())
->addAction(StatsResources::getName(), new StatsResources())
;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\CLI\Console;
use Utopia\Platform\Action;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class QueueCount extends Action
{
public static function getName(): string
{
return 'queue-count';
}
public function __construct()
{
$this
->desc('Return the number of from a specific queue identified by the name parameter with a specific type')
->param('name', '', new Text(100), 'Queue name')
->param('type', '', new WhiteList([
'success',
'failed',
'processing',
]), 'Queue type')
->inject('queue')
->callback(fn ($name, $type, $queue) => $this->action($name, $type, $queue));
}
/**
* @param string $name The name of the queue to count the jobs from
* @param string $type The type of jobs to count
* @param Connection $queue
*/
public function action(string $name, string $type, Connection $queue): void
{
if (!$name) {
Console::error('Missing required parameter $name');
return;
}
$queueClient = new Client($name, $queue);
$count = match ($type) {
'success' => $queueClient->countSuccessfulJobs(),
'failed' => $queueClient->countFailedJobs(),
'processing' => $queueClient->countProcessingJobs(),
default => 0
};
Console::log("Queue: '{$name}' has {$count} {$type} jobs.");
}
}
+16 -7
View File
@@ -4,8 +4,8 @@ namespace Appwrite\Platform\Tasks;
use Utopia\CLI\Console;
use Utopia\Platform\Action;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
use Utopia\Validator\Text;
use Utopia\Validator\Wildcard;
@@ -23,24 +23,33 @@ class QueueRetry extends Action
->desc('Retry failed jobs from a specific queue identified by the name parameter')
->param('name', '', new Text(100), 'Queue name')
->param('limit', 0, new Wildcard(), 'jobs limit', true)
->inject('publisher')
->callback(fn ($name, $limit, $publisher) => $this->action($name, $limit, $publisher));
->inject('queue')
->callback(fn ($name, $limit, $queue) => $this->action($name, $limit, $queue));
}
/**
* @param string $name The name of the queue to retry jobs from
* @param mixed $limit
* @param Publisher $publisher
* @param Connection $queue
*/
public function action(string $name, mixed $limit, Publisher $publisher): void
public function action(string $name, mixed $limit, Connection $queue): void
{
if (!$name) {
Console::error('Missing required parameter $name');
return;
}
$limit = (int)$limit;
$queueClient = new Client($name, $queue);
if ($queueClient->countFailedJobs() === 0) {
Console::error('No failed jobs found.');
return;
}
Console::log('Retrying failed jobs...');
$publisher->retry(new Queue($name), $limit);
$queueClient->retry($limit);
}
}
@@ -29,7 +29,7 @@ class ScheduleExecutions extends ScheduleBase
protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void
{
$queue = $pools->get('publisher')->pop();
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
$queueForFunctions = new Func($connection);
$intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds');
@@ -73,7 +73,7 @@ class ScheduleFunctions extends ScheduleBase
\go(function () use ($delay, $scheduleKeys, $pools, $dbForPlatform) {
\sleep($delay); // in seconds
$queue = $pools->get('publisher')->pop();
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
foreach ($scheduleKeys as $scheduleKey) {
@@ -41,7 +41,7 @@ class ScheduleMessages extends ScheduleBase
}
\go(function () use ($schedule, $pools, $dbForPlatform) {
$queue = $pools->get('publisher')->pop();
$queue = $pools->get('queue')->pop();
$connection = $queue->getResource();
$queueForMessaging = new Messaging($connection);
@@ -1,81 +0,0 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\StatsResources as EventStatsResources;
use Appwrite\Platform\Action;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\System\System;
/**
* Usage count
*
* Runs every hour, schedules project
* for aggregating resource count
*/
class StatsResources extends Action
{
/**
* Log Error Callback
*
* @var callable
*/
protected mixed $logError;
/**
* Console DB
*
* @var Database
*/
protected Database $dbForPlatform;
public static function getName()
{
return 'stats-resources';
}
public function __construct()
{
$this
->desc('Schedules projects for usage count')
->inject('dbForPlatform')
->inject('logError')
->inject('queueForStatsResources')
->callback([$this, 'action']);
}
public function action(Database $dbForPlatform, callable $logError, EventStatsResources $queue): void
{
$this->logError = $logError;
$this->dbForPlatform = $dbForPlatform;
Console::title("Stats resources V1");
Console::success('Stats resources: started');
$interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600');
Console::loop(function () use ($queue) {
Authorization::disable();
Authorization::setDefaultStatus(false);
$last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours'));
/**
* For each project that were accessed in last 24 hours
*/
$this->foreachDocument($this->dbForPlatform, 'projects', [
Query::greaterThanEqual('accessedAt', DateTime::format($last24Hours))
], function ($project) use ($queue) {
$queue
->setProject($project)
->trigger();
Console::success('project: ' . $project->getId() . '(' . $project->getInternalId() . ')' . ' queued');
});
}, $interval);
Console::log("Stats resources: exited");
}
}
+18 -81
View File
@@ -6,33 +6,15 @@ use Appwrite\Auth\Auth;
use Exception;
use Throwable;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\System\System;
class Audits extends Action
{
protected const BATCH_SIZE_DEVELOPMENT = 1; // smaller batch size for development
protected const BATCH_SIZE_PRODUCTION = 5_000;
protected const BATCH_AGGREGATION_INTERVAL = 60; // in seconds
private int $lastTriggeredTime = 0;
private array $logs = [];
protected function getBatchSize(): int
{
return System::getEnv('_APP_ENV', 'development') === 'development'
? self::BATCH_SIZE_DEVELOPMENT
: self::BATCH_SIZE_PRODUCTION;
}
public static function getName(): string
{
return 'audits';
@@ -46,34 +28,29 @@ class Audits extends Action
$this
->desc('Audits worker')
->inject('message')
->inject('getProjectDB')
->inject('project')
->callback([$this, 'action']);
$this->lastTriggeredTime = time();
->inject('dbForProject')
->callback(fn ($message, $dbForProject) => $this->action($message, $dbForProject));
}
/**
* @param Message $message
* @param callable $getProjectDB
* @param Document $project
* @param Database $dbForProject
* @return void
* @throws Throwable
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Structure
*/
public function action(Message $message, callable $getProjectDB, Document $project): void
public function action(Message $message, Database $dbForProject): void
{
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
throw new Exception('Missing payload');
}
Console::info('Aggregating audit logs');
$event = $payload['event'] ?? '';
$auditPayload = $payload['payload'] ?? '';
$mode = $payload['mode'] ?? '';
@@ -86,63 +63,23 @@ class Audits extends Action
$userEmail = $user->getAttribute('email', '');
$userType = $user->getAttribute('type', Auth::ACTIVITY_TYPE_USER);
// Create event data
$eventData = [
'userId' => $user->getInternalId(),
'event' => $event,
'resource' => $resource,
'userAgent' => $userAgent,
'ip' => $ip,
'location' => '',
'data' => [
$audit = new Audit($dbForProject);
$audit->log(
userId: $user->getInternalId(),
// Pass first, most verbose event pattern
event: $event,
resource: $resource,
userAgent: $userAgent,
ip: $ip,
location: '',
data: [
'userId' => $user->getId(),
'userName' => $userName,
'userEmail' => $userEmail,
'userType' => $userType,
'mode' => $mode,
'data' => $auditPayload,
],
'timestamp' => DateTime::formatTz(DateTime::now())
];
if (isset($this->logs[$project->getInternalId()])) {
$this->logs[$project->getInternalId()]['logs'][] = $eventData;
} else {
$this->logs[$project->getInternalId()] = [
'project' => new Document([
'$id' => $project->getId(),
'$internalId' => $project->getInternalId(),
'database' => $project->getAttribute('database'),
]),
'logs' => [$eventData]
];
}
// Check if we should process the batch by checking both for the batch size and the elapsed time
$batchSize = $this->getBatchSize();
$shouldProcessBatch = \count($this->logs) >= $batchSize;
if (!$shouldProcessBatch && \count($this->logs) > 0) {
$shouldProcessBatch = (\time() - $this->lastTriggeredTime) >= self::BATCH_AGGREGATION_INTERVAL;
}
if ($shouldProcessBatch) {
try {
foreach ($this->logs as $internalId => $projectLogs) {
$dbForProject = $getProjectDB($projectLogs['project']);
Console::log('Processing batch with ' . count($projectLogs['logs']) . ' events');
$audit = new Audit($dbForProject);
$audit->logBatch($projectLogs['logs']);
Console::success('Audit logs processed successfully');
unset($this->logs[$internalId]);
}
} catch (Throwable $e) {
Console::error('Error processing audit logs: ' . $e->getMessage());
} finally {
$this->lastTriggeredTime = time();
}
}
]
);
}
}
+84 -73
View File
@@ -5,9 +5,8 @@ namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Event\Usage;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Vcs\Comment;
use Exception;
@@ -50,17 +49,13 @@ class Builds extends Action
->inject('project')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('cache')
->inject('dbForProject')
->inject('deviceForFunctions')
->inject('isResourceBlocked')
->inject('log')
->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log) =>
$this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $usage, $cache, $dbForProject, $deviceForFunctions, $isResourceBlocked, $log));
->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log));
}
/**
@@ -68,10 +63,8 @@ class Builds extends Action
* @param Document $project
* @param Database $dbForPlatform
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param StatsUsage $queueForStatsUsage
* @param Usage $queueForUsage
* @param Cache $cache
* @param Database $dbForProject
* @param Device $deviceForFunctions
@@ -79,7 +72,7 @@ class Builds extends Action
* @return void
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log): void
public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -100,7 +93,7 @@ class Builds extends Action
case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId());
$github = new GitHub($cache);
$this->buildDeployment($deviceForFunctions, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log);
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
break;
default:
@@ -110,11 +103,9 @@ class Builds extends Action
/**
* @param Device $deviceForFunctions
* @param Webhook $queueForWebhooks
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param Event $queueForEvents
* @param StatsUsage $queueForStatsUsage
* @param Usage $queueForUsage
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param GitHub $github
@@ -127,7 +118,7 @@ class Builds extends Action
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function buildDeployment(Device $deviceForFunctions, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
{
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
@@ -136,11 +127,7 @@ class Builds extends Action
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new \Exception('Function not found');
}
if ($isResourceBlocked($project, RESOURCE_TYPE_FUNCTIONS, $functionId)) {
throw new \Exception('Function blocked');
throw new \Exception('Function not found', 404);
}
$deploymentId = $deployment->getId();
@@ -148,15 +135,15 @@ class Builds extends Action
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new \Exception('Deployment not found');
throw new \Exception('Deployment not found', 404);
}
if (empty($deployment->getAttribute('entrypoint', ''))) {
throw new \Exception('Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".');
throw new \Exception('Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".', 500);
}
$version = $function->getAttribute('version', 'v2');
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specifications', APP_FUNCTION_SPECIFICATION_DEFAULT)];
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$key = $function->getAttribute('runtime');
$runtime = $runtimes[$key] ?? null;
@@ -165,7 +152,10 @@ class Builds extends Action
}
// Realtime preparation
$event = "functions.[functionId].deployments.[deploymentId].update";
$allEvents = Event::generateEvents('functions.[functionId].deployments.[deploymentId].update', [
'functionId' => $function->getId(),
'deploymentId' => $deployment->getId()
]);
$startTime = DateTime::now();
$durationStart = \microtime(true);
@@ -379,16 +369,21 @@ class Builds extends Action
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
/**
* Trigger Realtime Event
* Send realtime Event
*/
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId())
->setPayload($build->getArrayCopy())
->trigger();
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
$tmpPath = '/tmp/builds/' . $buildId;
@@ -435,34 +430,40 @@ class Builds extends Action
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForPlatform);
}
/** Trigger Webhook */
$deploymentModel = new Deployment();
$deploymentUpdate =
$queueForEvents
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->setClass(Event::WEBHOOK_CLASS_NAME)
->setProject($project)
->setEvent('functions.[functionId].deployments.[deploymentId].update')
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId())
->setPayload($deployment->getArrayCopy(array_keys($deploymentModel->getRules())));
/** Trigger Webhook */
$queueForWebhooks
->from($deploymentUpdate)
->trigger();
$deploymentUpdate->trigger();
/** Trigger Functions */
$queueForFunctions
->from($deploymentUpdate)
->trigger();
/** Trigger Realtime Event */
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId())
->setPayload($build->getArrayCopy())
->trigger();
/** Trigger Realtime */
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
$vars = [];
@@ -555,12 +556,12 @@ class Builds extends Action
$err = $error;
}
}),
Co\go(function () use ($executor, $project, $function, $deployment, &$response, &$build, $dbForProject, $event, &$err, $queueForRealtime, &$isCanceled) {
Co\go(function () use ($executor, $project, $deployment, &$response, &$build, $dbForProject, $allEvents, &$err, &$isCanceled) {
try {
$executor->getLogs(
deploymentId: $deployment->getId(),
projectId: $project->getId(),
callback: function ($logs) use (&$response, &$err, &$build, $dbForProject, $event, $project, $function, $deployment, $queueForRealtime, &$isCanceled) {
callback: function ($logs) use (&$response, &$err, &$build, $dbForProject, $allEvents, $project, &$isCanceled) {
if ($isCanceled) {
return;
}
@@ -570,7 +571,7 @@ class Builds extends Action
$build = $dbForProject->getDocument('builds', $build->getId());
if ($build->isEmpty()) {
throw new \Exception('Build not found');
throw new \Exception('Build not found', 404);
}
if ($build->getAttribute('status') === 'canceled') {
@@ -585,16 +586,21 @@ class Builds extends Action
$build = $dbForProject->updateDocument('builds', $build->getId(), $build);
/**
* Trigger Realtime Event
* Send realtime Event
*/
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId())
->setPayload($build->getArrayCopy())
->trigger();
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
}
);
@@ -682,33 +688,38 @@ class Builds extends Action
}
} finally {
/**
* Trigger Realtime Event
* Send realtime Event
*/
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId())
->setPayload($build->getArrayCopy())
->trigger();
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $build,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $build->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
/** Trigger usage queue */
if ($build->getAttribute('status') === 'ready') {
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_BUILDS_SUCCESS, 1) // per project
->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_SUCCESS), 1) // per function
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS), (int)$build->getAttribute('duration', 0) * 1000);
} elseif ($build->getAttribute('status') === 'failed') {
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_BUILDS_FAILED, 1) // per project
->addMetric(METRIC_BUILDS_COMPUTE_FAILED, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_FAILED), 1) // per function
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_FAILED), (int)$build->getAttribute('duration', 0) * 1000);
}
$queueForStatsUsage
$queueForUsage
->addMetric(METRIC_BUILDS, 1) // per project
->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0))
->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000)
+41 -28
View File
@@ -6,8 +6,7 @@ use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model\Rule;
@@ -47,14 +46,12 @@ class Certificates extends Action
->inject('dbForPlatform')
->inject('queueForMails')
->inject('queueForEvents')
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
->inject('log')
->inject('certificates')
->callback(
fn (Message $message, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Log $log, CertificatesAdapter $certificates) =>
$this->action($message, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates)
fn (Message $message, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, CertificatesAdapter $certificates) =>
$this->action($message, $dbForPlatform, $queueForMails, $queueForEvents, $queueForFunctions, $log, $certificates)
);
}
@@ -63,16 +60,14 @@ class Certificates extends Action
* @param Database $dbForPlatform
* @param Mail $queueForMails
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param Log $log
* @param CertificatesAdapter $certificates
* @return void
* @throws Throwable
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Log $log, CertificatesAdapter $certificates): void
public function action(Message $message, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, CertificatesAdapter $certificates): void
{
$payload = $message->getPayload() ?? [];
@@ -86,7 +81,7 @@ class Certificates extends Action
$log->addTag('domain', $domain->get());
$this->execute($domain, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $skipRenewCheck);
$this->execute($domain, $dbForPlatform, $queueForMails, $queueForEvents, $queueForFunctions, $log, $certificates, $skipRenewCheck);
}
/**
@@ -95,14 +90,13 @@ class Certificates extends Action
* @param Mail $queueForMails
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param CertificatesAdapter $certificates
* @param bool $skipRenewCheck
* @return void
* @throws Throwable
* @throws \Utopia\Database\Exception
*/
private function execute(Domain $domain, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Log $log, CertificatesAdapter $certificates, bool $skipRenewCheck = false): void
private function execute(Domain $domain, Database $dbForPlatform, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, CertificatesAdapter $certificates, bool $skipRenewCheck = false): void
{
/**
* 1. Read arguments and validate domain
@@ -192,7 +186,7 @@ class Certificates extends Action
$certificate->setAttribute('updated', DateTime::now());
// Save all changes we made to certificate document into database
$this->saveCertificateDocument($domain->get(), $certificate, $success, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime);
$this->saveCertificateDocument($domain->get(), $certificate, $success, $dbForPlatform, $queueForEvents, $queueForFunctions);
}
}
@@ -205,14 +199,13 @@ class Certificates extends Action
* @param Database $dbForPlatform Database connection for console
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @return void
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Conflict
* @throws Structure
*/
private function saveCertificateDocument(string $domain, Document $certificate, bool $success, Database $dbForPlatform, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime): void
private function saveCertificateDocument(string $domain, Document $certificate, bool $success, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions): void
{
// Check if update or insert required
$certificateDocument = $dbForPlatform->findOne('certificates', [Query::equal('domain', [$domain])]);
@@ -226,7 +219,7 @@ class Certificates extends Action
}
$certificateId = $certificate->getId();
$this->updateDomainDocuments($certificateId, $domain, $success, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime);
$this->updateDomainDocuments($certificateId, $domain, $success, $dbForPlatform, $queueForEvents, $queueForFunctions);
}
/**
@@ -345,7 +338,7 @@ class Certificates extends Action
*
* @return void
*/
private function updateDomainDocuments(string $certificateId, string $domain, bool $success, Database $dbForPlatform, Event $queueForEvents, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime): void
private function updateDomainDocuments(string $certificateId, string $domain, bool $success, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions): void
{
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
@@ -374,28 +367,48 @@ class Certificates extends Action
return;
}
/** Trigger Webhook */
$ruleModel = new Rule();
$queueForEvents
->setProject($project)
->setEvent('rules.[ruleId].update')
->setParam('ruleId', $rule->getId())
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())));
/** Trigger Webhook */
$queueForWebhooks
->from($queueForEvents)
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())))
->trigger();
/** Trigger Functions */
$queueForFunctions
->from($queueForEvents)
->setProject($project)
->setEvent('rules.[ruleId].update')
->setParam('ruleId', $rule->getId())
->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules())))
->trigger();
/** Trigger Realtime Events */
$queueForRealtime
->from($queueForEvents)
->setSubscribers(['console', $projectId])
->trigger();
/** Trigger realtime event */
$allEvents = Event::generateEvents('rules.[ruleId].update', [
'ruleId' => $rule->getId(),
]);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $rule,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $rule->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
Realtime::send(
projectId: $project->getId(),
payload: $rule->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
}
}
}
+96 -76
View File
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Realtime;
use Appwrite\Event\Event;
use Appwrite\Messaging\Adapter\Realtime;
use Exception;
use Utopia\CLI\Console;
use Utopia\Database\Database;
@@ -36,9 +37,8 @@ class Databases extends Action
->inject('project')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('queueForRealtime')
->inject('log')
->callback(fn (Message $message, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime, Log $log) => $this->action($message, $project, $dbForPlatform, $dbForProject, $queueForRealtime, $log));
->callback(fn (Message $message, Document $project, Database $dbForPlatform, Database $dbForProject, Log $log) => $this->action($message, $project, $dbForPlatform, $dbForProject, $log));
}
/**
@@ -46,17 +46,16 @@ class Databases extends Action
* @param Document $project
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param Realtime $queueForRealtime
* @param Log $log
* @return void
* @throws \Exception
*/
public function action(Message $message, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime, Log $log): void
public function action(Message $message, Document $project, Database $dbForPlatform, Database $dbForProject, Log $log): void
{
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
throw new Exception('Missing payload');
throw new \Exception('Missing payload');
}
$type = $payload['type'];
@@ -76,11 +75,11 @@ class Databases extends Action
match (\strval($type)) {
DATABASE_TYPE_DELETE_DATABASE => $this->deleteDatabase($database, $project, $dbForProject),
DATABASE_TYPE_DELETE_COLLECTION => $this->deleteCollection($database, $collection, $project, $dbForProject),
DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $queueForRealtime),
DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $queueForRealtime),
DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $queueForRealtime),
DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject, $queueForRealtime),
default => throw new Exception('No database operation for type: ' . \strval($type)),
DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForPlatform, $dbForProject),
default => throw new \Exception('No database operation for type: ' . \strval($type)),
};
}
@@ -91,14 +90,13 @@ class Databases extends Action
* @param Document $project
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param Realtime $queueForRealtime
* @return void
* @throws Authorization
* @throws Conflict
* @throws \Exception
* @throws \Throwable
*/
private function createAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime): void
private function createAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -108,7 +106,12 @@ class Databases extends Action
}
$projectId = $project->getId();
$event = "databases.[databaseId].collections.[collectionId].attributes.[attributeId].update";
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].update', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'attributeId' => $attribute->getId()
]);
/**
* TODO @christyjacob4 verify if this is still the case
* Fetch attribute from the database, since with Resque float values are loosing informations.
@@ -166,7 +169,7 @@ class Databases extends Action
break;
default:
if (!$dbForProject->createAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters)) {
throw new Exception('Failed to create Attribute');
throw new \Exception('Failed to create Attribute');
}
}
@@ -197,7 +200,7 @@ class Databases extends Action
throw $e;
} finally {
$this->trigger($database, $collection, $project, $event, $queueForRealtime, $attribute);
$this->trigger($database, $collection, $attribute, $project, $projectId, $events);
if (! $relatedCollection->isEmpty()) {
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId());
@@ -214,14 +217,13 @@ class Databases extends Action
* @param Document $project
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param Realtime $queueForRealtime
* @return void
* @throws Authorization
* @throws Conflict
* @throws \Exception
* @throws \Throwable
**/
private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime): void
private function deleteAttribute(Document $database, Document $collection, Document $attribute, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -231,9 +233,15 @@ class Databases extends Action
}
$projectId = $project->getId();
$event = 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete';
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'attributeId' => $attribute->getId()
]);
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$status = $attribute->getAttribute('status', '');
$type = $attribute->getAttribute('type', '');
$project = $dbForPlatform->getDocument('projects', $projectId);
$options = $attribute->getAttribute('options', []);
@@ -304,7 +312,7 @@ class Databases extends Action
throw $e;
} finally {
$this->trigger($database, $collection, $project, $event, $queueForRealtime, $attribute);
$this->trigger($database, $collection, $attribute, $project, $projectId, $events);
}
// The underlying database removes/rebuilds indexes when attribute is removed
@@ -350,7 +358,7 @@ class Databases extends Action
}
if ($exists) { // Delete the duplicate if created, else update in db
$this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject, $queueForRealtime);
$this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject);
} else {
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
@@ -373,7 +381,6 @@ class Databases extends Action
* @param Document $project
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param Realtime $queueForRealtime
* @return void
* @throws Authorization
* @throws Conflict
@@ -381,7 +388,7 @@ class Databases extends Action
* @throws DatabaseException
* @throws \Throwable
*/
private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime): void
private function createIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -391,7 +398,12 @@ class Databases extends Action
}
$projectId = $project->getId();
$event = 'databases.[databaseId].collections.[collectionId].indexes.[indexId].update';
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].update', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'indexId' => $index->getId()
]);
$collectionId = $collection->getId();
$key = $index->getAttribute('key', '');
$type = $index->getAttribute('type', '');
@@ -418,7 +430,7 @@ class Databases extends Action
throw $e;
} finally {
$this->trigger($database, $collection, $project, $event, $queueForRealtime, null, $index);
$this->trigger($database, $collection, $index, $project, $projectId, $events);
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId);
}
}
@@ -430,7 +442,6 @@ class Databases extends Action
* @param Document $project
* @param Database $dbForPlatform
* @param Database $dbForProject
* @param Realtime $queueForRealtime
* @return void
* @throws Authorization
* @throws Conflict
@@ -438,7 +449,7 @@ class Databases extends Action
* @throws DatabaseException
* @throws \Throwable
*/
private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject, Realtime $queueForRealtime): void
private function deleteIndex(Document $database, Document $collection, Document $index, Document $project, Database $dbForPlatform, Database $dbForProject): void
{
if ($collection->isEmpty()) {
throw new Exception('Missing collection');
@@ -448,7 +459,12 @@ class Databases extends Action
}
$projectId = $project->getId();
$event = 'databases.[databaseId].collections.[collectionId].indexes.[indexId].delete';
$events = Event::generateEvents('databases.[databaseId].collections.[collectionId].indexes.[indexId].delete', [
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'indexId' => $index->getId()
]);
$key = $index->getAttribute('key');
$status = $index->getAttribute('status', '');
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -474,7 +490,7 @@ class Databases extends Action
throw $e;
} finally {
$this->trigger($database, $collection, $project, $event, $queueForRealtime, null, $index);
$this->trigger($database, $collection, $index, $project, $projectId, $events);
$dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collection->getId());
}
}
@@ -516,6 +532,7 @@ class Databases extends Action
$collectionId = $collection->getId();
$collectionInternalId = $collection->getInternalId();
$databaseId = $database->getId();
$databaseInternalId = $database->getInternalId();
$dbForProject->deleteCollection('database_' . $databaseInternalId . '_collection_' . $collection->getInternalId());
@@ -551,72 +568,75 @@ class Databases extends Action
/**
* @param string $collectionId
* @param string $collection collectionID
* @param array $queries
* @param Database $database
* @param callable|null $callback
* @return void
* @throws Exception
*/
protected function deleteByGroup(string $collectionId, array $queries, Database $database, callable $callback = null): void
protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void
{
$start = \microtime(true);
$count = 0;
$chunk = 0;
$limit = 50;
$sum = $limit;
try {
$documents = $database->deleteDocuments($collectionId, $queries);
} catch (\Throwable $th) {
Console::error('Failed to delete documents for collection ' . $collectionId . ': ' . $th->getMessage());
return;
}
$executionStart = \microtime(true);
if (\is_callable($callback)) {
foreach ($documents as $document) {
$callback($document);
while ($sum === $limit) {
$chunk++;
$results = $database->find($collection, \array_merge([Query::limit($limit)], $queries));
$sum = count($results);
Console::info('Deleting chunk #' . $chunk . '. Found ' . $sum . ' documents');
foreach ($results as $document) {
if ($database->deleteDocument($document->getCollection(), $document->getId())) {
Console::success('Deleted document "' . $document->getId() . '" successfully');
if (\is_callable($callback)) {
$callback($document);
}
} else {
Console::warning('Failed to delete document: ' . $document->getId());
}
$count++;
}
}
$end = \microtime(true);
$count = \count($documents);
$executionEnd = \microtime(true);
Console::info("Deleted {$count} documents by group in " . ($end - $start) . " seconds");
Console::info("Deleted {$count} document by group in " . ($executionEnd - $executionStart) . " seconds");
}
/**
* @param Document $database
* @param Document $collection
* @param Document $project
* @param Realtime $queueForRealtime
* @param Document|null $attribute
* @param Document|null $index
* @return void
*/
protected function trigger(
Document $database,
Document $collection,
Document $attribute,
Document $project,
string $event,
Realtime $queueForRealtime,
Document|null $attribute = null,
Document|null $index = null,
string $projectId,
array $events
): void {
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('databaseId', $database->getId())
->setParam('collectionId', $collection->getId());
if ($attribute !== null && !empty($attribute)) {
$queueForRealtime
->setParam('attributeId', $attribute->getId())
->setPayload($attribute->getArrayCopy());
}
if ($index !== null && !empty($index)) {
$queueForRealtime
->setParam('indexId', $index->getId())
->setPayload($index->getArrayCopy());
}
$queueForRealtime->trigger();
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $events[0],
payload: $attribute,
project: $project,
);
Realtime::send(
projectId: 'console',
payload: $attribute->getArrayCopy(),
events: $events,
channels: $target['channels'],
roles: $target['roles'],
options: [
'projectId' => $projectId,
'databaseId' => $database->getId(),
'collectionId' => $collection->getId()
]
);
}
}
+59 -33
View File
@@ -7,7 +7,6 @@ use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Extend\Exception;
use Executor\Executor;
use Throwable;
use Utopia\Abuse\Adapters\TimeLimit\Database as AbuseDatabase;
use Utopia\Audit\Audit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
@@ -506,8 +505,7 @@ class Deletes extends Action
$projectCollectionIds = [
...\array_keys(Config::getParam('collections', [])['projects']),
Audit::COLLECTION,
AbuseDatabase::COLLECTION,
Audit::COLLECTION
];
$limit = \count($projectCollectionIds) + 25;
@@ -929,39 +927,65 @@ class Deletes extends Action
$this->deleteRuntimes($getProjectDB, $document, $project);
}
/**
* @param Document $document to be deleted
* @param Database $database to delete it from
* @param callable|null $callback to perform after document is deleted
* @return void
*/
private function deleteById(Document $document, Database $database, callable $callback = null): void
{
if ($database->deleteDocument($document->getCollection(), $document->getId())) {
Console::success('Deleted document "' . $document->getId() . '" successfully');
if (is_callable($callback)) {
$callback($document);
}
} else {
Console::error('Failed to delete document: ' . $document->getId());
}
}
/**
* @param string $collection collectionID
* @param array $queries
* @param Database $database
* @param ?callable $callback
* @param callable|null $callback
* @return void
* @throws Exception
*/
protected function deleteByGroup(
string $collection,
array $queries,
Database $database,
?callable $callback = null
): void {
$start = \microtime(true);
protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void
{
$count = 0;
$chunk = 0;
$limit = 50;
$sum = $limit;
try {
$documents = $database->deleteDocuments($collection, $queries);
} catch (\Throwable $th) {
Console::error('Failed to delete documents for collection ' . $collection . ': ' . $th->getMessage());
return;
}
$executionStart = \microtime(true);
if (\is_callable($callback)) {
foreach ($documents as $document) {
$callback($document);
while ($sum === $limit) {
$chunk++;
try {
$results = $database->find($collection, [Query::limit($limit), ...$queries]);
} catch (DatabaseException $e) {
Console::error('Failed to find documents for collection ' . $collection . ': ' . $e->getMessage());
return;
}
$sum = count($results);
Console::info('Deleting chunk #' . $chunk . '. Found ' . $sum . ' documents');
foreach ($results as $document) {
$this->deleteById($document, $database, $callback);
$count++;
}
}
$end = \microtime(true);
$count = \count($documents);
$executionEnd = \microtime(true);
Console::info("Deleted {$count} documents by group in " . ($end - $start) . " seconds");
Console::info("Deleted {$count} document by group in " . ($executionEnd - $executionStart) . " seconds");
}
/**
@@ -975,23 +999,25 @@ class Deletes extends Action
protected function listByGroup(string $collection, array $queries, Database $database, callable $callback = null): void
{
$count = 0;
$limit = 1000;
$chunk = 0;
$limit = 50;
$results = [];
$sum = $limit;
$cursor = null;
$start = \microtime(true);
$executionStart = \microtime(true);
while ($sum === $limit) {
$chunk++;
$queries = \array_merge([Query::limit($limit)], $queries);
if ($cursor !== null) {
$queries[] = Query::cursorAfter($cursor);
$mergedQueries = \array_merge([Query::limit($limit)], $queries);
if ($cursor instanceof Document) {
$mergedQueries[] = Query::cursorAfter($cursor);
}
$results = $database->find($collection, $queries);
$results = $database->find($collection, $mergedQueries);
$sum = \count($results);
$sum = count($results);
if ($sum > 0) {
$cursor = $results[$sum - 1];
@@ -1006,9 +1032,9 @@ class Deletes extends Action
}
}
$end = \microtime(true);
$executionEnd = \microtime(true);
Console::info("Listed {$count} documents by group in " . ($end - $start) . " seconds");
Console::info("Listed {$count} document by group in " . ($executionEnd - $executionStart) . " seconds");
}
/**
+41 -33
View File
@@ -5,9 +5,8 @@ namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Event\Usage;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Utopia\Response\Model\Execution;
use Exception;
use Executor\Executor;
@@ -45,17 +44,15 @@ class Functions extends Action
->inject('project')
->inject('message')
->inject('dbForProject')
->inject('queueForWebhooks')
->inject('queueForFunctions')
->inject('queueForRealtime')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForUsage')
->inject('log')
->inject('isResourceBlocked')
->callback(fn (Document $project, Message $message, Database $dbForProject, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log, callable $isResourceBlocked) => $this->action($project, $message, $dbForProject, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $queueForEvents, $queueForStatsUsage, $log, $isResourceBlocked));
->callback(fn (Document $project, Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Log $log, callable $isResourceBlocked) => $this->action($project, $message, $dbForProject, $queueForFunctions, $queueForEvents, $queueForUsage, $log, $isResourceBlocked));
}
public function action(Document $project, Message $message, Database $dbForProject, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log, callable $isResourceBlocked): void
public function action(Document $project, Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Log $log, callable $isResourceBlocked): void
{
$payload = $message->getPayload() ?? [];
@@ -139,10 +136,8 @@ class Functions extends Action
$this->execute(
log: $log,
dbForProject: $dbForProject,
queueForWebhooks: $queueForWebhooks,
queueForFunctions: $queueForFunctions,
queueForRealtime: $queueForRealtime,
queueForStatsUsage: $queueForStatsUsage,
queueForUsage: $queueForUsage,
queueForEvents: $queueForEvents,
project: $project,
function: $function,
@@ -181,10 +176,8 @@ class Functions extends Action
$this->execute(
log: $log,
dbForProject: $dbForProject,
queueForWebhooks: $queueForWebhooks,
queueForFunctions: $queueForFunctions,
queueForRealtime: $queueForRealtime,
queueForStatsUsage: $queueForStatsUsage,
queueForUsage: $queueForUsage,
queueForEvents: $queueForEvents,
project: $project,
function: $function,
@@ -205,10 +198,8 @@ class Functions extends Action
$this->execute(
log: $log,
dbForProject: $dbForProject,
queueForWebhooks: $queueForWebhooks,
queueForFunctions: $queueForFunctions,
queueForRealtime: $queueForRealtime,
queueForStatsUsage: $queueForStatsUsage,
queueForUsage: $queueForUsage,
queueForEvents: $queueForEvents,
project: $project,
function: $function,
@@ -293,8 +284,7 @@ class Functions extends Action
* @param Log $log
* @param Database $dbForProject
* @param Func $queueForFunctions
* @param Realtime $queueForRealtime
* @param StatsUsage $queueForStatsUsage
* @param Usage $queueForUsage
* @param Event $queueForEvents
* @param Document $project
* @param Document $function
@@ -317,10 +307,8 @@ class Functions extends Action
private function execute(
Log $log,
Database $dbForProject,
Webhook $queueForWebhooks,
Func $queueForFunctions,
Realtime $queueForRealtime,
StatsUsage $queueForStatsUsage,
Usage $queueForUsage,
Event $queueForEvents,
Document $project,
Document $function,
@@ -564,7 +552,7 @@ class Functions extends Action
$errorCode = $th->getCode();
} finally {
/** Trigger usage queue */
$queueForStatsUsage
$queueForUsage
->setProject($project)
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
@@ -576,20 +564,20 @@ class Functions extends Action
;
}
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
/** Trigger Webhook */
$executionModel = new Execution();
$queueForEvents
->setQueue(Event::WEBHOOK_QUEUE_NAME)
->setClass(Event::WEBHOOK_CLASS_NAME)
->setProject($project)
->setUser($user)
->setEvent('functions.[functionId].executions.[executionId].update')
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->setPayload($execution->getArrayCopy(array_keys($executionModel->getRules())));
/** Trigger Webhook */
$queueForWebhooks
->from($queueForEvents)
->setPayload($execution->getArrayCopy(array_keys($executionModel->getRules())))
->trigger();
/** Trigger Functions */
@@ -597,11 +585,31 @@ class Functions extends Action
->from($queueForEvents)
->trigger();
/** Trigger Realtime Events */
$queueForRealtime
->from($queueForEvents)
->setSubscribers(['console', $project->getId()])
->trigger();
/** Trigger realtime event */
$allEvents = Event::generateEvents('functions.[functionId].executions.[executionId].update', [
'functionId' => $function->getId(),
'executionId' => $execution->getId()
]);
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $execution,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $execution->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
Realtime::send(
projectId: $project->getId(),
payload: $execution->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles']
);
if (!empty($error)) {
throw new Exception($error, $errorCode);
+87 -161
View File
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Usage;
use Appwrite\Messaging\Status as MessageStatus;
use Swoole\Runtime;
use Utopia\CLI\Console;
@@ -22,8 +22,6 @@ use Utopia\Messaging\Adapter\Push\APNS;
use Utopia\Messaging\Adapter\Push as PushAdapter;
use Utopia\Messaging\Adapter\Push\FCM;
use Utopia\Messaging\Adapter\SMS as SMSAdapter;
use Utopia\Messaging\Adapter\SMS\Fast2SMS;
use Utopia\Messaging\Adapter\SMS\GEOSMS;
use Utopia\Messaging\Adapter\SMS\Mock;
use Utopia\Messaging\Adapter\SMS\Msg91;
use Utopia\Messaging\Adapter\SMS\Telesign;
@@ -48,8 +46,6 @@ class Messaging extends Action
{
private ?Local $localDevice = null;
private ?SMSAdapter $adapter = null;
public static function getName(): string
{
return 'messaging';
@@ -60,9 +56,6 @@ class Messaging extends Action
*/
public function __construct()
{
$this->adapter = $this->createInternalSMSAdapter();
$this
->desc('Messaging worker')
->inject('message')
@@ -70,8 +63,8 @@ class Messaging extends Action
->inject('log')
->inject('dbForProject')
->inject('deviceForFiles')
->inject('queueForStatsUsage')
->callback(fn (Message $message, Document $project, Log $log, Database $dbForProject, Device $deviceForFiles, StatsUsage $queueForStatsUsage) => $this->action($message, $project, $log, $dbForProject, $deviceForFiles, $queueForStatsUsage));
->inject('queueForUsage')
->callback(fn (Message $message, Document $project, Log $log, Database $dbForProject, Device $deviceForFiles, Usage $queueForUsage) => $this->action($message, $project, $log, $dbForProject, $deviceForFiles, $queueForUsage));
}
/**
@@ -80,7 +73,7 @@ class Messaging extends Action
* @param Log $log
* @param Database $dbForProject
* @param Device $deviceForFiles
* @param StatsUsage $queueForStatsUsage
* @param Usage $queueForUsage
* @return void
* @throws \Exception
*/
@@ -90,7 +83,7 @@ class Messaging extends Action
Log $log,
Database $dbForProject,
Device $deviceForFiles,
StatsUsage $queueForStatsUsage
Usage $queueForUsage
): void {
Runtime::setHookFlags(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP);
$payload = $message->getPayload() ?? [];
@@ -111,7 +104,7 @@ class Messaging extends Action
case MESSAGE_SEND_TYPE_EXTERNAL:
$message = $dbForProject->getDocument('messages', $payload['messageId']);
$this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $queueForStatsUsage);
$this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $queueForUsage);
break;
default:
throw new \Exception('Unknown message type: ' . $type);
@@ -123,7 +116,7 @@ class Messaging extends Action
Document $message,
Device $deviceForFiles,
Document $project,
StatsUsage $queueForStatsUsage
Usage $queueForUsage
): void {
$topicIds = $message->getAttribute('topics', []);
$targetIds = $message->getAttribute('targets', []);
@@ -229,8 +222,8 @@ class Messaging extends Action
/**
* @var array<array> $results
*/
$results = batch(\array_map(function ($providerId) use ($identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $project, $queueForStatsUsage) {
return function () use ($providerId, $identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $project, $queueForStatsUsage) {
$results = batch(\array_map(function ($providerId) use ($identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $project, $queueForUsage) {
return function () use ($providerId, $identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $project, $queueForUsage) {
if (\array_key_exists($providerId, $providers)) {
$provider = $providers[$providerId];
} else {
@@ -257,8 +250,8 @@ class Messaging extends Action
$adapter->getMaxMessagesPerRequest()
);
return batch(\array_map(function ($batch) use ($message, $provider, $adapter, $dbForProject, $deviceForFiles, $project, $queueForStatsUsage) {
return function () use ($batch, $message, $provider, $adapter, $dbForProject, $deviceForFiles, $project, $queueForStatsUsage) {
return batch(\array_map(function ($batch) use ($message, $provider, $adapter, $dbForProject, $deviceForFiles, $project, $queueForUsage) {
return function () use ($batch, $message, $provider, $adapter, $dbForProject, $deviceForFiles, $project, $queueForUsage) {
$deliveredTotal = 0;
$deliveryErrors = [];
$messageData = clone $message;
@@ -298,7 +291,7 @@ class Messaging extends Action
$deliveryErrors[] = 'Failed sending to targets with error: ' . $e->getMessage();
} finally {
$errorTotal = \count($deliveryErrors);
$queueForStatsUsage
$queueForUsage
->setProject($project)
->addMetric(METRIC_MESSAGES, ($deliveredTotal + $errorTotal))
->addMetric(METRIC_MESSAGES_SENT, $deliveredTotal)
@@ -388,38 +381,100 @@ class Messaging extends Action
private function sendInternalSMSMessage(Document $message, Document $project, array $recipients, Log $log): void
{
if ($this->adapter === null) {
Console::warning('Skipped SMS processing. SMS adapter is not set.');
return;
if (empty(System::getEnv('_APP_SMS_PROVIDER')) || empty(System::getEnv('_APP_SMS_FROM'))) {
throw new \Exception('Skipped SMS processing. Missing "_APP_SMS_PROVIDER" or "_APP_SMS_FROM" environment variables.');
}
if ($project->isEmpty()) {
throw new \Exception('Project not set in payload');
}
Console::log('Processing project: ' . $project->getId());
Console::log('Project: ' . $project->getId());
$denyList = System::getEnv('_APP_SMS_PROJECTS_DENY_LIST', '');
$denyList = explode(',', $denyList);
if (\in_array($project->getId(), $denyList)) {
Console::error('Project is in the deny list. Skipping...');
return;
}
$from = System::getEnv('_APP_SMS_FROM', '');
$sms = new SMS(
$smsDSN = new DSN(System::getEnv('_APP_SMS_PROVIDER'));
$host = $smsDSN->getHost();
$password = $smsDSN->getPassword();
$user = $smsDSN->getUser();
$log->addTag('type', $host);
$from = System::getEnv('_APP_SMS_FROM');
$provider = new Document([
'$id' => ID::unique(),
'provider' => $host,
'type' => MESSAGE_TYPE_SMS,
'name' => 'Internal SMS',
'enabled' => true,
'credentials' => match ($host) {
'twilio' => [
'accountSid' => $user,
'authToken' => $password,
// Twilio Messaging Service SIDs always start with MG
// https://www.twilio.com/docs/messaging/services
'messagingServiceSid' => \str_starts_with($from, 'MG') ? $from : null
],
'textmagic' => [
'username' => $user,
'apiKey' => $password
],
'telesign' => [
'customerId' => $user,
'apiKey' => $password
],
'msg91' => [
'senderId' => $user,
'authKey' => $password,
'templateId' => $smsDSN->getParam('templateId', $from),
],
'vonage' => [
'apiKey' => $user,
'apiSecret' => $password
],
default => null
},
'options' => match ($host) {
'twilio' => [
'from' => \str_starts_with($from, 'MG') ? null : $from
],
default => [
'from' => $from
]
}
]);
$adapter = $this->getSmsAdapter($provider);
$batches = \array_chunk(
$recipients,
$message->getAttribute('data')['content'],
$from
$adapter->getMaxMessagesPerRequest()
);
try {
$result = $this->adapter->send($sms);
} catch (\Throwable $th) {
throw new \Exception('Failed sending to targets with error: ' . $th->getMessage());
}
batch(\array_map(function ($batch) use ($message, $provider, $adapter) {
return function () use ($batch, $message, $provider, $adapter) {
$message->setAttribute('to', $batch);
$data = $this->buildSmsMessage($message, $provider);
try {
$adapter->send($data);
} catch (\Throwable $th) {
throw new \Exception('Failed sending to targets with error: ' . $th->getMessage());
}
};
}, $batches));
}
private function getSmsAdapter(Document $provider): ?SMSAdapter
{
$credentials = $provider->getAttribute('credentials');
@@ -449,12 +504,6 @@ class Messaging extends Action
$credentials['apiKey'] ?? '',
$credentials['apiSecret'] ?? ''
),
'fast2sms' => new Fast2SMS(
$credentials['apiKey'] ?? '',
$credentials['senderId'] ?? '',
$credentials['messageId'] ?? '',
$credentials['useDLT'] ?? true
),
default => null
};
}
@@ -671,127 +720,4 @@ class Messaging extends Action
return $this->localDevice;
}
private function createInternalSMSAdapter(): ?SMSAdapter
{
if (empty(System::getEnv('_APP_SMS_PROVIDER')) || empty(System::getEnv('_APP_SMS_FROM'))) {
Console::warning('Skipped SMS processing. Missing "_APP_SMS_PROVIDER" or "_APP_SMS_FROM" environment variables.');
return null;
}
$providers = System::getEnv('_APP_SMS_PROVIDER', '');
$dsns = [];
if (!empty($providers)) {
$providers = explode(',', $providers);
foreach ($providers as $provider) {
$dsns[] = new DSN($provider);
}
}
if (count($dsns) === 1) {
$provider = $this->createProviderFromDSN($dsns[0]);
$adapter = $this->getSmsAdapter($provider);
return $adapter;
}
$defaultDSN = null;
$localDSNs = [];
/** @var DSN $dsn */
foreach ($dsns as $dsn) {
if ($dsn->getParam('local', '') === 'default') {
$defaultDSN = $dsn;
} else {
$localDSNs[] = $dsn;
}
}
if ($defaultDSN === null) {
throw new \Exception('No default SMS provider found');
}
$defaultProvider = $this->createProviderFromDSN($defaultDSN);
$adapter = $this->getSmsAdapter($defaultProvider);
$geosms = new GEOSMS($adapter);
/** @var DSN $localDSN */
foreach ($localDSNs as $localDSN) {
try {
$provider = $this->createProviderFromDSN($localDSN);
$adapter = $this->getSmsAdapter($provider);
} catch (\Exception) {
Console::warning('Unable to create adapter: ' . $localDSN->getHost());
continue;
}
$callingCode = $localDSN->getParam('local', '');
if (empty($callingCode)) {
Console::warning('Unable to register adapter: ' . $localDSN->getHost() . '. Missing `local` parameter.');
continue;
}
$geosms->setLocal($callingCode, $adapter);
}
return $geosms;
}
private function createProviderFromDSN(DSN $dsn): Document
{
$host = $dsn->getHost();
$password = $dsn->getPassword();
$user = $dsn->getUser();
$from = System::getEnv('_APP_SMS_FROM');
$provider = new Document([
'$id' => ID::unique(),
'provider' => $host,
'type' => MESSAGE_TYPE_SMS,
'name' => 'Internal SMS',
'enabled' => true,
'credentials' => match ($host) {
'twilio' => [
'accountSid' => $user,
'authToken' => $password,
// Twilio Messaging Service SIDs always start with MG
// https://www.twilio.com/docs/messaging/services
'messagingServiceSid' => \str_starts_with($from, 'MG') ? $from : null
],
'textmagic' => [
'username' => $user,
'apiKey' => $password
],
'telesign' => [
'customerId' => $user,
'apiKey' => $password
],
'msg91' => [
'senderId' => $user,
'authKey' => $password,
'templateId' => $dsn->getParam('templateId', $from),
],
'vonage' => [
'apiKey' => $user,
'apiSecret' => $password
],
'fast2sms' => [
'senderId' => $user,
'apiKey' => $password,
'messageId' => $dsn->getParam('messageId'),
'useDLT' => $dsn->getParam('useDLT'),
],
default => null
},
'options' => match ($host) {
'twilio' => [
'from' => \str_starts_with($from, 'MG') ? null : $from
],
default => [
'from' => $from
]
}
]);
return $provider;
}
}
+88 -39
View File
@@ -2,8 +2,10 @@
namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Realtime;
use Appwrite\Event\Event;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Permission;
use Appwrite\Role;
use Exception;
use Utopia\CLI\Console;
use Utopia\Config\Config;
@@ -13,6 +15,7 @@ use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Migration\Destination;
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
use Utopia\Migration\Exception as MigrationException;
@@ -24,7 +27,6 @@ use Utopia\Migration\Sources\Supabase;
use Utopia\Migration\Transfer;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\System\System;
class Migrations extends Action
{
@@ -53,14 +55,13 @@ class Migrations extends Action
->inject('dbForProject')
->inject('dbForPlatform')
->inject('logError')
->inject('queueForRealtime')
->callback(fn (Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime) => $this->action($message, $project, $dbForProject, $dbForPlatform, $logError, $queueForRealtime));
->callback(fn (Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError) => $this->action($message, $project, $dbForProject, $dbForPlatform, $logError));
}
/**
* @throws Exception
*/
public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime): void
public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError): void
{
$payload = $message->getPayload() ?? [];
@@ -87,7 +88,7 @@ class Migrations extends Action
return;
}
$this->processMigration($migration, $queueForRealtime);
$this->processMigration($migration);
}
/**
@@ -155,54 +156,98 @@ class Migrations extends Action
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function updateMigrationDocument(Document $migration, Document $project, Realtime $queueForRealtime): Document
protected function updateMigrationDocument(Document $migration, Document $project): Document
{
/** Trigger Realtime Events */
$queueForRealtime
->setProject($project)
->setSubscribers(['console', $project->getId()])
->setEvent('migrations.[migrationId].update')
->setParam('migrationId', $migration->getId())
->setPayload($migration->getArrayCopy())
->trigger();
/** Trigger Realtime */
$allEvents = Event::generateEvents('migrations.[migrationId].update', [
'migrationId' => $migration->getId(),
]);
$target = Realtime::fromPayload(
event: $allEvents[0],
payload: $migration,
project: $project
);
Realtime::send(
projectId: 'console',
payload: $migration->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
);
Realtime::send(
projectId: $project->getId(),
payload: $migration->getArrayCopy(),
events: $allEvents,
channels: $target['channels'],
roles: $target['roles'],
);
return $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration);
}
/**
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Conflict
* @throws Restricted
* @throws Structure
*/
protected function removeAPIKey(Document $apiKey): void
{
$this->dbForPlatform->deleteDocument('keys', $apiKey->getId());
}
/**
* @throws Authorization
* @throws Structure
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function generateAPIKey(Document $project): string
protected function generateAPIKey(Document $project): Document
{
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400, 0);
$generatedSecret = bin2hex(\random_bytes(128));
$apiKey = $jwt->encode([
'projectId' => $project->getId(),
'disabledMetrics' => [
METRIC_DATABASES_OPERATIONS_READS,
METRIC_DATABASES_OPERATIONS_WRITES,
METRIC_NETWORK_REQUESTS,
METRIC_NETWORK_INBOUND,
METRIC_NETWORK_OUTBOUND,
$key = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getInternalId(),
'projectId' => $project->getId(),
'name' => 'Transfer API Key',
'scopes' => [
'users.read',
'users.write',
'teams.read',
'teams.write',
'databases.read',
'databases.write',
'collections.read',
'collections.write',
'documents.read',
'documents.write',
'buckets.read',
'buckets.write',
'files.read',
'files.write',
'functions.read',
'functions.write',
'databases.read',
'collections.read',
'documents.read',
],
'expire' => null,
'sdks' => [],
'accessedAt' => null,
'secret' => $generatedSecret,
]);
return API_KEY_DYNAMIC . '_' . $apiKey;
$this->dbForPlatform->createDocument('keys', $key);
$this->dbForPlatform->purgeCachedDocument('projects', $project->getId());
return $key;
}
/**
@@ -213,7 +258,7 @@ class Migrations extends Action
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function processMigration(Document $migration, Realtime $queueForRealtime): void
protected function processMigration(Document $migration): void
{
$project = $this->project;
$projectDocument = $this->dbForPlatform->getDocument('projects', $project->getId());
@@ -230,17 +275,17 @@ class Migrations extends Action
$credentials['projectId'] = $credentials['projectId'] ?? $projectDocument->getId();
$credentials['endpoint'] = $credentials['endpoint'] ?? 'http://appwrite/v1';
$credentials['apiKey'] = $credentials['apiKey'] ?? $tempAPIKey;
$credentials['apiKey'] = $credentials['apiKey'] ?? $tempAPIKey['secret'];
$migration->setAttribute('credentials', $credentials);
}
$migration->setAttribute('stage', 'processing');
$migration->setAttribute('status', 'processing');
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
$this->updateMigrationDocument($migration, $projectDocument);
$source = $this->processSource($migration);
$destination = $this->processDestination($migration, $tempAPIKey);
$destination = $this->processDestination($migration, $tempAPIKey->getAttribute('secret'));
$source->report();
@@ -251,14 +296,14 @@ class Migrations extends Action
/** Start Transfer */
$migration->setAttribute('stage', 'migrating');
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
$this->updateMigrationDocument($migration, $projectDocument);
$transfer->run(
$migration->getAttribute('resources'),
function () use ($migration, $transfer, $projectDocument, $queueForRealtime) {
function () use ($migration, $transfer, $projectDocument) {
$migration->setAttribute('resourceData', json_encode($transfer->getCache()));
$migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters()));
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
$this->updateMigrationDocument($migration, $projectDocument);
},
$migration->getAttribute('resourceId'),
$migration->getAttribute('resourceType')
@@ -295,7 +340,7 @@ class Migrations extends Action
}
$migration->setAttribute('errors', $errorMessages);
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
$this->updateMigrationDocument($migration, $projectDocument);
return;
}
@@ -336,7 +381,11 @@ class Migrations extends Action
$migration->setAttribute('errors', $errorMessages);
}
} finally {
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
if (! $tempAPIKey->isEmpty()) {
$this->removeAPIKey($tempAPIKey);
}
$this->updateMigrationDocument($migration, $projectDocument);
if ($migration->getAttribute('status', '') === 'failed') {
Console::error('Migration('.$migration->getInternalId().':'.$migration->getId().') failed, Project('.$this->project->getInternalId().':'.$this->project->getId().')');

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