diff --git a/.env b/.env index 6ffb10bc93..123b1b538c 100644 --- a/.env +++ b/.env @@ -23,7 +23,7 @@ _APP_OPENSSL_KEY_V1=your-secret-key _APP_DOMAIN=traefik _APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_SITES=sites.localhost -_APP_DOMAIN_TARGET=localhost +_APP_DOMAIN_TARGET=test.appwrite.io _APP_RULES_FORMAT=md5 _APP_REDIS_HOST=redis _APP_REDIS_PORT=6379 @@ -40,6 +40,7 @@ _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 @@ -80,8 +81,8 @@ _APP_COMPUTE_RUNTIMES_NETWORK=runtimes _APP_EXECUTOR_SECRET=your-secret-key _APP_EXECUTOR_HOST=http://exc1/v1 _APP_FUNCTIONS_RUNTIMES=php-8.0,node-18.0,python-3.9,ruby-3.1 -_APP_SITES_RUNTIMES=static-1,ssr-22,flutter-3.24 -_APP_SITES_FRAMEWORKS=sveltekit,nextjs,nuxt,astro,remix,flutter,other # TODO: Angular +_APP_SITES_RUNTIMES=static-1,node-22,flutter-3.24 +_APP_SITES_FRAMEWORKS=sveltekit,nextjs,nuxt,astro,remix,flutter,other,react,vue # TODO: Angular _APP_MAINTENANCE_INTERVAL=86400 _APP_MAINTENANCE_DELAY= _APP_MAINTENANCE_RETENTION_CACHE=2592000 @@ -89,6 +90,7 @@ _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 @@ -113,3 +115,4 @@ _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 \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 050f80b10a..0c0482ca8f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,8 +13,13 @@ on: schedule: - cron: '0 16 * * 0' +permissions: + contents: read + jobs: analyze: + permissions: + security-events: write name: Analyze runs-on: ubuntu-latest diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000000..48b77eb4f8 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,16 @@ +name: "Static code analysis" + +on: [pull_request] +jobs: + lint: + name: CodeQL + runs-on: ubuntu-latest + + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Run CodeQL + run: | + docker run --rm -v $PWD:/app composer:2.6 sh -c \ + "composer install --profile --ignore-platform-reqs && composer check" \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9fe3e024d7..f7c5fe7c5c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,9 +8,33 @@ 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 @@ -106,6 +130,72 @@ 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, + Sites, + Proxy, + 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: Wait for Open Runtimes + timeout-minutes: 3 + run: | + while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do + echo "Waiting for Executor to come online" + sleep 1 + done + + - 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: @@ -123,6 +213,7 @@ jobs: Projects, Realtime, Sites, + Proxy, Storage, Teams, Users, @@ -132,7 +223,6 @@ jobs: Migrations ] tables-mode: [ - 'Project', 'Shared V1', 'Shared V2', ] @@ -152,7 +242,15 @@ jobs: run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose up -d - sleep 60 + sleep 30 + + - name: Wait for Open Runtimes + timeout-minutes: 3 + run: | + while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do + echo "Waiting for Executor to come online" + sleep 1 + done - name: Run ${{ matrix.service }} tests with ${{ matrix.tables-mode }} table mode run: | @@ -164,14 +262,7 @@ 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 - - echo 'Sleep 1 minute, as temporary fix for v4rc executor startup (image pulling)' - sleep 60 docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ @@ -258,4 +349,4 @@ jobs: comment-id: ${{ steps.fc.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} body-path: benchmark.txt - edit-mode: replace \ No newline at end of file + edit-mode: replace diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19fd9e3d48..7746ef99af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -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. +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. ## 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 don’t 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! +If you are worried or don’t 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! ## 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 🚀 +## Submit a Pull Request :rocket: 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 ``` -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. +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. ```bash composer lint --report=diff ``` -5. Push changes to GitHub. +5. Push changes to GitHub ``` $ git push origin [name_of_your_new_branch] @@ -323,7 +323,7 @@ Adding a new dependency should have vital value for the product with minimum pos ## Introducing New Features -We would 💖 you to contribute to Appwrite, but we also want to ensure Appwrite is loyal to its vision and mission statement 🙏. +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:. For us to find the right balance, please open an issue explaining your ideas before introducing a new pull request. @@ -389,7 +389,7 @@ In file `app/controllers/shared/api.php` On the database listener, add to an exi ```php case $document->getCollection() === 'teams': - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_TEAMS, $value); // per project break; ``` @@ -401,10 +401,10 @@ In that case you need also to handle children removal using addReduce() method c ```php case $document->getCollection() === 'buckets': //buckets - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_BUCKETS, $value); // per project if ($event === Database::EVENT_DOCUMENT_DELETE) { - $queueForUsage + $queueForStatsUsage ->addReduce($document); } break; @@ -450,16 +450,16 @@ public function __construct() ->inject('dbForProject') ->inject('queueForFunctions') ->inject('queueForEvents') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('log') - ->callback(fn (Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Log $log) => $this->action($message, $dbForProject, $queueForFunctions, $queueForEvents, $queueForUsage, $log)); + ->callback(fn (Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log) => $this->action($message, $dbForProject, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $log)); } ``` and then trigger the queue with the new metric like so: ```php -$queueForUsage +$queueForStatsUsage ->addMetric(METRIC_BUILDS, 1) ->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0)) ->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000) @@ -662,7 +662,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 Appwrite’s 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. +Blogging, speaking about, or creating tutorials about one of Appwrite’s 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. ### Presenting at Meetups diff --git a/Dockerfile b/Dockerfile index 41810f5dc4..c40ea06d1c 100755 --- a/Dockerfile +++ b/Dockerfile @@ -68,6 +68,7 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/sdks && \ chmod +x /usr/local/bin/specs && \ chmod +x /usr/local/bin/ssl && \ + chmod +x /usr/local/bin/screenshot && \ chmod +x /usr/local/bin/test && \ chmod +x /usr/local/bin/upgrade && \ chmod +x /usr/local/bin/vars && \ @@ -85,6 +86,10 @@ 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 diff --git a/README-CN.md b/README-CN.md index 92a9bf9806..c024491907 100644 --- a/README-CN.md +++ b/README-CN.md @@ -1,8 +1,8 @@ -> 好消息!Appwrite 云现已进入公开测试版!立即访问 cloud.appwrite.io 注册,体验无忧的托管服务。今天就加入我们的云端吧!☁️🎉 +> 好消息!Appwrite 云现已进入公开测试版!立即访问 cloud.appwrite.io 注册,体验无忧的托管服务。今天就加入我们的云端吧!:cloud: :tada:

- Appwrite Logo + Appwrite banner with logo and slogan build like a team of hundreds

适用于[Flutter/Vue/Angular/React/iOS/Android/* 等等平台 *]的完整后端服务 @@ -36,6 +36,8 @@ Appwrite 可以提供给开发者用户验证,外部授权,用户数据读 内容: + +- [开始](#开始) - [安装](#安装) - [Unix](#unix) - [Windows](#windows) @@ -54,6 +56,9 @@ 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。 @@ -98,7 +103,42 @@ docker run -it --rm ` ### 从旧版本升级 -如果您从旧版本升级 Appwrite 服务器,则应在设置完成后使用 Appwrite 迁移工具。有关这方面的更多信息,请查看 [安装文档](https://appwrite.io/docs/installation)。 +如果您从旧版本升级 Appwrite 服务器,则应在设置完成后使用 Appwrite 迁移工具。有关这方面的更多信息,请查看 [安装文档](https://appwrite.io/docs/self-hosting)。 + +## 一键配置 + +除了在本地运行 Appwrite,您还可以使用预配置的设置启动 Appwrite。这样可以让您快速启动并运行 Appwrite,而无需在本地计算机上安装 Docker。 + +请从以下提供商中选择一个: + + + + + + + + +
+ + DigitalOcean Logo +
DigitalOcean
+ +
+ + Gitpod Logo +
Gitpod
+ +
+ + Akamai Logo +
Akamai Compute
+ +
+ + AWS Logo +
AWS Marketplace
+ +
## 入门 @@ -146,29 +186,25 @@ docker run -it --rm ` 以下是当前支持的平台和语言列表。如果您想帮助我们为您选择的平台添加支持,您可以访问我们的 [SDK 生成器](https://github.com/appwrite/sdk-generator) 项目并查看我们的 [贡献指南](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md)。 #### 客户端 - -- ✅   [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护) -- ✅   [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护) -- ✅   [Apple](https://github.com/appwrite/sdk-for-apple) (由 Appwrite 团队维护) -- ✅   [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护) -- ✅   [React Native](https://github.com/appwrite/sdk-for-react-native) - **公测** (由 Appwrite 团队维护) +* :white_check_mark:   [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护) +* :white_check_mark:   [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护) +* :white_check_mark:   [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护) +* :white_check_mark:   [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护) #### 服务器 - -- ✅   [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护) -- ✅   [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwrite 团队维护) -- ✅   [Dart](https://github.com/appwrite/sdk-for-dart) (由 Appwrite 团队维护) -- ✅   [Deno](https://github.com/appwrite/sdk-for-deno) (由 Appwrite 团队维护) -- ✅   [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护) -- ✅   [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护) -- ✅   [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (由 Appwrite 团队维护) -- ✅   [Swift](https://github.com/appwrite/sdk-for-swift) (由 Appwrite 团队维护) -- ✅   [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护) +* :white_check_mark:   [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护) +* :white_check_mark:   [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwrite 团队维护) +* :white_check_mark:   [Dart](https://github.com/appwrite/sdk-for-dart) - (由 Appwrite 团队维护) +* :white_check_mark:   [Deno](https://github.com/appwrite/sdk-for-deno) - **公测** (由 Appwrite 团队维护) +* :white_check_mark:   [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护) +* :white_check_mark:   [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护) +* :white_check_mark:   [Kotlin](https://github.com/appwrite/sdk-for-kotlin) - **公测** (由 Appwrite 团队维护) +* :white_check_mark:   [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护) +* :white_check_mark:   [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护) #### 开发者社区 - -- ✅   [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/)) -- ✅   [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (维护者 [fenix-hub @GodotNuts](https://github.com/fenix-hub)) +* :white_check_mark:   [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/)) +* :white_check_mark:   [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)! diff --git a/README.md b/README.md index ab57e65c4c..5bccf4739f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -> Appwrite Init has concluded! You can check out all the latest announcements [on our Init website](https://appwrite.io/init) 🚀 +> Appwrite Init has concluded! You can check out all the latest announcements [on our Init website](https://appwrite.io/init) :rocket:

- Appwrite Logo + Appwrite banner, with logo and text saying

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. @@ -12,11 +12,11 @@ -[![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) +[![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) @@ -37,13 +37,14 @@ Using Appwrite, you can easily integrate your app with user authentication and m

-![Appwrite](public/images/github.png) +![Appwrite project dashboard showing various Appwrite features](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: -- [Installation](#installation) +- [Getting Started](#getting-started) +- [Self-Hosting](#self-hosting) - [Unix](#unix) - [Windows](#windows) - [CMD](#cmd) @@ -62,11 +63,14 @@ Table of Contents: - [Follow Us](#follow-us) - [License](#license) -## Installation +## 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 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/). -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: +Before running the installation command, make sure you have [Docker](https://www.docker.com/products/docker-desktop) installed on your machine: ### Unix @@ -106,7 +110,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/installation). +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). ## One-Click Setups @@ -192,34 +196,34 @@ Below is a list of currently supported platforms and languages. If you would lik #### Client -- ✅   [Web](https://github.com/appwrite/sdk-for-web) (Maintained by the Appwrite Team) -- ✅   [Flutter](https://github.com/appwrite/sdk-for-flutter) (Maintained by the Appwrite Team) -- ✅   [Apple](https://github.com/appwrite/sdk-for-apple) (Maintained by the Appwrite Team) -- ✅   [Android](https://github.com/appwrite/sdk-for-android) (Maintained by the Appwrite Team) -- ✅   [React Native](https://github.com/appwrite/sdk-for-react-native) - **Beta** (Maintained by the Appwrite Team) +- :white_check_mark:   [Web](https://github.com/appwrite/sdk-for-web) (Maintained by the Appwrite Team) +- :white_check_mark:   [Flutter](https://github.com/appwrite/sdk-for-flutter) (Maintained by the Appwrite Team) +- :white_check_mark:   [Apple](https://github.com/appwrite/sdk-for-apple) (Maintained by the Appwrite Team) +- :white_check_mark:   [Android](https://github.com/appwrite/sdk-for-android) (Maintained by the Appwrite Team) +- :white_check_mark:   [React Native](https://github.com/appwrite/sdk-for-react-native) - **Beta** (Maintained by the Appwrite Team) #### Server -- ✅   [NodeJS](https://github.com/appwrite/sdk-for-node) (Maintained by the Appwrite Team) -- ✅   [PHP](https://github.com/appwrite/sdk-for-php) (Maintained by the Appwrite Team) -- ✅   [Dart](https://github.com/appwrite/sdk-for-dart) (Maintained by the Appwrite Team) -- ✅   [Deno](https://github.com/appwrite/sdk-for-deno) (Maintained by the Appwrite Team) -- ✅   [Ruby](https://github.com/appwrite/sdk-for-ruby) (Maintained by the Appwrite Team) -- ✅   [Python](https://github.com/appwrite/sdk-for-python) (Maintained by the Appwrite Team) -- ✅   [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team) -- ✅   [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team) -- ✅   [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team) +- :white_check_mark:   [NodeJS](https://github.com/appwrite/sdk-for-node) (Maintained by the Appwrite Team) +- :white_check_mark:   [PHP](https://github.com/appwrite/sdk-for-php) (Maintained by the Appwrite Team) +- :white_check_mark:   [Dart](https://github.com/appwrite/sdk-for-dart) (Maintained by the Appwrite Team) +- :white_check_mark:   [Deno](https://github.com/appwrite/sdk-for-deno) (Maintained by the Appwrite Team) +- :white_check_mark:   [Ruby](https://github.com/appwrite/sdk-for-ruby) (Maintained by the Appwrite Team) +- :white_check_mark:   [Python](https://github.com/appwrite/sdk-for-python) (Maintained by the Appwrite Team) +- :white_check_mark:   [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team) +- :white_check_mark:   [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team) +- :white_check_mark:   [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team) #### Community -- ✅   [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (Maintained by [Michael Gangolf](https://github.com/m1ga/)) -- ✅   [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (Maintained by [fenix-hub @GodotNuts](https://github.com/fenix-hub)) +- :white_check_mark:   [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (Maintained by [Michael Gangolf](https://github.com/m1ga/)) +- :white_check_mark:   [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](docs/specs/overview.drawio.svg) +![Appwrite Architecture showing how Appwrite is built and the services and tools it uses](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. @@ -229,7 +233,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 ❤️ 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 :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). ## Security diff --git a/app/cli.php b/app/cli.php index 22f09c7c23..360b5478c5 100644 --- a/app/cli.php +++ b/app/cli.php @@ -5,6 +5,8 @@ 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; @@ -160,6 +162,45 @@ 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(); }, ['pools']); diff --git a/app/config/avatars/credit-cards.php b/app/config/avatars/credit-cards.php index eb76c576cf..52760bf9dc 100644 --- a/app/config/avatars/credit-cards.php +++ b/app/config/avatars/credit-cards.php @@ -16,5 +16,6 @@ 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'] + 'maestro' => ['name' => 'Maestro', 'path' => __DIR__ . '/credit-cards/maestro.png'], + 'rupay' => ['name' => 'Rupay', 'path' => __DIR__ . '/credit-cards/rupay.png'] ]; diff --git a/app/config/avatars/credit-cards/rupay.png b/app/config/avatars/credit-cards/rupay.png new file mode 100644 index 0000000000..dfb95e9844 Binary files /dev/null and b/app/config/avatars/credit-cards/rupay.png differ diff --git a/app/config/collections.php b/app/config/collections.php index 8c8356aafd..533dee57a8 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -5,6 +5,7 @@ $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']; @@ -27,6 +28,7 @@ $collections = [ 'databases' => $databases, 'projects' => array_merge($projects, $common), 'console' => array_merge($platform, $common), + 'logs' => $logs, ]; return $collections; diff --git a/app/config/collections/logs.php b/app/config/collections/logs.php new file mode 100644 index 0000000000..069dcf5a4b --- /dev/null +++ b/app/config/collections/logs.php @@ -0,0 +1,94 @@ + 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; diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index 8a46bfd3ec..58867bf2ba 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -1013,10 +1013,10 @@ return [ 'filters' => [], ], [ - '$id' => ID::custom('resourceType'), + '$id' => ID::custom('type'), // 'api', 'redirect', 'deployment' (site or function) 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 100, + 'size' => 32, 'signed' => true, 'required' => true, 'default' => null, @@ -1024,24 +1024,28 @@ return [ 'filters' => [], ], [ - '$id' => ID::custom('resourceInternalId'), + // If 'api', then (empty) + // If 'redirect', then URL + // If 'deployment', then deployment ID + '$id' => ID::custom('value'), 'type' => Database::VAR_STRING, 'format' => '', - 'size' => Database::LENGTH_KEY, + 'size' => 512, 'signed' => true, 'required' => false, - 'default' => null, + 'default' => '', 'array' => false, 'filters' => [], ], [ - '$id' => ID::custom('resourceId'), + // Examples: branch=main + '$id' => ID::custom('automation'), 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, - 'default' => null, + 'default' => '', 'array' => false, 'filters' => [], ], @@ -1066,9 +1070,27 @@ return [ 'default' => null, 'array' => false, 'filters' => [], - ] + ], + [ + '$id' => ID::custom('search'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ + [ + '$id' => ID::custom('_key_search'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['search'], + 'lengths' => [], + 'orders' => [], + ], [ '$id' => ID::custom('_key_domain'), 'type' => Database::INDEX_UNIQUE, @@ -1091,24 +1113,24 @@ return [ 'orders' => [Database::ORDER_ASC], ], [ - '$id' => '_key_resourceInternalId', + '$id' => '_key_type', 'type' => Database::INDEX_KEY, - 'attributes' => ['resourceInternalId'], - 'lengths' => [Database::LENGTH_KEY], + 'attributes' => ['type'], + 'lengths' => [32], 'orders' => [Database::ORDER_ASC], ], [ - '$id' => '_key_resourceId', + '$id' => '_key_value', 'type' => Database::INDEX_KEY, - 'attributes' => ['resourceId'], - 'lengths' => [Database::LENGTH_KEY], + 'attributes' => ['value'], + 'lengths' => [512], 'orders' => [Database::ORDER_ASC], ], [ - '$id' => '_key_resourceType', + '$id' => '_key_automation', 'type' => Database::INDEX_KEY, - 'attributes' => ['resourceType'], - 'lengths' => [], + 'attributes' => ['automation'], + 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], ], diff --git a/app/config/collections/projects.php b/app/config/collections/projects.php index b31681fd64..597f02f7d6 100644 --- a/app/config/collections/projects.php +++ b/app/config/collections/projects.php @@ -1528,7 +1528,29 @@ return [ 'default' => false, 'array' => false, 'filters' => [], - ] + ], + [ + '$id' => ID::custom('screenshotLight'), // File ID from 'screenshots' Console bucket + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 32, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('screenshotDark'), // File ID from 'screenshots' Console bucket + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 32, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/app/config/errors.php b/app/config/errors.php index 4a36cee152..e41f663168 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -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 => [ @@ -375,6 +375,13 @@ return [ 'code' => 409, ], + /** Console */ + Exception::RESOURCE_ALREADY_EXISTS => [ + 'name' => Exception::RESOURCE_ALREADY_EXISTS, + 'description' => 'Resource with the requested ID already exists. Please choose a different ID and try again.', + 'code' => 409, + ], + /** Membership */ Exception::MEMBERSHIP_NOT_FOUND => [ 'name' => Exception::MEMBERSHIP_NOT_FOUND, @@ -868,6 +875,11 @@ return [ 'description' => 'Variable with the same ID already exists in this project. Try again with a different ID.', 'code' => 409, ], + Exception::VARIABLE_CANNOT_UNSET_SECRET => [ + 'name' => Exception::VARIABLE_CANNOT_UNSET_SECRET, + 'description' => 'Secret variables cannot be marked as non-secret. Please re-create the variable if this is your intention.', + 'code' => 400, + ], Exception::GRAPHQL_NO_QUERY => [ 'name' => Exception::GRAPHQL_NO_QUERY, 'description' => 'Param "query" is not optional.', diff --git a/app/config/frameworks.php b/app/config/frameworks.php index d19262ff57..332035a79c 100644 --- a/app/config/frameworks.php +++ b/app/config/frameworks.php @@ -21,7 +21,7 @@ return [ 'nextjs' => [ 'key' => 'nextjs', 'name' => 'Next.js', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'), 'adapters' => [ 'ssr' => [ @@ -44,10 +44,28 @@ return [ ] ] ], + 'react' => [ + 'key' => 'react', + 'name' => 'React', + 'buildRuntime' => 'node-22', + 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'), + 'adapters' => [ + 'static' => [ + 'key' => 'static', + 'buildCommand' => 'npm run build', + 'installCommand' => 'npm install', + 'outputDirectory' => './dist', + 'startCommand' => 'sh helpers/server.sh', + 'bundleCommand' => '', + 'envCommand' => '', + 'fallbackFile' => 'index.html' + ] + ] + ], 'nuxt' => [ 'key' => 'nuxt', 'name' => 'Nuxt', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'), 'adapters' => [ 'ssr' => [ @@ -70,10 +88,28 @@ return [ ] ] ], + 'vue' => [ + 'key' => 'vue', + 'name' => 'Vue.js', + 'buildRuntime' => 'node-22', + 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'), + 'adapters' => [ + 'static' => [ + 'key' => 'static', + 'buildCommand' => 'npm run build', + 'installCommand' => 'npm install', + 'outputDirectory' => './dist', + 'startCommand' => 'sh helpers/server.sh', + 'bundleCommand' => '', + 'envCommand' => '', + 'fallbackFile' => 'index.html' + ] + ] + ], 'sveltekit' => [ 'key' => 'sveltekit', 'name' => 'SvelteKit', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'), 'adapters' => [ 'ssr' => [ @@ -99,7 +135,7 @@ return [ 'astro' => [ 'key' => 'astro', 'name' => 'Astro', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'), 'adapters' => [ 'ssr' => [ @@ -125,7 +161,7 @@ return [ 'remix' => [ 'key' => 'remix', 'name' => 'Remix', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'), 'adapters' => [ 'ssr' => [ @@ -168,7 +204,7 @@ return [ 'other' => [ 'key' => 'other', 'name' => 'Other', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'runtimes' => getVersions($templateRuntimes['NODE']['versions'], 'node'), 'adapters' => [ 'static' => [ diff --git a/app/config/roles.php b/app/config/roles.php index 8bc25cfba2..a4abee0c45 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -26,6 +26,7 @@ $member = [ 'subscribers.write', 'subscribers.read', 'assistant.read', + 'rules.read' ]; $admins = [ diff --git a/app/config/services.php b/app/config/services.php index 32eac3d324..43e80387f1 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -191,7 +191,7 @@ return [ 'name' => 'Functions', 'subtitle' => 'The Functions Service allows you view, create and manage your Cloud Functions.', 'description' => '/docs/services/functions.md', - 'controller' => 'api/functions.php', + 'controller' => '', // Uses modules 'sdk' => true, 'docs' => true, 'docsUrl' => 'https://appwrite.io/docs/functions', diff --git a/app/config/site-templates.php b/app/config/site-templates.php index 64696cedfa..9e0b36053e 100644 --- a/app/config/site-templates.php +++ b/app/config/site-templates.php @@ -1,9 +1,21 @@ 'npm install', 'buildCommand' => 'npm run build', 'outputDirectory' => './build', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'adapter' => 'ssr', 'fallbackFile' => null, ], @@ -23,7 +35,7 @@ const TEMPLATE_FRAMEWORKS = [ 'installCommand' => 'npm install', 'buildCommand' => 'npm run build', 'outputDirectory' => './.next', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'adapter' => 'ssr', 'fallbackFile' => null, ], @@ -33,7 +45,7 @@ const TEMPLATE_FRAMEWORKS = [ 'installCommand' => 'npm install', 'buildCommand' => 'npm run build', 'outputDirectory' => './.output', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'adapter' => 'ssr', 'fallbackFile' => null, ], @@ -43,7 +55,7 @@ const TEMPLATE_FRAMEWORKS = [ 'installCommand' => 'npm install', 'buildCommand' => 'npm run build', 'outputDirectory' => './build', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'adapter' => 'ssr', 'fallbackFile' => null, ], @@ -53,7 +65,7 @@ const TEMPLATE_FRAMEWORKS = [ 'installCommand' => 'npm install', 'buildCommand' => 'npm run build', 'outputDirectory' => './dist', - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'adapter' => 'ssr', 'fallbackFile' => null, ], @@ -67,6 +79,35 @@ const TEMPLATE_FRAMEWORKS = [ 'adapter' => 'static', 'fallbackFile' => null, ], + 'OTHER' => [ + 'key' => 'other', + 'name' => 'Other', + 'installCommand' => 'npm install', + 'buildCommand' => 'npm run build', + 'buildRuntime' => 'node-22', + 'adapter' => 'static', + 'fallbackFile' => 'index.html', + ], + 'REACT' => [ + 'key' => 'react', + 'name' => 'React', + 'installCommand' => 'npm install', + 'buildCommand' => 'npm run build', + 'buildRuntime' => 'node-22', + 'adapter' => 'static', + 'outputDirectory' => './dist', + 'fallbackFile' => 'index.html', + ], + 'VUE' => [ + 'key' => 'vue', + 'name' => 'Vue.js', + 'installCommand' => 'npm install', + 'buildCommand' => 'npm run build', + 'buildRuntime' => 'node-22', + 'adapter' => 'static', + 'outputDirectory' => './dist', + 'fallbackFile' => 'index.html', + ], ]; function getFramework(string $frameworkEnum, array $overrides) @@ -76,12 +117,448 @@ function getFramework(string $frameworkEnum, array $overrides) } return [ + [ + 'key' => 'template-for-onelink', + 'name' => 'Onelink template', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/template-for-onelink-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-onelink-light.png', + 'frameworks' => [ + getFramework('NUXT', [ + 'providerRootDirectory' => './onelink', + 'buildCommand' => 'npm run generate', + 'outputDirectory' => './dist', + 'adapter' => 'static', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'Meldiron', + 'providerVersion' => '0.1.*', + 'variables' => [] + ], + [ + 'key' => 'starter-for-svelte', + 'name' => 'Svelte starter', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/starter-for-svelte-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-svelte-light.png', + 'frameworks' => [ + getFramework('SVELTEKIT', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'starter-for-svelte', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'PUBLIC_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'PUBLIC_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'PUBLIC_APPWRITE_PROJECT_NAME', + 'description' => 'Your Appwrite project name', + 'value' => '{projectName}', + 'placeholder' => '{projectName}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'starter-for-react', + 'name' => 'React starter', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/starter-for-react-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-react-light.png', + 'frameworks' => [ + getFramework('REACT', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'starter-for-react', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'VITE_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'VITE_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'VITE_APPWRITE_PROJECT_NAME', + 'description' => 'Your Appwrite project name', + 'value' => '{projectName}', + 'placeholder' => '{projectName}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'starter-for-vue', + 'name' => 'Vue starter', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/starter-for-vue-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-vue-light.png', + 'frameworks' => [ + getFramework('VUE', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'starter-for-vue', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'VITE_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'VITE_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'VITE_APPWRITE_PROJECT_NAME', + 'description' => 'Your Appwrite project name', + 'value' => '{projectName}', + 'placeholder' => '{projectName}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'starter-for-react-native', + 'name' => 'React Native starter', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/starter-for-react-native-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-react-native-light.png', + 'frameworks' => [ + getFramework('REACT', [ + 'providerRootDirectory' => './', + 'fallbackFile' => '+not-found.html', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'starter-for-react-native', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'EXPO_PUBLIC_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'EXPO_PUBLIC_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'EXPO_PUBLIC_APPWRITE_PROJECT_NAME', + 'description' => 'Your Appwrite project name', + 'value' => '{projectName}', + 'placeholder' => '{projectName}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'starter-for-nextjs', + 'name' => 'Next.js starter', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/starter-for-nextjs-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-nextjs-light.png', + 'frameworks' => [ + getFramework('NEXTJS', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'starter-for-nextjs', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'NEXT_PUBLIC_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'NEXT_PUBLIC_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'NEXT_PUBLIC_APPWRITE_PROJECT_NAME', + 'description' => 'Your Appwrite project name', + 'value' => '{projectName}', + 'placeholder' => '{projectName}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'starter-for-nuxt', + 'name' => 'Nuxt starter', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/starter-for-nuxt-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-nuxt-light.png', + 'frameworks' => [ + getFramework('NUXT', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'starter-for-nuxt', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'NUXT_PUBLIC_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'NUXT_PUBLIC_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'NUXT_PUBLIC_APPWRITE_PROJECT_NAME', + 'description' => 'Your Appwrite project name', + 'value' => '{projectName}', + 'placeholder' => '{projectName}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'template-for-event', + 'name' => 'Event template', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/template-for-event-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-event-light.png', + 'frameworks' => [ + getFramework('NEXTJS', [ + 'providerRootDirectory' => './', + 'installCommand' => 'pnpm install', + 'buildCommand' => 'npm run build', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'template-for-event', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'NEXT_PUBLIC_APPWRITE_FUNCTION_PROJECT_ID', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'NEXT_PUBLIC_APPWRITE_FUNCTION_API_ENDPOINT', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'template-for-portfolio', + 'name' => 'Portfolio template', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/template-for-portfolio-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-portfolio-light.png', + 'frameworks' => [ + getFramework('NEXTJS', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'template-for-portfolio', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [] + ], + [ + 'key' => 'template-for-store', + 'name' => 'Store template', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/template-for-store-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-store-light.png', + 'frameworks' => [ + getFramework('SVELTEKIT', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'template-for-store', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [ + [ + 'name' => 'STRIPE_SECRET_KEY', + 'description' => 'Your Stripe secret key', + 'value' => 'disabled', + 'placeholder' => 'sk_.....', + 'required' => false, + 'type' => 'password' + ], + [ + 'name' => 'PUBLIC_APPWRITE_ENDPOINT', + 'description' => 'Endpoint of Appwrite server', + 'value' => '{apiEndpoint}', + 'placeholder' => '{apiEndpoint}', + 'required' => true, + 'type' => 'text' + ], + [ + 'name' => 'PUBLIC_APPWRITE_PROJECT_ID', + 'description' => 'Your Appwrite project ID', + 'value' => '{projectId}', + 'placeholder' => '{projectId}', + 'required' => true, + 'type' => 'text' + ], + ] + ], + [ + 'key' => 'template-for-blog', + 'name' => 'Blog template', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/template-for-blog-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-blog-light.png', + 'frameworks' => [ + getFramework('SVELTEKIT', [ + 'providerRootDirectory' => './', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'template-for-blog', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.1.*', + 'variables' => [] + ], + [ + 'key' => 'astro-starter', + 'name' => 'Astro starter', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/astro-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/astro-starter-light.png', + 'frameworks' => [ + getFramework('ASTRO', [ + 'providerRootDirectory' => './astro/starter', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.2.*', + 'variables' => [], + ], + [ + 'key' => 'remix-starter', + 'name' => 'Remix starter', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/remix-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/remix-starter-light.png', + 'frameworks' => [ + getFramework('REMIX', [ + 'providerRootDirectory' => './remix/starter', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.2.*', + 'variables' => [], + ], + [ + 'key' => 'flutter-starter', + 'name' => 'Flutter starter', + 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/flutter-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/flutter-starter-light.png', + 'frameworks' => [ + getFramework('FLUTTER', [ + 'providerRootDirectory' => './flutter/starter', + ]), + ], + 'vcsProvider' => 'github', + 'providerRepositoryId' => 'templates-for-sites', + 'providerOwner' => 'appwrite', + 'providerVersion' => '0.2.*', + 'variables' => [], + ], [ 'key' => 'nextjs-starter', 'name' => 'Next.js starter website', 'useCases' => ['starter'], - 'demoUrl' => 'https://nextjs-starter.sites.qa17.appwrite.org/', - 'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/nextjs-starter.png', + 'screenshotDark' => $url . '/images/sites/templates/nextjs-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/nextjs-starter-light.png', 'frameworks' => [ getFramework('NEXTJS', [ 'providerRootDirectory' => './nextjs/starter', @@ -97,8 +574,8 @@ return [ 'key' => 'nuxt-starter', 'name' => 'Nuxt starter website', 'useCases' => ['starter'], - 'demoUrl' => 'https://nuxt-starter.sites.qa17.appwrite.org/', - 'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/nuxt-starter.png', + 'screenshotDark' => $url . '/images/sites/templates/nuxt-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/nuxt-starter-light.png', 'frameworks' => [ getFramework('NUXT', [ 'providerRootDirectory' => './nuxt/starter', @@ -114,8 +591,8 @@ return [ 'key' => 'sveltekit-starter', 'name' => 'SvelteKit starter website', 'useCases' => ['starter'], - 'demoUrl' => 'https://sveltekit-starter.sites.qa17.appwrite.org/', - 'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/sveltekit-starter.png', + 'screenshotDark' => $url . '/images/sites/templates/sveltekit-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/sveltekit-starter-light.png', 'frameworks' => [ getFramework('SVELTEKIT', [ 'providerRootDirectory' => './sveltekit/starter', @@ -127,55 +604,4 @@ return [ 'providerVersion' => '0.2.*', 'variables' => [], ], - [ - 'key' => 'astro-starter', - 'name' => 'Astro starter website', - 'useCases' => ['starter'], - 'demoUrl' => 'https://astro-starter.sites.qa17.appwrite.org/', - 'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/astro-starter.png', - 'frameworks' => [ - getFramework('ASTRO', [ - 'providerRootDirectory' => './astro/starter', - ]), - ], - 'vcsProvider' => 'github', - 'providerRepositoryId' => 'templates-for-sites', - 'providerOwner' => 'appwrite', - 'providerVersion' => '0.2.*', - 'variables' => [], - ], - [ - 'key' => 'remix-starter', - 'name' => 'Remix starter website', - 'useCases' => ['starter'], - 'demoUrl' => 'https://remix-starter.sites.qa17.appwrite.org/', - 'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/remix-starter.png', - 'frameworks' => [ - getFramework('REMIX', [ - 'providerRootDirectory' => './remix/starter', - ]), - ], - 'vcsProvider' => 'github', - 'providerRepositoryId' => 'templates-for-sites', - 'providerOwner' => 'appwrite', - 'providerVersion' => '0.2.*', - 'variables' => [], - ], - [ - 'key' => 'flutter-starter', - 'name' => 'Flutter starter website', - 'useCases' => ['starter'], - 'demoUrl' => 'https://flutter-starter.sites.qa17.appwrite.org/', - 'demoImage' => 'https://qa17.appwrite.org/console/images/sites/templates/flutter-starter.png', - 'frameworks' => [ - getFramework('FLUTTER', [ - 'providerRootDirectory' => './flutter/starter', - ]), - ], - 'vcsProvider' => 'github', - 'providerRepositoryId' => 'templates-for-sites', - 'providerOwner' => 'appwrite', - 'providerVersion' => '0.2.*', - 'variables' => [], - ], ]; diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index a626c47bd0..05b38d5cbf 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -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.", + "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.", "required": true, "schema": { "type": "string", @@ -3404,7 +3404,8 @@ "union-china-pay", "visa", "mir", - "maestro" + "maestro", + "rupay" ], "x-enum-name": "CreditCard", "x-enum-keys": [ @@ -3423,7 +3424,8 @@ "Union China Pay", "Visa", "MIR", - "Maestro" + "Maestro", + "Rupay" ] }, "in": "path" @@ -4365,7 +4367,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.", + "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", "responses": { "201": { "description": "Document", @@ -4761,7 +4763,7 @@ }, "x-appwrite": { "method": "listExecutions", - "weight": 300, + "weight": 301, "cookies": false, "type": "", "deprecated": false, @@ -4846,7 +4848,7 @@ }, "x-appwrite": { "method": "createExecution", - "weight": 299, + "weight": 300, "cookies": false, "type": "", "deprecated": false, @@ -4960,7 +4962,7 @@ }, "x-appwrite": { "method": "getExecution", - "weight": 301, + "weight": 302, "cookies": false, "type": "", "deprecated": false, @@ -5033,7 +5035,7 @@ }, "x-appwrite": { "method": "query", - "weight": 326, + "weight": 325, "cookies": false, "type": "graphql", "deprecated": false, @@ -5084,7 +5086,7 @@ }, "x-appwrite": { "method": "mutation", - "weight": 325, + "weight": 324, "cookies": false, "type": "graphql", "deprecated": false, @@ -5543,7 +5545,7 @@ }, "x-appwrite": { "method": "createSubscriber", - "weight": 371, + "weight": 370, "cookies": false, "type": "", "deprecated": false, @@ -5625,7 +5627,7 @@ }, "x-appwrite": { "method": "deleteSubscriber", - "weight": 375, + "weight": 374, "cookies": false, "type": "", "deprecated": false, @@ -5699,7 +5701,7 @@ }, "x-appwrite": { "method": "listFiles", - "weight": 207, + "weight": 208, "cookies": false, "type": "", "deprecated": false, @@ -5784,7 +5786,7 @@ }, "x-appwrite": { "method": "createFile", - "weight": 206, + "weight": 207, "cookies": false, "type": "upload", "deprecated": false, @@ -5881,7 +5883,7 @@ }, "x-appwrite": { "method": "getFile", - "weight": 208, + "weight": 209, "cookies": false, "type": "", "deprecated": false, @@ -5952,7 +5954,7 @@ }, "x-appwrite": { "method": "updateFile", - "weight": 213, + "weight": 214, "cookies": false, "type": "", "deprecated": false, @@ -6040,7 +6042,7 @@ }, "x-appwrite": { "method": "deleteFile", - "weight": 214, + "weight": 215, "cookies": false, "type": "", "deprecated": false, @@ -6106,7 +6108,7 @@ }, "x-appwrite": { "method": "getFileDownload", - "weight": 210, + "weight": 211, "cookies": false, "type": "location", "deprecated": false, @@ -6172,7 +6174,7 @@ }, "x-appwrite": { "method": "getFilePreview", - "weight": 209, + "weight": 210, "cookies": false, "type": "location", "deprecated": false, @@ -6388,7 +6390,7 @@ }, "x-appwrite": { "method": "getFileView", - "weight": 211, + "weight": 212, "cookies": false, "type": "location", "deprecated": false, @@ -6461,7 +6463,7 @@ }, "x-appwrite": { "method": "list", - "weight": 218, + "weight": 219, "cookies": false, "type": "", "deprecated": false, @@ -6536,7 +6538,7 @@ }, "x-appwrite": { "method": "create", - "weight": 217, + "weight": 218, "cookies": false, "type": "", "deprecated": false, @@ -6620,7 +6622,7 @@ }, "x-appwrite": { "method": "get", - "weight": 219, + "weight": 220, "cookies": false, "type": "", "deprecated": false, @@ -6681,7 +6683,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 221, + "weight": 222, "cookies": false, "type": "", "deprecated": false, @@ -6754,7 +6756,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 223, + "weight": 224, "cookies": false, "type": "", "deprecated": false, @@ -6817,7 +6819,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 225, + "weight": 226, "cookies": false, "type": "", "deprecated": false, @@ -6902,7 +6904,7 @@ }, "x-appwrite": { "method": "createMembership", - "weight": 224, + "weight": 225, "cookies": false, "type": "", "deprecated": false, @@ -7012,7 +7014,7 @@ }, "x-appwrite": { "method": "getMembership", - "weight": 226, + "weight": 227, "cookies": false, "type": "", "deprecated": false, @@ -7083,7 +7085,7 @@ }, "x-appwrite": { "method": "updateMembership", - "weight": 227, + "weight": 228, "cookies": false, "type": "", "deprecated": false, @@ -7169,7 +7171,7 @@ }, "x-appwrite": { "method": "deleteMembership", - "weight": 229, + "weight": 230, "cookies": false, "type": "", "deprecated": false, @@ -7242,7 +7244,7 @@ }, "x-appwrite": { "method": "updateMembershipStatus", - "weight": 228, + "weight": 229, "cookies": false, "type": "", "deprecated": false, @@ -7339,7 +7341,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 220, + "weight": 221, "cookies": false, "type": "", "deprecated": false, @@ -7399,7 +7401,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 222, + "weight": 223, "cookies": false, "type": "", "deprecated": false, diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 954f3fcef7..6be9d6e5e0 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -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.", + "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.", "required": true, "schema": { "type": "string", @@ -3408,7 +3408,8 @@ "union-china-pay", "visa", "mir", - "maestro" + "maestro", + "rupay" ], "x-enum-name": "CreditCard", "x-enum-keys": [ @@ -3427,7 +3428,8 @@ "Union China Pay", "Visa", "MIR", - "Maestro" + "Maestro", + "Rupay" ] }, "in": "path" @@ -4293,7 +4295,7 @@ }, "x-appwrite": { "method": "chat", - "weight": 328, + "weight": 327, "cookies": false, "type": "", "deprecated": false, @@ -4337,6 +4339,73 @@ } } }, + "\/console\/resources": { + "get": { + "summary": "Check resource ID availability", + "operationId": "consoleGetResource", + "tags": [ + "console" + ], + "description": "", + "responses": { + "204": { + "description": "No content" + } + }, + "x-appwrite": { + "method": "getResource", + "weight": 422, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "console\/get-resource.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCheck if a resource ID is available.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.read", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "parameters": [ + { + "name": "value", + "description": "Resource value.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "query" + }, + { + "name": "type", + "description": "Resource type.", + "required": true, + "schema": { + "type": "string", + "x-example": "rules", + "enum": [ + "rules" + ], + "x-enum-name": "ConsoleResourceType", + "x-enum-keys": [] + }, + "in": "query" + } + ] + } + }, "\/console\/variables": { "get": { "summary": "Get variables", @@ -4359,7 +4428,7 @@ }, "x-appwrite": { "method": "variables", - "weight": 327, + "weight": 326, "cookies": false, "type": "", "deprecated": false, @@ -7823,7 +7892,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.", + "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", "responses": { "201": { "description": "Document", @@ -8986,7 +9055,7 @@ }, "x-appwrite": { "method": "list", - "weight": 389, + "weight": 388, "cookies": false, "type": "", "deprecated": false, @@ -9058,7 +9127,7 @@ }, "x-appwrite": { "method": "create", - "weight": 387, + "weight": 386, "cookies": false, "type": "", "deprecated": false, @@ -9248,26 +9317,6 @@ "description": "Path to function code in the linked repo.", "x-example": "" }, - "templateRepository": { - "type": "string", - "description": "Repository name of the template.", - "x-example": "" - }, - "templateOwner": { - "type": "string", - "description": "The name of the owner of the template.", - "x-example": "" - }, - "templateRootDirectory": { - "type": "string", - "description": "Path to function code in the template repo.", - "x-example": "" - }, - "templateVersion": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "specification": { "type": "string", "description": "Runtime specification for the function and builds.", @@ -9307,7 +9356,7 @@ }, "x-appwrite": { "method": "listRuntimes", - "weight": 390, + "weight": 389, "cookies": false, "type": "", "deprecated": false, @@ -9355,7 +9404,7 @@ }, "x-appwrite": { "method": "listSpecifications", - "weight": 287, + "weight": 288, "cookies": false, "type": "", "deprecated": false, @@ -9404,7 +9453,7 @@ }, "x-appwrite": { "method": "listTemplates", - "weight": 308, + "weight": 309, "cookies": false, "type": "", "deprecated": false, @@ -9503,7 +9552,7 @@ }, "x-appwrite": { "method": "getTemplate", - "weight": 309, + "weight": 310, "cookies": false, "type": "", "deprecated": false, @@ -9562,7 +9611,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 290, + "weight": 291, "cookies": false, "type": "", "deprecated": false, @@ -9633,7 +9682,7 @@ }, "x-appwrite": { "method": "get", - "weight": 288, + "weight": 289, "cookies": false, "type": "", "deprecated": false, @@ -9691,7 +9740,7 @@ }, "x-appwrite": { "method": "update", - "weight": 388, + "weight": 387, "cookies": false, "type": "", "deprecated": false, @@ -9917,7 +9966,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 293, + "weight": 294, "cookies": false, "type": "", "deprecated": false, @@ -9977,7 +10026,7 @@ }, "x-appwrite": { "method": "listDeployments", - "weight": 294, + "weight": 295, "cookies": false, "type": "", "deprecated": false, @@ -10059,7 +10108,7 @@ }, "x-appwrite": { "method": "createDeployment", - "weight": 391, + "weight": 390, "cookies": false, "type": "upload", "deprecated": false, @@ -10132,6 +10181,203 @@ } } }, + "\/functions\/{functionId}\/deployments\/template": { + "post": { + "summary": "Create template deployment", + "operationId": "functionsCreateTemplateDeployment", + "tags": [ + "functions" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "x-appwrite": { + "method": "createTemplateDeployment", + "weight": 391, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "functions\/create-template-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment based on a template.\n\nUse this endpoint with combination of [listTemplates](https:\/\/appwrite.io\/docs\/server\/functions#listTemplates) to find the template details.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "functions.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "functionId", + "description": "Function ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "Repository name of the template.", + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to function code in the template repo.", + "x-example": "" + }, + "version": { + "type": "string", + "description": "Version (tag) for the repo linked to the function template.", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "repository", + "owner", + "rootDirectory", + "version" + ] + } + } + } + } + } + }, + "\/functions\/{functionId}\/deployments\/vcs": { + "post": { + "summary": "Create VCS deployment", + "operationId": "functionsCreateVcsDeployment", + "tags": [ + "functions" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "x-appwrite": { + "method": "createVcsDeployment", + "weight": 392, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "functions\/create-vcs-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment when a function is connected to VCS.\n\nThis endpoint lets you create deployment from a branch, commit, or a tag.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "functions.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "functionId", + "description": "Function ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of reference passed. Allowed values are: branch", + "x-example": "branch", + "enum": [ + "branch" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "VCS reference to create deployment from. Depending on type this can be: branch name", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "type", + "reference" + ] + } + } + } + } + } + }, "\/functions\/{functionId}\/deployments\/{deploymentId}": { "get": { "summary": "Get deployment", @@ -10154,7 +10400,7 @@ }, "x-appwrite": { "method": "getDeployment", - "weight": 295, + "weight": 296, "cookies": false, "type": "", "deprecated": false, @@ -10215,7 +10461,7 @@ }, "x-appwrite": { "method": "deleteDeployment", - "weight": 296, + "weight": 297, "cookies": false, "type": "", "deprecated": false, @@ -10278,7 +10524,7 @@ }, "x-appwrite": { "method": "createBuild", - "weight": 297, + "weight": 298, "cookies": false, "type": "", "deprecated": false, @@ -10357,7 +10603,7 @@ }, "x-appwrite": { "method": "getDeploymentDownload", - "weight": 291, + "weight": 292, "cookies": false, "type": "location", "deprecated": false, @@ -10429,7 +10675,7 @@ }, "x-appwrite": { "method": "listExecutions", - "weight": 300, + "weight": 301, "cookies": false, "type": "", "deprecated": false, @@ -10514,7 +10760,7 @@ }, "x-appwrite": { "method": "createExecution", - "weight": 299, + "weight": 300, "cookies": false, "type": "", "deprecated": false, @@ -10628,7 +10874,7 @@ }, "x-appwrite": { "method": "getExecution", - "weight": 301, + "weight": 302, "cookies": false, "type": "", "deprecated": false, @@ -10692,7 +10938,7 @@ }, "x-appwrite": { "method": "deleteExecution", - "weight": 302, + "weight": 303, "cookies": false, "type": "", "deprecated": false, @@ -10762,7 +11008,7 @@ }, "x-appwrite": { "method": "getFunctionUsage", - "weight": 289, + "weight": 290, "cookies": false, "type": "", "deprecated": false, @@ -10843,7 +11089,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 304, + "weight": 305, "cookies": false, "type": "", "deprecated": false, @@ -10901,7 +11147,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 303, + "weight": 304, "cookies": false, "type": "", "deprecated": false, @@ -10955,7 +11201,7 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", + "description": "Secret variables can be updated or deleted, but only functions can read them during build and runtime.", "x-example": false } }, @@ -10991,7 +11237,7 @@ }, "x-appwrite": { "method": "query", - "weight": 326, + "weight": 325, "cookies": false, "type": "graphql", "deprecated": false, @@ -11042,7 +11288,7 @@ }, "x-appwrite": { "method": "mutation", - "weight": 325, + "weight": 324, "cookies": false, "type": "graphql", "deprecated": false, @@ -11141,7 +11387,7 @@ }, "x-appwrite": { "method": "getAntivirus", - "weight": 146, + "weight": 147, "cookies": false, "type": "", "deprecated": false, @@ -11647,7 +11893,7 @@ }, "x-appwrite": { "method": "getFailedJobs", - "weight": 147, + "weight": 148, "cookies": false, "type": "", "deprecated": false, @@ -11685,8 +11931,9 @@ "v1-audits", "v1-mails", "v1-functions", - "v1-usage", - "v1-usage-dump", + "v1-stats-resources", + "v1-stats-usage", + "v1-stats-usage-dump", "v1-webhooks", "v1-certificates", "v1-builds", @@ -12017,14 +12264,14 @@ ] } }, - "\/health\/queue\/usage": { + "\/health\/queue\/stats-resources": { "get": { - "summary": "Get usage queue", - "operationId": "healthGetQueueUsage", + "summary": "Get stats resources queue", + "operationId": "healthGetQueueStatsResources", "tags": [ "health" ], - "description": "Get the number of metrics that are waiting to be processed in the Appwrite internal queue server.", + "description": "Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue.", "responses": { "200": { "description": "Health Queue", @@ -12038,13 +12285,13 @@ } }, "x-appwrite": { - "method": "getQueueUsage", + "method": "getQueueStatsResources", "weight": 142, "cookies": false, "type": "", "deprecated": false, - "demo": "health\/get-queue-usage.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage.md", + "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}", @@ -12078,10 +12325,71 @@ ] } }, - "\/health\/queue\/usage-dump": { + "\/health\/queue\/stats-usage": { + "get": { + "summary": "Get stats usage queue", + "operationId": "healthGetQueueUsage", + "tags": [ + "health" + ], + "description": "Get the number of metrics that are waiting to be processed in the Appwrite internal queue server.", + "responses": { + "200": { + "description": "Health Queue", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/healthQueue" + } + } + } + } + }, + "x-appwrite": { + "method": "getQueueUsage", + "weight": 143, + "cookies": false, + "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", + "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-dump": { "get": { "summary": "Get usage dump queue", - "operationId": "healthGetQueueUsageDump", + "operationId": "healthGetQueueStatsUsageDump", "tags": [ "health" ], @@ -12099,13 +12407,13 @@ } }, "x-appwrite": { - "method": "getQueueUsageDump", - "weight": 143, + "method": "getQueueStatsUsageDump", + "weight": 144, "cookies": false, "type": "", "deprecated": false, - "demo": "health\/get-queue-usage-dump.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage-dump.md", + "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", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", @@ -12222,7 +12530,7 @@ }, "x-appwrite": { "method": "getStorage", - "weight": 145, + "weight": 146, "cookies": false, "type": "", "deprecated": false, @@ -12270,7 +12578,7 @@ }, "x-appwrite": { "method": "getStorageLocal", - "weight": 144, + "weight": 145, "cookies": false, "type": "", "deprecated": false, @@ -12774,7 +13082,7 @@ }, "x-appwrite": { "method": "listMessages", - "weight": 379, + "weight": 378, "cookies": false, "type": "", "deprecated": false, @@ -12849,7 +13157,7 @@ }, "x-appwrite": { "method": "createEmail", - "weight": 376, + "weight": 375, "cookies": false, "type": "", "deprecated": false, @@ -12992,7 +13300,7 @@ }, "x-appwrite": { "method": "updateEmail", - "weight": 383, + "weight": 382, "cookies": false, "type": "", "deprecated": false, @@ -13137,7 +13445,7 @@ }, "x-appwrite": { "method": "createPush", - "weight": 378, + "weight": 377, "cookies": false, "type": "", "deprecated": false, @@ -13310,7 +13618,7 @@ }, "x-appwrite": { "method": "updatePush", - "weight": 385, + "weight": 384, "cookies": false, "type": "", "deprecated": false, @@ -13487,7 +13795,7 @@ }, "x-appwrite": { "method": "createSms", - "weight": 377, + "weight": 376, "cookies": false, "type": "", "deprecated": false, @@ -13595,7 +13903,7 @@ }, "x-appwrite": { "method": "updateSms", - "weight": 384, + "weight": 383, "cookies": false, "type": "", "deprecated": false, @@ -13706,7 +14014,7 @@ }, "x-appwrite": { "method": "getMessage", - "weight": 382, + "weight": 381, "cookies": false, "type": "", "deprecated": false, @@ -13758,7 +14066,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 386, + "weight": 385, "cookies": false, "type": "", "deprecated": false, @@ -13819,7 +14127,7 @@ }, "x-appwrite": { "method": "listMessageLogs", - "weight": 380, + "weight": 379, "cookies": false, "type": "", "deprecated": false, @@ -13893,7 +14201,7 @@ }, "x-appwrite": { "method": "listTargets", - "weight": 381, + "weight": 380, "cookies": false, "type": "", "deprecated": false, @@ -13967,7 +14275,7 @@ }, "x-appwrite": { "method": "listProviders", - "weight": 351, + "weight": 350, "cookies": false, "type": "", "deprecated": false, @@ -14042,7 +14350,7 @@ }, "x-appwrite": { "method": "createApnsProvider", - "weight": 350, + "weight": 349, "cookies": false, "type": "", "deprecated": false, @@ -14146,7 +14454,7 @@ }, "x-appwrite": { "method": "updateApnsProvider", - "weight": 363, + "weight": 362, "cookies": false, "type": "", "deprecated": false, @@ -14253,7 +14561,7 @@ }, "x-appwrite": { "method": "createFcmProvider", - "weight": 349, + "weight": 348, "cookies": false, "type": "", "deprecated": false, @@ -14337,7 +14645,7 @@ }, "x-appwrite": { "method": "updateFcmProvider", - "weight": 362, + "weight": 361, "cookies": false, "type": "", "deprecated": false, @@ -14424,7 +14732,7 @@ }, "x-appwrite": { "method": "createMailgunProvider", - "weight": 341, + "weight": 340, "cookies": false, "type": "", "deprecated": false, @@ -14538,7 +14846,7 @@ }, "x-appwrite": { "method": "updateMailgunProvider", - "weight": 354, + "weight": 353, "cookies": false, "type": "", "deprecated": false, @@ -14655,7 +14963,7 @@ }, "x-appwrite": { "method": "createMsg91Provider", - "weight": 344, + "weight": 343, "cookies": false, "type": "", "deprecated": false, @@ -14749,7 +15057,7 @@ }, "x-appwrite": { "method": "updateMsg91Provider", - "weight": 357, + "weight": 356, "cookies": false, "type": "", "deprecated": false, @@ -14846,7 +15154,7 @@ }, "x-appwrite": { "method": "createSendgridProvider", - "weight": 342, + "weight": 341, "cookies": false, "type": "", "deprecated": false, @@ -14950,7 +15258,7 @@ }, "x-appwrite": { "method": "updateSendgridProvider", - "weight": 355, + "weight": 354, "cookies": false, "type": "", "deprecated": false, @@ -15057,7 +15365,7 @@ }, "x-appwrite": { "method": "createSmtpProvider", - "weight": 343, + "weight": 342, "cookies": false, "type": "", "deprecated": false, @@ -15199,7 +15507,7 @@ }, "x-appwrite": { "method": "updateSmtpProvider", - "weight": 356, + "weight": 355, "cookies": false, "type": "", "deprecated": false, @@ -15343,7 +15651,7 @@ }, "x-appwrite": { "method": "createTelesignProvider", - "weight": 345, + "weight": 344, "cookies": false, "type": "", "deprecated": false, @@ -15437,7 +15745,7 @@ }, "x-appwrite": { "method": "updateTelesignProvider", - "weight": 358, + "weight": 357, "cookies": false, "type": "", "deprecated": false, @@ -15534,7 +15842,7 @@ }, "x-appwrite": { "method": "createTextmagicProvider", - "weight": 346, + "weight": 345, "cookies": false, "type": "", "deprecated": false, @@ -15628,7 +15936,7 @@ }, "x-appwrite": { "method": "updateTextmagicProvider", - "weight": 359, + "weight": 358, "cookies": false, "type": "", "deprecated": false, @@ -15725,7 +16033,7 @@ }, "x-appwrite": { "method": "createTwilioProvider", - "weight": 347, + "weight": 346, "cookies": false, "type": "", "deprecated": false, @@ -15819,7 +16127,7 @@ }, "x-appwrite": { "method": "updateTwilioProvider", - "weight": 360, + "weight": 359, "cookies": false, "type": "", "deprecated": false, @@ -15916,7 +16224,7 @@ }, "x-appwrite": { "method": "createVonageProvider", - "weight": 348, + "weight": 347, "cookies": false, "type": "", "deprecated": false, @@ -16010,7 +16318,7 @@ }, "x-appwrite": { "method": "updateVonageProvider", - "weight": 361, + "weight": 360, "cookies": false, "type": "", "deprecated": false, @@ -16107,7 +16415,7 @@ }, "x-appwrite": { "method": "getProvider", - "weight": 353, + "weight": 352, "cookies": false, "type": "", "deprecated": false, @@ -16159,7 +16467,7 @@ }, "x-appwrite": { "method": "deleteProvider", - "weight": 364, + "weight": 363, "cookies": false, "type": "", "deprecated": false, @@ -16220,7 +16528,7 @@ }, "x-appwrite": { "method": "listProviderLogs", - "weight": 352, + "weight": 351, "cookies": false, "type": "", "deprecated": false, @@ -16294,7 +16602,7 @@ }, "x-appwrite": { "method": "listSubscriberLogs", - "weight": 373, + "weight": 372, "cookies": false, "type": "", "deprecated": false, @@ -16368,7 +16676,7 @@ }, "x-appwrite": { "method": "listTopics", - "weight": 366, + "weight": 365, "cookies": false, "type": "", "deprecated": false, @@ -16441,7 +16749,7 @@ }, "x-appwrite": { "method": "createTopic", - "weight": 365, + "weight": 364, "cookies": false, "type": "", "deprecated": false, @@ -16523,7 +16831,7 @@ }, "x-appwrite": { "method": "getTopic", - "weight": 368, + "weight": 367, "cookies": false, "type": "", "deprecated": false, @@ -16582,7 +16890,7 @@ }, "x-appwrite": { "method": "updateTopic", - "weight": 369, + "weight": 368, "cookies": false, "type": "", "deprecated": false, @@ -16658,7 +16966,7 @@ }, "x-appwrite": { "method": "deleteTopic", - "weight": 370, + "weight": 369, "cookies": false, "type": "", "deprecated": false, @@ -16719,7 +17027,7 @@ }, "x-appwrite": { "method": "listTopicLogs", - "weight": 367, + "weight": 366, "cookies": false, "type": "", "deprecated": false, @@ -16793,7 +17101,7 @@ }, "x-appwrite": { "method": "listSubscribers", - "weight": 372, + "weight": 371, "cookies": false, "type": "", "deprecated": false, @@ -16876,7 +17184,7 @@ }, "x-appwrite": { "method": "createSubscriber", - "weight": 371, + "weight": 370, "cookies": false, "type": "", "deprecated": false, @@ -16965,7 +17273,7 @@ }, "x-appwrite": { "method": "getSubscriber", - "weight": 374, + "weight": 373, "cookies": false, "type": "", "deprecated": false, @@ -17027,7 +17335,7 @@ }, "x-appwrite": { "method": "deleteSubscriber", - "weight": 375, + "weight": 374, "cookies": false, "type": "", "deprecated": false, @@ -17101,7 +17409,7 @@ }, "x-appwrite": { "method": "list", - "weight": 333, + "weight": 332, "cookies": false, "type": "", "deprecated": false, @@ -17174,7 +17482,7 @@ }, "x-appwrite": { "method": "createAppwriteMigration", - "weight": 329, + "weight": 328, "cookies": false, "type": "", "deprecated": false, @@ -17213,17 +17521,17 @@ }, "endpoint": { "type": "string", - "description": "Source's Appwrite Endpoint", + "description": "Source Appwrite endpoint", "x-example": "https:\/\/example.com" }, "projectId": { "type": "string", - "description": "Source's Project ID", + "description": "Source Project ID", "x-example": "" }, "apiKey": { "type": "string", - "description": "Source's API Key", + "description": "Source API Key", "x-example": "" } }, @@ -17261,7 +17569,7 @@ }, "x-appwrite": { "method": "getAppwriteReport", - "weight": 335, + "weight": 334, "cookies": false, "type": "", "deprecated": false, @@ -17353,7 +17661,7 @@ }, "x-appwrite": { "method": "createFirebaseMigration", - "weight": 330, + "weight": 329, "cookies": false, "type": "", "deprecated": false, @@ -17428,7 +17736,7 @@ }, "x-appwrite": { "method": "getFirebaseReport", - "weight": 336, + "weight": 335, "cookies": false, "type": "", "deprecated": false, @@ -17499,7 +17807,7 @@ }, "x-appwrite": { "method": "createNHostMigration", - "weight": 332, + "weight": 331, "cookies": false, "type": "", "deprecated": false, @@ -17609,7 +17917,7 @@ }, "x-appwrite": { "method": "getNHostReport", - "weight": 338, + "weight": 337, "cookies": false, "type": "", "deprecated": false, @@ -17741,7 +18049,7 @@ }, "x-appwrite": { "method": "createSupabaseMigration", - "weight": 331, + "weight": 330, "cookies": false, "type": "", "deprecated": false, @@ -17845,7 +18153,7 @@ }, "x-appwrite": { "method": "getSupabaseReport", - "weight": 337, + "weight": 336, "cookies": false, "type": "", "deprecated": false, @@ -17968,7 +18276,7 @@ }, "x-appwrite": { "method": "get", - "weight": 334, + "weight": 333, "cookies": false, "type": "", "deprecated": false, @@ -18025,7 +18333,7 @@ }, "x-appwrite": { "method": "retry", - "weight": 339, + "weight": 338, "cookies": false, "type": "", "deprecated": false, @@ -18075,7 +18383,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 340, + "weight": 339, "cookies": false, "type": "", "deprecated": false, @@ -18134,7 +18442,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 195, + "weight": 196, "cookies": false, "type": "", "deprecated": false, @@ -18221,7 +18529,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 197, + "weight": 198, "cookies": false, "type": "", "deprecated": false, @@ -18266,7 +18574,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 196, + "weight": 197, "cookies": false, "type": "", "deprecated": false, @@ -18307,7 +18615,7 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", + "description": "Secret variables can be updated or deleted, but only projects can read them during build and runtime.", "x-example": false } }, @@ -18343,7 +18651,7 @@ }, "x-appwrite": { "method": "getVariable", - "weight": 198, + "weight": 199, "cookies": false, "type": "", "deprecated": false, @@ -18400,7 +18708,7 @@ }, "x-appwrite": { "method": "updateVariable", - "weight": 199, + "weight": 200, "cookies": false, "type": "", "deprecated": false, @@ -18450,6 +18758,11 @@ "type": "string", "description": "Variable value. Max length: 8192 chars.", "x-example": "" + }, + "secret": { + "type": "boolean", + "description": "Secret variables can be updated or deleted, but only projects can read them during build and runtime.", + "x-example": false } }, "required": [ @@ -18474,7 +18787,7 @@ }, "x-appwrite": { "method": "deleteVariable", - "weight": 200, + "weight": 201, "cookies": false, "type": "", "deprecated": false, @@ -18533,7 +18846,7 @@ }, "x-appwrite": { "method": "list", - "weight": 150, + "weight": 151, "cookies": false, "type": "", "deprecated": false, @@ -18604,7 +18917,7 @@ }, "x-appwrite": { "method": "create", - "weight": 149, + "weight": 150, "cookies": false, "type": "", "deprecated": false, @@ -18738,7 +19051,7 @@ }, "x-appwrite": { "method": "get", - "weight": 151, + "weight": 152, "cookies": false, "type": "", "deprecated": false, @@ -18795,7 +19108,7 @@ }, "x-appwrite": { "method": "update", - "weight": 152, + "weight": 153, "cookies": false, "type": "", "deprecated": false, @@ -18909,7 +19222,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 169, + "weight": 170, "cookies": false, "type": "", "deprecated": false, @@ -18968,7 +19281,7 @@ }, "x-appwrite": { "method": "updateApiStatus", - "weight": 156, + "weight": 157, "cookies": false, "type": "", "deprecated": false, @@ -19059,7 +19372,7 @@ }, "x-appwrite": { "method": "updateApiStatusAll", - "weight": 157, + "weight": 158, "cookies": false, "type": "", "deprecated": false, @@ -19137,7 +19450,7 @@ }, "x-appwrite": { "method": "updateAuthDuration", - "weight": 162, + "weight": 163, "cookies": false, "type": "", "deprecated": false, @@ -19215,7 +19528,7 @@ }, "x-appwrite": { "method": "updateAuthLimit", - "weight": 161, + "weight": 162, "cookies": false, "type": "", "deprecated": false, @@ -19293,7 +19606,7 @@ }, "x-appwrite": { "method": "updateAuthSessionsLimit", - "weight": 167, + "weight": 168, "cookies": false, "type": "", "deprecated": false, @@ -19371,7 +19684,7 @@ }, "x-appwrite": { "method": "updateMembershipsPrivacy", - "weight": 160, + "weight": 161, "cookies": false, "type": "", "deprecated": false, @@ -19461,7 +19774,7 @@ }, "x-appwrite": { "method": "updateMockNumbers", - "weight": 168, + "weight": 169, "cookies": false, "type": "", "deprecated": false, @@ -19542,7 +19855,7 @@ }, "x-appwrite": { "method": "updateAuthPasswordDictionary", - "weight": 165, + "weight": 166, "cookies": false, "type": "", "deprecated": false, @@ -19620,7 +19933,7 @@ }, "x-appwrite": { "method": "updateAuthPasswordHistory", - "weight": 164, + "weight": 165, "cookies": false, "type": "", "deprecated": false, @@ -19698,7 +20011,7 @@ }, "x-appwrite": { "method": "updatePersonalDataCheck", - "weight": 166, + "weight": 167, "cookies": false, "type": "", "deprecated": false, @@ -19776,7 +20089,7 @@ }, "x-appwrite": { "method": "updateSessionAlerts", - "weight": 159, + "weight": 160, "cookies": false, "type": "", "deprecated": false, @@ -19854,7 +20167,7 @@ }, "x-appwrite": { "method": "updateAuthStatus", - "weight": 163, + "weight": 164, "cookies": false, "type": "", "deprecated": false, @@ -19953,7 +20266,7 @@ }, "x-appwrite": { "method": "createJWT", - "weight": 181, + "weight": 182, "cookies": false, "type": "", "deprecated": false, @@ -20039,7 +20352,7 @@ }, "x-appwrite": { "method": "listKeys", - "weight": 177, + "weight": 178, "cookies": false, "type": "", "deprecated": false, @@ -20096,7 +20409,7 @@ }, "x-appwrite": { "method": "createKey", - "weight": 176, + "weight": 177, "cookies": false, "type": "", "deprecated": false, @@ -20188,7 +20501,7 @@ }, "x-appwrite": { "method": "getKey", - "weight": 178, + "weight": 179, "cookies": false, "type": "", "deprecated": false, @@ -20255,7 +20568,7 @@ }, "x-appwrite": { "method": "updateKey", - "weight": 179, + "weight": 180, "cookies": false, "type": "", "deprecated": false, @@ -20348,7 +20661,7 @@ }, "x-appwrite": { "method": "deleteKey", - "weight": 180, + "weight": 181, "cookies": false, "type": "", "deprecated": false, @@ -20417,7 +20730,7 @@ }, "x-appwrite": { "method": "updateOAuth2", - "weight": 158, + "weight": 159, "cookies": false, "type": "", "deprecated": false, @@ -20553,7 +20866,7 @@ }, "x-appwrite": { "method": "listPlatforms", - "weight": 183, + "weight": 184, "cookies": false, "type": "", "deprecated": false, @@ -20610,7 +20923,7 @@ }, "x-appwrite": { "method": "createPlatform", - "weight": 182, + "weight": 183, "cookies": false, "type": "", "deprecated": false, @@ -20728,7 +21041,7 @@ }, "x-appwrite": { "method": "getPlatform", - "weight": 184, + "weight": 185, "cookies": false, "type": "", "deprecated": false, @@ -20795,7 +21108,7 @@ }, "x-appwrite": { "method": "updatePlatform", - "weight": 185, + "weight": 186, "cookies": false, "type": "", "deprecated": false, @@ -20889,7 +21202,7 @@ }, "x-appwrite": { "method": "deletePlatform", - "weight": 186, + "weight": 187, "cookies": false, "type": "", "deprecated": false, @@ -20958,7 +21271,7 @@ }, "x-appwrite": { "method": "updateServiceStatus", - "weight": 154, + "weight": 155, "cookies": false, "type": "", "deprecated": false, @@ -21058,7 +21371,7 @@ }, "x-appwrite": { "method": "updateServiceStatusAll", - "weight": 155, + "weight": 156, "cookies": false, "type": "", "deprecated": false, @@ -21136,7 +21449,7 @@ }, "x-appwrite": { "method": "updateSmtp", - "weight": 187, + "weight": 188, "cookies": false, "type": "", "deprecated": false, @@ -21253,7 +21566,7 @@ }, "x-appwrite": { "method": "createSmtpTest", - "weight": 188, + "weight": 189, "cookies": false, "type": "", "deprecated": false, @@ -21383,7 +21696,7 @@ }, "x-appwrite": { "method": "updateTeam", - "weight": 153, + "weight": 154, "cookies": false, "type": "", "deprecated": false, @@ -21461,7 +21774,7 @@ }, "x-appwrite": { "method": "getEmailTemplate", - "weight": 190, + "weight": 191, "cookies": false, "type": "", "deprecated": false, @@ -21684,7 +21997,7 @@ }, "x-appwrite": { "method": "updateEmailTemplate", - "weight": 192, + "weight": 193, "cookies": false, "type": "", "deprecated": false, @@ -21947,7 +22260,7 @@ }, "x-appwrite": { "method": "deleteEmailTemplate", - "weight": 194, + "weight": 195, "cookies": false, "type": "", "deprecated": false, @@ -22172,7 +22485,7 @@ }, "x-appwrite": { "method": "getSmsTemplate", - "weight": 189, + "weight": 190, "cookies": false, "type": "", "deprecated": false, @@ -22392,7 +22705,7 @@ }, "x-appwrite": { "method": "updateSmsTemplate", - "weight": 191, + "weight": 192, "cookies": false, "type": "", "deprecated": false, @@ -22631,7 +22944,7 @@ }, "x-appwrite": { "method": "deleteSmsTemplate", - "weight": 193, + "weight": 194, "cookies": false, "type": "", "deprecated": false, @@ -22853,7 +23166,7 @@ }, "x-appwrite": { "method": "listWebhooks", - "weight": 171, + "weight": 172, "cookies": false, "type": "", "deprecated": false, @@ -22910,7 +23223,7 @@ }, "x-appwrite": { "method": "createWebhook", - "weight": 170, + "weight": 171, "cookies": false, "type": "", "deprecated": false, @@ -23024,7 +23337,7 @@ }, "x-appwrite": { "method": "getWebhook", - "weight": 172, + "weight": 173, "cookies": false, "type": "", "deprecated": false, @@ -23091,7 +23404,7 @@ }, "x-appwrite": { "method": "updateWebhook", - "weight": 173, + "weight": 174, "cookies": false, "type": "", "deprecated": false, @@ -23206,7 +23519,7 @@ }, "x-appwrite": { "method": "deleteWebhook", - "weight": 175, + "weight": 176, "cookies": false, "type": "", "deprecated": false, @@ -23275,7 +23588,7 @@ }, "x-appwrite": { "method": "updateWebhookSignature", - "weight": 174, + "weight": 175, "cookies": false, "type": "", "deprecated": false, @@ -23370,7 +23683,7 @@ "parameters": [ { "name": "queries", - "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, resourceType, resourceId, url", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, type, value, automation, url", "required": false, "schema": { "type": "array", @@ -23393,14 +23706,16 @@ "in": "query" } ] - }, + } + }, + "\/proxy\/rules\/api": { "post": { - "summary": "Create rule", - "operationId": "proxyCreateRule", + "summary": "Create API rule", + "operationId": "proxyCreateAPIRule", "tags": [ "proxy" ], - "description": "Create a new proxy rule.", + "description": "", "responses": { "201": { "description": "Rule", @@ -23414,16 +23729,82 @@ } }, "x-appwrite": { - "method": "createRule", - "weight": 310, + "method": "createAPIRule", + "weight": 423, "cookies": false, "type": "", "deprecated": false, - "demo": "proxy\/create-rule.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/proxy\/create-rule.md", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", + "demo": "proxy\/create-a-p-i-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite's API on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "x-example": null + } + }, + "required": [ + "domain" + ] + } + } + } + } + } + }, + "\/proxy\/rules\/function": { + "post": { + "summary": "Create function rule", + "operationId": "proxyCreateFunctionRule", + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/proxyRule" + } + } + } + } + }, + "x-appwrite": { + "method": "createFunctionRule", + "weight": 425, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-function-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for executing Appwrite Function on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", "scope": "rules.write", "platforms": [ "console" @@ -23449,27 +23830,169 @@ "description": "Domain name.", "x-example": null }, - "resourceType": { + "functionId": { "type": "string", - "description": "Action definition for the rule. Possible values are \"api\", \"function\" and \"site\"", - "x-example": "api", - "enum": [ - "api", - "function", - "site" - ], - "x-enum-name": null, - "x-enum-keys": [] + "description": "ID of function to be executed.", + "x-example": "" }, - "resourceId": { + "branch": { "type": "string", - "description": "ID of resource for the action type. If resourceType is \"api\", leave empty. If resourceType is \"function\", provide ID of the function.", - "x-example": "" + "description": "Name of VCS branch to deploy changes automatically", + "x-example": "" } }, "required": [ "domain", - "resourceType" + "functionId" + ] + } + } + } + } + } + }, + "\/proxy\/rules\/redirect": { + "post": { + "summary": "Create Redirect rule", + "operationId": "proxyCreateRedirectRule", + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/proxyRule" + } + } + } + } + }, + "x-appwrite": { + "method": "createRedirectRule", + "weight": 426, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-redirect-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for to redirect from custom domain to another domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "x-example": null + }, + "target": { + "type": "string", + "description": "Target domain (hostname) of redirection", + "x-example": null + } + }, + "required": [ + "domain", + "target" + ] + } + } + } + } + } + }, + "\/proxy\/rules\/site": { + "post": { + "summary": "Create site rule", + "operationId": "proxyCreateSiteRule", + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/proxyRule" + } + } + } + } + }, + "x-appwrite": { + "method": "createSiteRule", + "weight": 424, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-site-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite Site on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "x-example": null + }, + "siteId": { + "type": "string", + "description": "ID of site to be executed.", + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Name of VCS branch to deploy changes automatically", + "x-example": "" + } + }, + "required": [ + "domain", + "siteId" ] } } @@ -23667,7 +24190,7 @@ }, "x-appwrite": { "method": "list", - "weight": 394, + "weight": 395, "cookies": false, "type": "", "deprecated": false, @@ -23736,7 +24259,7 @@ }, "x-appwrite": { "method": "create", - "weight": 392, + "weight": 393, "cookies": false, "type": "", "deprecated": false, @@ -23782,7 +24305,9 @@ "x-example": "nextjs", "enum": [ "nextjs", + "react", "nuxt", + "vue", "sveltekit", "astro", "remix", @@ -23817,11 +24342,6 @@ "description": "Output Directory for site.", "x-example": "" }, - "subdomain": { - "type": "string", - "description": "Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.", - "x-example": "" - }, "buildRuntime": { "type": "string", "description": "Runtime to use during build step.", @@ -23895,8 +24415,14 @@ }, "adapter": { "type": "string", - "description": "Framework adapter. Allows: static, ssr", - "x-example": "" + "description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr", + "x-example": "static", + "enum": [ + "static", + "ssr" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "installationId": { "type": "string", @@ -23928,26 +24454,6 @@ "description": "Path to site code in the linked repo.", "x-example": "" }, - "templateRepository": { - "type": "string", - "description": "Repository name of the template.", - "x-example": "" - }, - "templateOwner": { - "type": "string", - "description": "The name of the owner of the template.", - "x-example": "" - }, - "templateRootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "x-example": "" - }, - "templateVersion": { - "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "x-example": "" - }, "specification": { "type": "string", "description": "Framework specification for the site and builds.", @@ -23988,7 +24494,7 @@ }, "x-appwrite": { "method": "listFrameworks", - "weight": 397, + "weight": 398, "cookies": false, "type": "", "deprecated": false, @@ -24036,7 +24542,7 @@ }, "x-appwrite": { "method": "listTemplates", - "weight": 415, + "weight": 418, "cookies": false, "type": "", "deprecated": false, @@ -24135,7 +24641,7 @@ }, "x-appwrite": { "method": "getTemplate", - "weight": 416, + "weight": 419, "cookies": false, "type": "", "deprecated": false, @@ -24194,7 +24700,7 @@ }, "x-appwrite": { "method": "listUsage", - "weight": 417, + "weight": 420, "cookies": false, "type": "", "deprecated": false, @@ -24261,7 +24767,7 @@ }, "x-appwrite": { "method": "get", - "weight": 393, + "weight": 394, "cookies": false, "type": "", "deprecated": false, @@ -24319,7 +24825,7 @@ }, "x-appwrite": { "method": "update", - "weight": 395, + "weight": 396, "cookies": false, "type": "", "deprecated": false, @@ -24372,7 +24878,9 @@ "x-example": "nextjs", "enum": [ "nextjs", + "react", "nuxt", + "vue", "sveltekit", "astro", "remix", @@ -24480,8 +24988,14 @@ }, "adapter": { "type": "string", - "description": "Framework adapter. Usuallly allows: static, ssr", - "x-example": "" + "description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr", + "x-example": "static", + "enum": [ + "static", + "ssr" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "fallbackFile": { "type": "string", @@ -24542,7 +25056,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 396, + "weight": 397, "cookies": false, "type": "", "deprecated": false, @@ -24602,7 +25116,7 @@ }, "x-appwrite": { "method": "listDeployments", - "weight": 400, + "weight": 403, "cookies": false, "type": "", "deprecated": false, @@ -24684,7 +25198,7 @@ }, "x-appwrite": { "method": "createDeployment", - "weight": 398, + "weight": 399, "cookies": false, "type": "upload", "deprecated": false, @@ -24762,6 +25276,203 @@ } } }, + "\/sites\/{siteId}\/deployments\/template": { + "post": { + "summary": "Create deployment", + "operationId": "sitesCreateTemplateDeployment", + "tags": [ + "sites" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "x-appwrite": { + "method": "createTemplateDeployment", + "weight": 400, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "sites\/create-template-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment based on a template.\n\nUse this endpoint with combination of [listTemplates](https:\/\/appwrite.io\/docs\/server\/sites#listTemplates) to find the template details.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "siteId", + "description": "Site ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "Repository name of the template.", + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "x-example": "" + }, + "version": { + "type": "string", + "description": "Version (tag) for the repo linked to the site template.", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "repository", + "owner", + "rootDirectory", + "version" + ] + } + } + } + } + } + }, + "\/sites\/{siteId}\/deployments\/vcs": { + "post": { + "summary": "Create VCS deployment", + "operationId": "sitesCreateVcsDeployment", + "tags": [ + "sites" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "x-appwrite": { + "method": "createVcsDeployment", + "weight": 401, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "sites\/create-vcs-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment when a site is connected to VCS.\n\nThis endpoint lets you create deployment from a branch, commit, or a tag.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "siteId", + "description": "Site ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of reference passed. Allowed values are: branch", + "x-example": "branch", + "enum": [ + "branch" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "VCS reference to create deployment from. Depending on type this can be: branch name", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "type", + "reference" + ] + } + } + } + } + } + }, "\/sites\/{siteId}\/deployments\/{deploymentId}": { "get": { "summary": "Get deployment", @@ -24784,7 +25495,7 @@ }, "x-appwrite": { "method": "getDeployment", - "weight": 399, + "weight": 402, "cookies": false, "type": "", "deprecated": false, @@ -24852,7 +25563,7 @@ }, "x-appwrite": { "method": "updateDeployment", - "weight": 401, + "weight": 404, "cookies": false, "type": "", "deprecated": false, @@ -24913,7 +25624,7 @@ }, "x-appwrite": { "method": "deleteDeployment", - "weight": 402, + "weight": 405, "cookies": false, "type": "", "deprecated": false, @@ -24976,7 +25687,7 @@ }, "x-appwrite": { "method": "createDeploymentBuild", - "weight": 405, + "weight": 408, "cookies": false, "type": "", "deprecated": false, @@ -25044,7 +25755,7 @@ }, "x-appwrite": { "method": "updateDeploymentBuild", - "weight": 406, + "weight": 409, "cookies": false, "type": "", "deprecated": false, @@ -25107,7 +25818,7 @@ }, "x-appwrite": { "method": "getDeploymentBuildDownload", - "weight": 404, + "weight": 407, "cookies": false, "type": "location", "deprecated": false, @@ -25172,7 +25883,7 @@ }, "x-appwrite": { "method": "getDeploymentDownload", - "weight": 403, + "weight": 406, "cookies": false, "type": "location", "deprecated": false, @@ -25244,7 +25955,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 408, + "weight": 411, "cookies": false, "type": "", "deprecated": false, @@ -25325,7 +26036,7 @@ }, "x-appwrite": { "method": "getLog", - "weight": 407, + "weight": 410, "cookies": false, "type": "", "deprecated": false, @@ -25386,7 +26097,7 @@ }, "x-appwrite": { "method": "deleteLog", - "weight": 409, + "weight": 412, "cookies": false, "type": "", "deprecated": false, @@ -25456,7 +26167,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 418, + "weight": 421, "cookies": false, "type": "", "deprecated": false, @@ -25537,7 +26248,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 412, + "weight": 415, "cookies": false, "type": "", "deprecated": false, @@ -25595,7 +26306,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 410, + "weight": 413, "cookies": false, "type": "", "deprecated": false, @@ -25649,7 +26360,7 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", + "description": "Secret variables can be updated or deleted, but only sites can read them during build and runtime.", "x-example": false } }, @@ -25685,7 +26396,7 @@ }, "x-appwrite": { "method": "getVariable", - "weight": 411, + "weight": 414, "cookies": false, "type": "", "deprecated": false, @@ -25753,7 +26464,7 @@ }, "x-appwrite": { "method": "updateVariable", - "weight": 413, + "weight": 416, "cookies": false, "type": "", "deprecated": false, @@ -25814,6 +26525,11 @@ "type": "string", "description": "Variable value. Max length: 8192 chars.", "x-example": "" + }, + "secret": { + "type": "boolean", + "description": "Secret variables can be updated or deleted, but only sites can read them during build and runtime.", + "x-example": false } }, "required": [ @@ -25838,7 +26554,7 @@ }, "x-appwrite": { "method": "deleteVariable", - "weight": 414, + "weight": 417, "cookies": false, "type": "", "deprecated": false, @@ -25908,7 +26624,7 @@ }, "x-appwrite": { "method": "listBuckets", - "weight": 202, + "weight": 203, "cookies": false, "type": "", "deprecated": false, @@ -25980,7 +26696,7 @@ }, "x-appwrite": { "method": "createBucket", - "weight": 201, + "weight": 202, "cookies": false, "type": "", "deprecated": false, @@ -26106,7 +26822,7 @@ }, "x-appwrite": { "method": "getBucket", - "weight": 203, + "weight": 204, "cookies": false, "type": "", "deprecated": false, @@ -26164,7 +26880,7 @@ }, "x-appwrite": { "method": "updateBucket", - "weight": 204, + "weight": 205, "cookies": false, "type": "", "deprecated": false, @@ -26287,7 +27003,7 @@ }, "x-appwrite": { "method": "deleteBucket", - "weight": 205, + "weight": 206, "cookies": false, "type": "", "deprecated": false, @@ -26347,7 +27063,7 @@ }, "x-appwrite": { "method": "listFiles", - "weight": 207, + "weight": 208, "cookies": false, "type": "", "deprecated": false, @@ -26432,7 +27148,7 @@ }, "x-appwrite": { "method": "createFile", - "weight": 206, + "weight": 207, "cookies": false, "type": "upload", "deprecated": false, @@ -26529,7 +27245,7 @@ }, "x-appwrite": { "method": "getFile", - "weight": 208, + "weight": 209, "cookies": false, "type": "", "deprecated": false, @@ -26600,7 +27316,7 @@ }, "x-appwrite": { "method": "updateFile", - "weight": 213, + "weight": 214, "cookies": false, "type": "", "deprecated": false, @@ -26688,7 +27404,7 @@ }, "x-appwrite": { "method": "deleteFile", - "weight": 214, + "weight": 215, "cookies": false, "type": "", "deprecated": false, @@ -26754,7 +27470,7 @@ }, "x-appwrite": { "method": "getFileDownload", - "weight": 210, + "weight": 211, "cookies": false, "type": "location", "deprecated": false, @@ -26820,7 +27536,7 @@ }, "x-appwrite": { "method": "getFilePreview", - "weight": 209, + "weight": 210, "cookies": false, "type": "location", "deprecated": false, @@ -27036,7 +27752,7 @@ }, "x-appwrite": { "method": "getFileView", - "weight": 211, + "weight": 212, "cookies": false, "type": "location", "deprecated": false, @@ -27109,7 +27825,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 215, + "weight": 216, "cookies": false, "type": "", "deprecated": false, @@ -27180,7 +27896,7 @@ }, "x-appwrite": { "method": "getBucketUsage", - "weight": 216, + "weight": 217, "cookies": false, "type": "", "deprecated": false, @@ -27261,7 +27977,7 @@ }, "x-appwrite": { "method": "list", - "weight": 218, + "weight": 219, "cookies": false, "type": "", "deprecated": false, @@ -27336,7 +28052,7 @@ }, "x-appwrite": { "method": "create", - "weight": 217, + "weight": 218, "cookies": false, "type": "", "deprecated": false, @@ -27420,7 +28136,7 @@ }, "x-appwrite": { "method": "get", - "weight": 219, + "weight": 220, "cookies": false, "type": "", "deprecated": false, @@ -27481,7 +28197,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 221, + "weight": 222, "cookies": false, "type": "", "deprecated": false, @@ -27554,7 +28270,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 223, + "weight": 224, "cookies": false, "type": "", "deprecated": false, @@ -27617,7 +28333,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 230, + "weight": 231, "cookies": false, "type": "", "deprecated": false, @@ -27689,7 +28405,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 225, + "weight": 226, "cookies": false, "type": "", "deprecated": false, @@ -27774,7 +28490,7 @@ }, "x-appwrite": { "method": "createMembership", - "weight": 224, + "weight": 225, "cookies": false, "type": "", "deprecated": false, @@ -27884,7 +28600,7 @@ }, "x-appwrite": { "method": "getMembership", - "weight": 226, + "weight": 227, "cookies": false, "type": "", "deprecated": false, @@ -27955,7 +28671,7 @@ }, "x-appwrite": { "method": "updateMembership", - "weight": 227, + "weight": 228, "cookies": false, "type": "", "deprecated": false, @@ -28041,7 +28757,7 @@ }, "x-appwrite": { "method": "deleteMembership", - "weight": 229, + "weight": 230, "cookies": false, "type": "", "deprecated": false, @@ -28114,7 +28830,7 @@ }, "x-appwrite": { "method": "updateMembershipStatus", - "weight": 228, + "weight": 229, "cookies": false, "type": "", "deprecated": false, @@ -28210,7 +28926,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 220, + "weight": 221, "cookies": false, "type": "", "deprecated": false, @@ -28269,7 +28985,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 222, + "weight": 223, "cookies": false, "type": "", "deprecated": false, @@ -28349,7 +29065,7 @@ }, "x-appwrite": { "method": "list", - "weight": 240, + "weight": 241, "cookies": false, "type": "", "deprecated": false, @@ -28421,7 +29137,7 @@ }, "x-appwrite": { "method": "create", - "weight": 231, + "weight": 232, "cookies": false, "type": "", "deprecated": false, @@ -28508,7 +29224,7 @@ }, "x-appwrite": { "method": "createArgon2User", - "weight": 234, + "weight": 235, "cookies": false, "type": "", "deprecated": false, @@ -28592,7 +29308,7 @@ }, "x-appwrite": { "method": "createBcryptUser", - "weight": 232, + "weight": 233, "cookies": false, "type": "", "deprecated": false, @@ -28676,7 +29392,7 @@ }, "x-appwrite": { "method": "listIdentities", - "weight": 248, + "weight": 249, "cookies": false, "type": "", "deprecated": false, @@ -28743,7 +29459,7 @@ }, "x-appwrite": { "method": "deleteIdentity", - "weight": 271, + "weight": 272, "cookies": false, "type": "", "deprecated": false, @@ -28803,7 +29519,7 @@ }, "x-appwrite": { "method": "createMD5User", - "weight": 233, + "weight": 234, "cookies": false, "type": "", "deprecated": false, @@ -28887,7 +29603,7 @@ }, "x-appwrite": { "method": "createPHPassUser", - "weight": 236, + "weight": 237, "cookies": false, "type": "", "deprecated": false, @@ -28971,7 +29687,7 @@ }, "x-appwrite": { "method": "createScryptUser", - "weight": 237, + "weight": 238, "cookies": false, "type": "", "deprecated": false, @@ -29085,7 +29801,7 @@ }, "x-appwrite": { "method": "createScryptModifiedUser", - "weight": 238, + "weight": 239, "cookies": false, "type": "", "deprecated": false, @@ -29187,7 +29903,7 @@ }, "x-appwrite": { "method": "createSHAUser", - "weight": 235, + "weight": 236, "cookies": false, "type": "", "deprecated": false, @@ -29291,7 +30007,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 273, + "weight": 274, "cookies": false, "type": "", "deprecated": false, @@ -29362,7 +30078,7 @@ }, "x-appwrite": { "method": "get", - "weight": 241, + "weight": 242, "cookies": false, "type": "", "deprecated": false, @@ -29413,7 +30129,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 269, + "weight": 270, "cookies": false, "type": "", "deprecated": false, @@ -29473,7 +30189,7 @@ }, "x-appwrite": { "method": "updateEmail", - "weight": 254, + "weight": 255, "cookies": false, "type": "", "deprecated": false, @@ -29552,7 +30268,7 @@ }, "x-appwrite": { "method": "createJWT", - "weight": 272, + "weight": 273, "cookies": false, "type": "", "deprecated": false, @@ -29633,7 +30349,7 @@ }, "x-appwrite": { "method": "updateLabels", - "weight": 250, + "weight": 251, "cookies": false, "type": "", "deprecated": false, @@ -29715,7 +30431,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 246, + "weight": 247, "cookies": false, "type": "", "deprecated": false, @@ -29788,7 +30504,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 245, + "weight": 246, "cookies": false, "type": "", "deprecated": false, @@ -29848,7 +30564,7 @@ }, "x-appwrite": { "method": "updateMfa", - "weight": 259, + "weight": 260, "cookies": false, "type": "", "deprecated": false, @@ -29920,7 +30636,7 @@ }, "x-appwrite": { "method": "deleteMfaAuthenticator", - "weight": 264, + "weight": 265, "cookies": false, "type": "", "deprecated": false, @@ -29995,7 +30711,7 @@ }, "x-appwrite": { "method": "listMfaFactors", - "weight": 260, + "weight": 261, "cookies": false, "type": "", "deprecated": false, @@ -30055,7 +30771,7 @@ }, "x-appwrite": { "method": "getMfaRecoveryCodes", - "weight": 261, + "weight": 262, "cookies": false, "type": "", "deprecated": false, @@ -30113,7 +30829,7 @@ }, "x-appwrite": { "method": "updateMfaRecoveryCodes", - "weight": 263, + "weight": 264, "cookies": false, "type": "", "deprecated": false, @@ -30171,7 +30887,7 @@ }, "x-appwrite": { "method": "createMfaRecoveryCodes", - "weight": 262, + "weight": 263, "cookies": false, "type": "", "deprecated": false, @@ -30231,7 +30947,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 252, + "weight": 253, "cookies": false, "type": "", "deprecated": false, @@ -30310,7 +31026,7 @@ }, "x-appwrite": { "method": "updatePassword", - "weight": 253, + "weight": 254, "cookies": false, "type": "", "deprecated": false, @@ -30389,7 +31105,7 @@ }, "x-appwrite": { "method": "updatePhone", - "weight": 255, + "weight": 256, "cookies": false, "type": "", "deprecated": false, @@ -30468,7 +31184,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 242, + "weight": 243, "cookies": false, "type": "", "deprecated": false, @@ -30526,7 +31242,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 257, + "weight": 258, "cookies": false, "type": "", "deprecated": false, @@ -30605,7 +31321,7 @@ }, "x-appwrite": { "method": "listSessions", - "weight": 244, + "weight": 245, "cookies": false, "type": "", "deprecated": false, @@ -30663,7 +31379,7 @@ }, "x-appwrite": { "method": "createSession", - "weight": 265, + "weight": 266, "cookies": false, "type": "", "deprecated": false, @@ -30714,7 +31430,7 @@ }, "x-appwrite": { "method": "deleteSessions", - "weight": 268, + "weight": 269, "cookies": false, "type": "", "deprecated": false, @@ -30767,7 +31483,7 @@ }, "x-appwrite": { "method": "deleteSession", - "weight": 267, + "weight": 268, "cookies": false, "type": "", "deprecated": false, @@ -30837,7 +31553,7 @@ }, "x-appwrite": { "method": "updateStatus", - "weight": 249, + "weight": 250, "cookies": false, "type": "", "deprecated": false, @@ -30916,7 +31632,7 @@ }, "x-appwrite": { "method": "listTargets", - "weight": 247, + "weight": 248, "cookies": false, "type": "", "deprecated": false, @@ -30988,7 +31704,7 @@ }, "x-appwrite": { "method": "createTarget", - "weight": 239, + "weight": 240, "cookies": false, "type": "", "deprecated": false, @@ -31097,7 +31813,7 @@ }, "x-appwrite": { "method": "getTarget", - "weight": 243, + "weight": 244, "cookies": false, "type": "", "deprecated": false, @@ -31166,7 +31882,7 @@ }, "x-appwrite": { "method": "updateTarget", - "weight": 258, + "weight": 259, "cookies": false, "type": "", "deprecated": false, @@ -31254,7 +31970,7 @@ }, "x-appwrite": { "method": "deleteTarget", - "weight": 270, + "weight": 271, "cookies": false, "type": "", "deprecated": false, @@ -31325,7 +32041,7 @@ }, "x-appwrite": { "method": "createToken", - "weight": 266, + "weight": 267, "cookies": false, "type": "", "deprecated": false, @@ -31406,7 +32122,7 @@ }, "x-appwrite": { "method": "updateEmailVerification", - "weight": 256, + "weight": 257, "cookies": false, "type": "", "deprecated": false, @@ -31485,7 +32201,7 @@ }, "x-appwrite": { "method": "updatePhoneVerification", - "weight": 251, + "weight": 252, "cookies": false, "type": "", "deprecated": false, @@ -31564,7 +32280,7 @@ }, "x-appwrite": { "method": "listRepositories", - "weight": 278, + "weight": 279, "cookies": false, "type": "", "deprecated": false, @@ -31632,7 +32348,7 @@ }, "x-appwrite": { "method": "createRepository", - "weight": 279, + "weight": 280, "cookies": false, "type": "", "deprecated": false, @@ -31716,7 +32432,7 @@ }, "x-appwrite": { "method": "getRepository", - "weight": 280, + "weight": 281, "cookies": false, "type": "", "deprecated": false, @@ -31785,7 +32501,7 @@ }, "x-appwrite": { "method": "listRepositoryBranches", - "weight": 281, + "weight": 282, "cookies": false, "type": "", "deprecated": false, @@ -31854,7 +32570,7 @@ }, "x-appwrite": { "method": "getRepositoryContents", - "weight": 276, + "weight": 277, "cookies": false, "type": "", "deprecated": false, @@ -31934,7 +32650,7 @@ }, "x-appwrite": { "method": "createRepositoryDetection", - "weight": 277, + "weight": 278, "cookies": false, "type": "", "deprecated": false, @@ -32012,7 +32728,7 @@ }, "x-appwrite": { "method": "updateExternalDeployments", - "weight": 286, + "weight": 287, "cookies": false, "type": "", "deprecated": false, @@ -32100,7 +32816,7 @@ }, "x-appwrite": { "method": "listInstallations", - "weight": 283, + "weight": 284, "cookies": false, "type": "", "deprecated": false, @@ -32173,7 +32889,7 @@ }, "x-appwrite": { "method": "getInstallation", - "weight": 284, + "weight": 285, "cookies": false, "type": "", "deprecated": false, @@ -32223,7 +32939,7 @@ }, "x-appwrite": { "method": "deleteInstallation", - "weight": 285, + "weight": 286, "cookies": false, "type": "", "deprecated": false, @@ -36647,6 +37363,16 @@ "description": "Whether the deployment should be automatically activated.", "x-example": true }, + "screenshotLight": { + "type": "string", + "description": "Screenshot with light theme preference file ID.", + "x-example": "5e5ea5c16897e" + }, + "screenshotDark": { + "type": "string", + "description": "Screenshot with dark theme preference file ID.", + "x-example": "5e5ea5c16897e" + }, "status": { "type": "string", "description": "The deployment status. Possible values are \"processing\", \"building\", \"waiting\", \"ready\", and \"failed\".", @@ -36663,11 +37389,6 @@ "x-example": 128, "format": "int32" }, - "domain": { - "type": "string", - "description": "Preview domain.", - "x-example": "deploy1-project1.appwrite.site" - }, "providerRepositoryName": { "type": "string", "description": "The name of the vcs provider repository", @@ -36731,10 +37452,11 @@ "buildSize", "buildId", "activate", + "screenshotLight", + "screenshotDark", "status", "buildLogs", "buildTime", - "domain", "providerRepositoryName", "providerRepositoryOwner", "providerRepositoryUrl", @@ -39274,15 +39996,20 @@ "description": "Domain name.", "x-example": "appwrite.company.com" }, - "resourceType": { + "type": { "type": "string", - "description": "Action definition for the rule. Possible values are \"api\", \"function\", or \"redirect\"", - "x-example": "function" + "description": "Action definition for the rule. Possible values are \"api\", \"deployment\", or \"redirect\"", + "x-example": "deployment" }, - "resourceId": { + "value": { "type": "string", - "description": "ID of resource for the action type. If resourceType is \"api\" or \"url\", it is empty. If resourceType is \"function\", it is ID of the function.", - "x-example": "myAwesomeFunction" + "description": "Detail specification for the type. If type is \"api\", this is empty. If type is \"redirect\", this is URL. If type is \"deployment\", this is deployment ID.", + "x-example": "67a9cf1a00150ee93abd" + }, + "automation": { + "type": "string", + "description": "Action that results in a rule update. If VCS branch, value can be of syntax \"branch=[name]\"", + "x-example": "branch=dev" }, "status": { "type": "string", @@ -39305,8 +40032,9 @@ "$createdAt", "$updatedAt", "domain", - "resourceType", - "resourceId", + "type", + "value", + "automation", "status", "logs", "renewAt" @@ -39438,6 +40166,11 @@ "type": "string", "description": "Defines if HTTPS is enforced for all requests.", "x-example": "enabled" + }, + "_APP_DOMAINS_NAMESERVERS": { + "type": "string", + "description": "Comma-separated list of nameservers.", + "x-example": "ns1.example.com,ns2.example.com" } }, "required": [ @@ -39449,7 +40182,8 @@ "_APP_DOMAIN_ENABLED", "_APP_ASSISTANT_ENABLED", "_APP_DOMAIN_SITES", - "_APP_OPTIONS_FORCE_HTTPS" + "_APP_OPTIONS_FORCE_HTTPS", + "_APP_DOMAINS_NAMESERVERS" ] }, "mfaChallenge": { diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index eb740ba0da..9bc803d777 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -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.", + "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.", "required": true, "schema": { "type": "string", @@ -3098,7 +3098,8 @@ "union-china-pay", "visa", "mir", - "maestro" + "maestro", + "rupay" ], "x-enum-name": "CreditCard", "x-enum-keys": [ @@ -3117,7 +3118,8 @@ "Union China Pay", "Visa", "MIR", - "Maestro" + "Maestro", + "Rupay" ] }, "in": "path" @@ -7381,7 +7383,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.", + "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", "responses": { "201": { "description": "Document", @@ -8138,7 +8140,7 @@ }, "x-appwrite": { "method": "list", - "weight": 389, + "weight": 388, "cookies": false, "type": "", "deprecated": false, @@ -8211,7 +8213,7 @@ }, "x-appwrite": { "method": "create", - "weight": 387, + "weight": 386, "cookies": false, "type": "", "deprecated": false, @@ -8402,26 +8404,6 @@ "description": "Path to function code in the linked repo.", "x-example": "" }, - "templateRepository": { - "type": "string", - "description": "Repository name of the template.", - "x-example": "" - }, - "templateOwner": { - "type": "string", - "description": "The name of the owner of the template.", - "x-example": "" - }, - "templateRootDirectory": { - "type": "string", - "description": "Path to function code in the template repo.", - "x-example": "" - }, - "templateVersion": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "specification": { "type": "string", "description": "Runtime specification for the function and builds.", @@ -8461,7 +8443,7 @@ }, "x-appwrite": { "method": "listRuntimes", - "weight": 390, + "weight": 389, "cookies": false, "type": "", "deprecated": false, @@ -8510,7 +8492,7 @@ }, "x-appwrite": { "method": "listSpecifications", - "weight": 287, + "weight": 288, "cookies": false, "type": "", "deprecated": false, @@ -8560,7 +8542,7 @@ }, "x-appwrite": { "method": "get", - "weight": 288, + "weight": 289, "cookies": false, "type": "", "deprecated": false, @@ -8619,7 +8601,7 @@ }, "x-appwrite": { "method": "update", - "weight": 388, + "weight": 387, "cookies": false, "type": "", "deprecated": false, @@ -8846,7 +8828,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 293, + "weight": 294, "cookies": false, "type": "", "deprecated": false, @@ -8907,7 +8889,7 @@ }, "x-appwrite": { "method": "listDeployments", - "weight": 294, + "weight": 295, "cookies": false, "type": "", "deprecated": false, @@ -8990,7 +8972,7 @@ }, "x-appwrite": { "method": "createDeployment", - "weight": 391, + "weight": 390, "cookies": false, "type": "upload", "deprecated": false, @@ -9064,6 +9046,205 @@ } } }, + "\/functions\/{functionId}\/deployments\/template": { + "post": { + "summary": "Create template deployment", + "operationId": "functionsCreateTemplateDeployment", + "tags": [ + "functions" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "x-appwrite": { + "method": "createTemplateDeployment", + "weight": 391, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "functions\/create-template-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment based on a template.\n\nUse this endpoint with combination of [listTemplates](https:\/\/appwrite.io\/docs\/server\/functions#listTemplates) to find the template details.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "functions.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "functionId", + "description": "Function ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "Repository name of the template.", + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to function code in the template repo.", + "x-example": "" + }, + "version": { + "type": "string", + "description": "Version (tag) for the repo linked to the function template.", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "repository", + "owner", + "rootDirectory", + "version" + ] + } + } + } + } + } + }, + "\/functions\/{functionId}\/deployments\/vcs": { + "post": { + "summary": "Create VCS deployment", + "operationId": "functionsCreateVcsDeployment", + "tags": [ + "functions" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "x-appwrite": { + "method": "createVcsDeployment", + "weight": 392, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "functions\/create-vcs-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment when a function is connected to VCS.\n\nThis endpoint lets you create deployment from a branch, commit, or a tag.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "functions.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "functionId", + "description": "Function ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of reference passed. Allowed values are: branch", + "x-example": "branch", + "enum": [ + "branch" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "VCS reference to create deployment from. Depending on type this can be: branch name", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "type", + "reference" + ] + } + } + } + } + } + }, "\/functions\/{functionId}\/deployments\/{deploymentId}": { "get": { "summary": "Get deployment", @@ -9086,7 +9267,7 @@ }, "x-appwrite": { "method": "getDeployment", - "weight": 295, + "weight": 296, "cookies": false, "type": "", "deprecated": false, @@ -9148,7 +9329,7 @@ }, "x-appwrite": { "method": "deleteDeployment", - "weight": 296, + "weight": 297, "cookies": false, "type": "", "deprecated": false, @@ -9212,7 +9393,7 @@ }, "x-appwrite": { "method": "createBuild", - "weight": 297, + "weight": 298, "cookies": false, "type": "", "deprecated": false, @@ -9292,7 +9473,7 @@ }, "x-appwrite": { "method": "getDeploymentDownload", - "weight": 291, + "weight": 292, "cookies": false, "type": "location", "deprecated": false, @@ -9365,7 +9546,7 @@ }, "x-appwrite": { "method": "listExecutions", - "weight": 300, + "weight": 301, "cookies": false, "type": "", "deprecated": false, @@ -9452,7 +9633,7 @@ }, "x-appwrite": { "method": "createExecution", - "weight": 299, + "weight": 300, "cookies": false, "type": "", "deprecated": false, @@ -9568,7 +9749,7 @@ }, "x-appwrite": { "method": "getExecution", - "weight": 301, + "weight": 302, "cookies": false, "type": "", "deprecated": false, @@ -9634,7 +9815,7 @@ }, "x-appwrite": { "method": "deleteExecution", - "weight": 302, + "weight": 303, "cookies": false, "type": "", "deprecated": false, @@ -9705,7 +9886,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 304, + "weight": 305, "cookies": false, "type": "", "deprecated": false, @@ -9764,7 +9945,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 303, + "weight": 304, "cookies": false, "type": "", "deprecated": false, @@ -9819,7 +10000,7 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", + "description": "Secret variables can be updated or deleted, but only functions can read them during build and runtime.", "x-example": false } }, @@ -9855,7 +10036,7 @@ }, "x-appwrite": { "method": "query", - "weight": 326, + "weight": 325, "cookies": false, "type": "graphql", "deprecated": false, @@ -9908,7 +10089,7 @@ }, "x-appwrite": { "method": "mutation", - "weight": 325, + "weight": 324, "cookies": false, "type": "graphql", "deprecated": false, @@ -10010,7 +10191,7 @@ }, "x-appwrite": { "method": "getAntivirus", - "weight": 146, + "weight": 147, "cookies": false, "type": "", "deprecated": false, @@ -10525,7 +10706,7 @@ }, "x-appwrite": { "method": "getFailedJobs", - "weight": 147, + "weight": 148, "cookies": false, "type": "", "deprecated": false, @@ -10564,8 +10745,9 @@ "v1-audits", "v1-mails", "v1-functions", - "v1-usage", - "v1-usage-dump", + "v1-stats-resources", + "v1-stats-usage", + "v1-stats-usage-dump", "v1-webhooks", "v1-certificates", "v1-builds", @@ -10901,14 +11083,14 @@ ] } }, - "\/health\/queue\/usage": { + "\/health\/queue\/stats-resources": { "get": { - "summary": "Get usage queue", - "operationId": "healthGetQueueUsage", + "summary": "Get stats resources queue", + "operationId": "healthGetQueueStatsResources", "tags": [ "health" ], - "description": "Get the number of metrics that are waiting to be processed in the Appwrite internal queue server.", + "description": "Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue.", "responses": { "200": { "description": "Health Queue", @@ -10922,13 +11104,13 @@ } }, "x-appwrite": { - "method": "getQueueUsage", + "method": "getQueueStatsResources", "weight": 142, "cookies": false, "type": "", "deprecated": false, - "demo": "health\/get-queue-usage.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage.md", + "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}", @@ -10963,10 +11145,72 @@ ] } }, - "\/health\/queue\/usage-dump": { + "\/health\/queue\/stats-usage": { + "get": { + "summary": "Get stats usage queue", + "operationId": "healthGetQueueUsage", + "tags": [ + "health" + ], + "description": "Get the number of metrics that are waiting to be processed in the Appwrite internal queue server.", + "responses": { + "200": { + "description": "Health Queue", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/healthQueue" + } + } + } + } + }, + "x-appwrite": { + "method": "getQueueUsage", + "weight": 143, + "cookies": false, + "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", + "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-dump": { "get": { "summary": "Get usage dump queue", - "operationId": "healthGetQueueUsageDump", + "operationId": "healthGetQueueStatsUsageDump", "tags": [ "health" ], @@ -10984,13 +11228,13 @@ } }, "x-appwrite": { - "method": "getQueueUsageDump", - "weight": 143, + "method": "getQueueStatsUsageDump", + "weight": 144, "cookies": false, "type": "", "deprecated": false, - "demo": "health\/get-queue-usage-dump.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage-dump.md", + "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", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", @@ -11109,7 +11353,7 @@ }, "x-appwrite": { "method": "getStorage", - "weight": 145, + "weight": 146, "cookies": false, "type": "", "deprecated": false, @@ -11158,7 +11402,7 @@ }, "x-appwrite": { "method": "getStorageLocal", - "weight": 144, + "weight": 145, "cookies": false, "type": "", "deprecated": false, @@ -11680,7 +11924,7 @@ }, "x-appwrite": { "method": "listMessages", - "weight": 379, + "weight": 378, "cookies": false, "type": "", "deprecated": false, @@ -11756,7 +12000,7 @@ }, "x-appwrite": { "method": "createEmail", - "weight": 376, + "weight": 375, "cookies": false, "type": "", "deprecated": false, @@ -11900,7 +12144,7 @@ }, "x-appwrite": { "method": "updateEmail", - "weight": 383, + "weight": 382, "cookies": false, "type": "", "deprecated": false, @@ -12046,7 +12290,7 @@ }, "x-appwrite": { "method": "createPush", - "weight": 378, + "weight": 377, "cookies": false, "type": "", "deprecated": false, @@ -12220,7 +12464,7 @@ }, "x-appwrite": { "method": "updatePush", - "weight": 385, + "weight": 384, "cookies": false, "type": "", "deprecated": false, @@ -12398,7 +12642,7 @@ }, "x-appwrite": { "method": "createSms", - "weight": 377, + "weight": 376, "cookies": false, "type": "", "deprecated": false, @@ -12507,7 +12751,7 @@ }, "x-appwrite": { "method": "updateSms", - "weight": 384, + "weight": 383, "cookies": false, "type": "", "deprecated": false, @@ -12619,7 +12863,7 @@ }, "x-appwrite": { "method": "getMessage", - "weight": 382, + "weight": 381, "cookies": false, "type": "", "deprecated": false, @@ -12672,7 +12916,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 386, + "weight": 385, "cookies": false, "type": "", "deprecated": false, @@ -12734,7 +12978,7 @@ }, "x-appwrite": { "method": "listMessageLogs", - "weight": 380, + "weight": 379, "cookies": false, "type": "", "deprecated": false, @@ -12809,7 +13053,7 @@ }, "x-appwrite": { "method": "listTargets", - "weight": 381, + "weight": 380, "cookies": false, "type": "", "deprecated": false, @@ -12884,7 +13128,7 @@ }, "x-appwrite": { "method": "listProviders", - "weight": 351, + "weight": 350, "cookies": false, "type": "", "deprecated": false, @@ -12960,7 +13204,7 @@ }, "x-appwrite": { "method": "createApnsProvider", - "weight": 350, + "weight": 349, "cookies": false, "type": "", "deprecated": false, @@ -13065,7 +13309,7 @@ }, "x-appwrite": { "method": "updateApnsProvider", - "weight": 363, + "weight": 362, "cookies": false, "type": "", "deprecated": false, @@ -13173,7 +13417,7 @@ }, "x-appwrite": { "method": "createFcmProvider", - "weight": 349, + "weight": 348, "cookies": false, "type": "", "deprecated": false, @@ -13258,7 +13502,7 @@ }, "x-appwrite": { "method": "updateFcmProvider", - "weight": 362, + "weight": 361, "cookies": false, "type": "", "deprecated": false, @@ -13346,7 +13590,7 @@ }, "x-appwrite": { "method": "createMailgunProvider", - "weight": 341, + "weight": 340, "cookies": false, "type": "", "deprecated": false, @@ -13461,7 +13705,7 @@ }, "x-appwrite": { "method": "updateMailgunProvider", - "weight": 354, + "weight": 353, "cookies": false, "type": "", "deprecated": false, @@ -13579,7 +13823,7 @@ }, "x-appwrite": { "method": "createMsg91Provider", - "weight": 344, + "weight": 343, "cookies": false, "type": "", "deprecated": false, @@ -13674,7 +13918,7 @@ }, "x-appwrite": { "method": "updateMsg91Provider", - "weight": 357, + "weight": 356, "cookies": false, "type": "", "deprecated": false, @@ -13772,7 +14016,7 @@ }, "x-appwrite": { "method": "createSendgridProvider", - "weight": 342, + "weight": 341, "cookies": false, "type": "", "deprecated": false, @@ -13877,7 +14121,7 @@ }, "x-appwrite": { "method": "updateSendgridProvider", - "weight": 355, + "weight": 354, "cookies": false, "type": "", "deprecated": false, @@ -13985,7 +14229,7 @@ }, "x-appwrite": { "method": "createSmtpProvider", - "weight": 343, + "weight": 342, "cookies": false, "type": "", "deprecated": false, @@ -14128,7 +14372,7 @@ }, "x-appwrite": { "method": "updateSmtpProvider", - "weight": 356, + "weight": 355, "cookies": false, "type": "", "deprecated": false, @@ -14273,7 +14517,7 @@ }, "x-appwrite": { "method": "createTelesignProvider", - "weight": 345, + "weight": 344, "cookies": false, "type": "", "deprecated": false, @@ -14368,7 +14612,7 @@ }, "x-appwrite": { "method": "updateTelesignProvider", - "weight": 358, + "weight": 357, "cookies": false, "type": "", "deprecated": false, @@ -14466,7 +14710,7 @@ }, "x-appwrite": { "method": "createTextmagicProvider", - "weight": 346, + "weight": 345, "cookies": false, "type": "", "deprecated": false, @@ -14561,7 +14805,7 @@ }, "x-appwrite": { "method": "updateTextmagicProvider", - "weight": 359, + "weight": 358, "cookies": false, "type": "", "deprecated": false, @@ -14659,7 +14903,7 @@ }, "x-appwrite": { "method": "createTwilioProvider", - "weight": 347, + "weight": 346, "cookies": false, "type": "", "deprecated": false, @@ -14754,7 +14998,7 @@ }, "x-appwrite": { "method": "updateTwilioProvider", - "weight": 360, + "weight": 359, "cookies": false, "type": "", "deprecated": false, @@ -14852,7 +15096,7 @@ }, "x-appwrite": { "method": "createVonageProvider", - "weight": 348, + "weight": 347, "cookies": false, "type": "", "deprecated": false, @@ -14947,7 +15191,7 @@ }, "x-appwrite": { "method": "updateVonageProvider", - "weight": 361, + "weight": 360, "cookies": false, "type": "", "deprecated": false, @@ -15045,7 +15289,7 @@ }, "x-appwrite": { "method": "getProvider", - "weight": 353, + "weight": 352, "cookies": false, "type": "", "deprecated": false, @@ -15098,7 +15342,7 @@ }, "x-appwrite": { "method": "deleteProvider", - "weight": 364, + "weight": 363, "cookies": false, "type": "", "deprecated": false, @@ -15160,7 +15404,7 @@ }, "x-appwrite": { "method": "listProviderLogs", - "weight": 352, + "weight": 351, "cookies": false, "type": "", "deprecated": false, @@ -15235,7 +15479,7 @@ }, "x-appwrite": { "method": "listSubscriberLogs", - "weight": 373, + "weight": 372, "cookies": false, "type": "", "deprecated": false, @@ -15310,7 +15554,7 @@ }, "x-appwrite": { "method": "listTopics", - "weight": 366, + "weight": 365, "cookies": false, "type": "", "deprecated": false, @@ -15384,7 +15628,7 @@ }, "x-appwrite": { "method": "createTopic", - "weight": 365, + "weight": 364, "cookies": false, "type": "", "deprecated": false, @@ -15467,7 +15711,7 @@ }, "x-appwrite": { "method": "getTopic", - "weight": 368, + "weight": 367, "cookies": false, "type": "", "deprecated": false, @@ -15527,7 +15771,7 @@ }, "x-appwrite": { "method": "updateTopic", - "weight": 369, + "weight": 368, "cookies": false, "type": "", "deprecated": false, @@ -15604,7 +15848,7 @@ }, "x-appwrite": { "method": "deleteTopic", - "weight": 370, + "weight": 369, "cookies": false, "type": "", "deprecated": false, @@ -15666,7 +15910,7 @@ }, "x-appwrite": { "method": "listTopicLogs", - "weight": 367, + "weight": 366, "cookies": false, "type": "", "deprecated": false, @@ -15741,7 +15985,7 @@ }, "x-appwrite": { "method": "listSubscribers", - "weight": 372, + "weight": 371, "cookies": false, "type": "", "deprecated": false, @@ -15825,7 +16069,7 @@ }, "x-appwrite": { "method": "createSubscriber", - "weight": 371, + "weight": 370, "cookies": false, "type": "", "deprecated": false, @@ -15916,7 +16160,7 @@ }, "x-appwrite": { "method": "getSubscriber", - "weight": 374, + "weight": 373, "cookies": false, "type": "", "deprecated": false, @@ -15979,7 +16223,7 @@ }, "x-appwrite": { "method": "deleteSubscriber", - "weight": 375, + "weight": 374, "cookies": false, "type": "", "deprecated": false, @@ -16055,7 +16299,7 @@ }, "x-appwrite": { "method": "list", - "weight": 394, + "weight": 395, "cookies": false, "type": "", "deprecated": false, @@ -16125,7 +16369,7 @@ }, "x-appwrite": { "method": "create", - "weight": 392, + "weight": 393, "cookies": false, "type": "", "deprecated": false, @@ -16172,7 +16416,9 @@ "x-example": "nextjs", "enum": [ "nextjs", + "react", "nuxt", + "vue", "sveltekit", "astro", "remix", @@ -16207,11 +16453,6 @@ "description": "Output Directory for site.", "x-example": "" }, - "subdomain": { - "type": "string", - "description": "Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.", - "x-example": "" - }, "buildRuntime": { "type": "string", "description": "Runtime to use during build step.", @@ -16285,8 +16526,14 @@ }, "adapter": { "type": "string", - "description": "Framework adapter. Allows: static, ssr", - "x-example": "" + "description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr", + "x-example": "static", + "enum": [ + "static", + "ssr" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "installationId": { "type": "string", @@ -16318,26 +16565,6 @@ "description": "Path to site code in the linked repo.", "x-example": "" }, - "templateRepository": { - "type": "string", - "description": "Repository name of the template.", - "x-example": "" - }, - "templateOwner": { - "type": "string", - "description": "The name of the owner of the template.", - "x-example": "" - }, - "templateRootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "x-example": "" - }, - "templateVersion": { - "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "x-example": "" - }, "specification": { "type": "string", "description": "Framework specification for the site and builds.", @@ -16378,7 +16605,7 @@ }, "x-appwrite": { "method": "listFrameworks", - "weight": 397, + "weight": 398, "cookies": false, "type": "", "deprecated": false, @@ -16427,7 +16654,7 @@ }, "x-appwrite": { "method": "get", - "weight": 393, + "weight": 394, "cookies": false, "type": "", "deprecated": false, @@ -16486,7 +16713,7 @@ }, "x-appwrite": { "method": "update", - "weight": 395, + "weight": 396, "cookies": false, "type": "", "deprecated": false, @@ -16540,7 +16767,9 @@ "x-example": "nextjs", "enum": [ "nextjs", + "react", "nuxt", + "vue", "sveltekit", "astro", "remix", @@ -16648,8 +16877,14 @@ }, "adapter": { "type": "string", - "description": "Framework adapter. Usuallly allows: static, ssr", - "x-example": "" + "description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr", + "x-example": "static", + "enum": [ + "static", + "ssr" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "fallbackFile": { "type": "string", @@ -16710,7 +16945,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 396, + "weight": 397, "cookies": false, "type": "", "deprecated": false, @@ -16771,7 +17006,7 @@ }, "x-appwrite": { "method": "listDeployments", - "weight": 400, + "weight": 403, "cookies": false, "type": "", "deprecated": false, @@ -16854,7 +17089,7 @@ }, "x-appwrite": { "method": "createDeployment", - "weight": 398, + "weight": 399, "cookies": false, "type": "upload", "deprecated": false, @@ -16933,6 +17168,205 @@ } } }, + "\/sites\/{siteId}\/deployments\/template": { + "post": { + "summary": "Create deployment", + "operationId": "sitesCreateTemplateDeployment", + "tags": [ + "sites" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "x-appwrite": { + "method": "createTemplateDeployment", + "weight": 400, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "sites\/create-template-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment based on a template.\n\nUse this endpoint with combination of [listTemplates](https:\/\/appwrite.io\/docs\/server\/sites#listTemplates) to find the template details.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "siteId", + "description": "Site ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "Repository name of the template.", + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "x-example": "" + }, + "version": { + "type": "string", + "description": "Version (tag) for the repo linked to the site template.", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "repository", + "owner", + "rootDirectory", + "version" + ] + } + } + } + } + } + }, + "\/sites\/{siteId}\/deployments\/vcs": { + "post": { + "summary": "Create VCS deployment", + "operationId": "sitesCreateVcsDeployment", + "tags": [ + "sites" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "x-appwrite": { + "method": "createVcsDeployment", + "weight": 401, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "sites\/create-vcs-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment when a site is connected to VCS.\n\nThis endpoint lets you create deployment from a branch, commit, or a tag.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "siteId", + "description": "Site ID.", + "required": true, + "schema": { + "type": "string", + "x-example": "" + }, + "in": "path" + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of reference passed. Allowed values are: branch", + "x-example": "branch", + "enum": [ + "branch" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "VCS reference to create deployment from. Depending on type this can be: branch name", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "type", + "reference" + ] + } + } + } + } + } + }, "\/sites\/{siteId}\/deployments\/{deploymentId}": { "get": { "summary": "Get deployment", @@ -16955,7 +17389,7 @@ }, "x-appwrite": { "method": "getDeployment", - "weight": 399, + "weight": 402, "cookies": false, "type": "", "deprecated": false, @@ -17024,7 +17458,7 @@ }, "x-appwrite": { "method": "updateDeployment", - "weight": 401, + "weight": 404, "cookies": false, "type": "", "deprecated": false, @@ -17086,7 +17520,7 @@ }, "x-appwrite": { "method": "deleteDeployment", - "weight": 402, + "weight": 405, "cookies": false, "type": "", "deprecated": false, @@ -17150,7 +17584,7 @@ }, "x-appwrite": { "method": "createDeploymentBuild", - "weight": 405, + "weight": 408, "cookies": false, "type": "", "deprecated": false, @@ -17219,7 +17653,7 @@ }, "x-appwrite": { "method": "updateDeploymentBuild", - "weight": 406, + "weight": 409, "cookies": false, "type": "", "deprecated": false, @@ -17283,7 +17717,7 @@ }, "x-appwrite": { "method": "getDeploymentBuildDownload", - "weight": 404, + "weight": 407, "cookies": false, "type": "location", "deprecated": false, @@ -17349,7 +17783,7 @@ }, "x-appwrite": { "method": "getDeploymentDownload", - "weight": 403, + "weight": 406, "cookies": false, "type": "location", "deprecated": false, @@ -17422,7 +17856,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 408, + "weight": 411, "cookies": false, "type": "", "deprecated": false, @@ -17504,7 +17938,7 @@ }, "x-appwrite": { "method": "getLog", - "weight": 407, + "weight": 410, "cookies": false, "type": "", "deprecated": false, @@ -17566,7 +18000,7 @@ }, "x-appwrite": { "method": "deleteLog", - "weight": 409, + "weight": 412, "cookies": false, "type": "", "deprecated": false, @@ -17637,7 +18071,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 412, + "weight": 415, "cookies": false, "type": "", "deprecated": false, @@ -17696,7 +18130,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 410, + "weight": 413, "cookies": false, "type": "", "deprecated": false, @@ -17751,7 +18185,7 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", + "description": "Secret variables can be updated or deleted, but only sites can read them during build and runtime.", "x-example": false } }, @@ -17787,7 +18221,7 @@ }, "x-appwrite": { "method": "getVariable", - "weight": 411, + "weight": 414, "cookies": false, "type": "", "deprecated": false, @@ -17856,7 +18290,7 @@ }, "x-appwrite": { "method": "updateVariable", - "weight": 413, + "weight": 416, "cookies": false, "type": "", "deprecated": false, @@ -17918,6 +18352,11 @@ "type": "string", "description": "Variable value. Max length: 8192 chars.", "x-example": "" + }, + "secret": { + "type": "boolean", + "description": "Secret variables can be updated or deleted, but only sites can read them during build and runtime.", + "x-example": false } }, "required": [ @@ -17942,7 +18381,7 @@ }, "x-appwrite": { "method": "deleteVariable", - "weight": 414, + "weight": 417, "cookies": false, "type": "", "deprecated": false, @@ -18013,7 +18452,7 @@ }, "x-appwrite": { "method": "listBuckets", - "weight": 202, + "weight": 203, "cookies": false, "type": "", "deprecated": false, @@ -18086,7 +18525,7 @@ }, "x-appwrite": { "method": "createBucket", - "weight": 201, + "weight": 202, "cookies": false, "type": "", "deprecated": false, @@ -18213,7 +18652,7 @@ }, "x-appwrite": { "method": "getBucket", - "weight": 203, + "weight": 204, "cookies": false, "type": "", "deprecated": false, @@ -18272,7 +18711,7 @@ }, "x-appwrite": { "method": "updateBucket", - "weight": 204, + "weight": 205, "cookies": false, "type": "", "deprecated": false, @@ -18396,7 +18835,7 @@ }, "x-appwrite": { "method": "deleteBucket", - "weight": 205, + "weight": 206, "cookies": false, "type": "", "deprecated": false, @@ -18457,7 +18896,7 @@ }, "x-appwrite": { "method": "listFiles", - "weight": 207, + "weight": 208, "cookies": false, "type": "", "deprecated": false, @@ -18544,7 +18983,7 @@ }, "x-appwrite": { "method": "createFile", - "weight": 206, + "weight": 207, "cookies": false, "type": "upload", "deprecated": false, @@ -18643,7 +19082,7 @@ }, "x-appwrite": { "method": "getFile", - "weight": 208, + "weight": 209, "cookies": false, "type": "", "deprecated": false, @@ -18716,7 +19155,7 @@ }, "x-appwrite": { "method": "updateFile", - "weight": 213, + "weight": 214, "cookies": false, "type": "", "deprecated": false, @@ -18806,7 +19245,7 @@ }, "x-appwrite": { "method": "deleteFile", - "weight": 214, + "weight": 215, "cookies": false, "type": "", "deprecated": false, @@ -18874,7 +19313,7 @@ }, "x-appwrite": { "method": "getFileDownload", - "weight": 210, + "weight": 211, "cookies": false, "type": "location", "deprecated": false, @@ -18942,7 +19381,7 @@ }, "x-appwrite": { "method": "getFilePreview", - "weight": 209, + "weight": 210, "cookies": false, "type": "location", "deprecated": false, @@ -19160,7 +19599,7 @@ }, "x-appwrite": { "method": "getFileView", - "weight": 211, + "weight": 212, "cookies": false, "type": "location", "deprecated": false, @@ -19235,7 +19674,7 @@ }, "x-appwrite": { "method": "list", - "weight": 218, + "weight": 219, "cookies": false, "type": "", "deprecated": false, @@ -19312,7 +19751,7 @@ }, "x-appwrite": { "method": "create", - "weight": 217, + "weight": 218, "cookies": false, "type": "", "deprecated": false, @@ -19398,7 +19837,7 @@ }, "x-appwrite": { "method": "get", - "weight": 219, + "weight": 220, "cookies": false, "type": "", "deprecated": false, @@ -19461,7 +19900,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 221, + "weight": 222, "cookies": false, "type": "", "deprecated": false, @@ -19536,7 +19975,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 223, + "weight": 224, "cookies": false, "type": "", "deprecated": false, @@ -19601,7 +20040,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 225, + "weight": 226, "cookies": false, "type": "", "deprecated": false, @@ -19688,7 +20127,7 @@ }, "x-appwrite": { "method": "createMembership", - "weight": 224, + "weight": 225, "cookies": false, "type": "", "deprecated": false, @@ -19800,7 +20239,7 @@ }, "x-appwrite": { "method": "getMembership", - "weight": 226, + "weight": 227, "cookies": false, "type": "", "deprecated": false, @@ -19873,7 +20312,7 @@ }, "x-appwrite": { "method": "updateMembership", - "weight": 227, + "weight": 228, "cookies": false, "type": "", "deprecated": false, @@ -19961,7 +20400,7 @@ }, "x-appwrite": { "method": "deleteMembership", - "weight": 229, + "weight": 230, "cookies": false, "type": "", "deprecated": false, @@ -20036,7 +20475,7 @@ }, "x-appwrite": { "method": "updateMembershipStatus", - "weight": 228, + "weight": 229, "cookies": false, "type": "", "deprecated": false, @@ -20134,7 +20573,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 220, + "weight": 221, "cookies": false, "type": "", "deprecated": false, @@ -20195,7 +20634,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 222, + "weight": 223, "cookies": false, "type": "", "deprecated": false, @@ -20277,7 +20716,7 @@ }, "x-appwrite": { "method": "list", - "weight": 240, + "weight": 241, "cookies": false, "type": "", "deprecated": false, @@ -20350,7 +20789,7 @@ }, "x-appwrite": { "method": "create", - "weight": 231, + "weight": 232, "cookies": false, "type": "", "deprecated": false, @@ -20438,7 +20877,7 @@ }, "x-appwrite": { "method": "createArgon2User", - "weight": 234, + "weight": 235, "cookies": false, "type": "", "deprecated": false, @@ -20523,7 +20962,7 @@ }, "x-appwrite": { "method": "createBcryptUser", - "weight": 232, + "weight": 233, "cookies": false, "type": "", "deprecated": false, @@ -20608,7 +21047,7 @@ }, "x-appwrite": { "method": "listIdentities", - "weight": 248, + "weight": 249, "cookies": false, "type": "", "deprecated": false, @@ -20676,7 +21115,7 @@ }, "x-appwrite": { "method": "deleteIdentity", - "weight": 271, + "weight": 272, "cookies": false, "type": "", "deprecated": false, @@ -20737,7 +21176,7 @@ }, "x-appwrite": { "method": "createMD5User", - "weight": 233, + "weight": 234, "cookies": false, "type": "", "deprecated": false, @@ -20822,7 +21261,7 @@ }, "x-appwrite": { "method": "createPHPassUser", - "weight": 236, + "weight": 237, "cookies": false, "type": "", "deprecated": false, @@ -20907,7 +21346,7 @@ }, "x-appwrite": { "method": "createScryptUser", - "weight": 237, + "weight": 238, "cookies": false, "type": "", "deprecated": false, @@ -21022,7 +21461,7 @@ }, "x-appwrite": { "method": "createScryptModifiedUser", - "weight": 238, + "weight": 239, "cookies": false, "type": "", "deprecated": false, @@ -21125,7 +21564,7 @@ }, "x-appwrite": { "method": "createSHAUser", - "weight": 235, + "weight": 236, "cookies": false, "type": "", "deprecated": false, @@ -21230,7 +21669,7 @@ }, "x-appwrite": { "method": "get", - "weight": 241, + "weight": 242, "cookies": false, "type": "", "deprecated": false, @@ -21282,7 +21721,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 269, + "weight": 270, "cookies": false, "type": "", "deprecated": false, @@ -21343,7 +21782,7 @@ }, "x-appwrite": { "method": "updateEmail", - "weight": 254, + "weight": 255, "cookies": false, "type": "", "deprecated": false, @@ -21423,7 +21862,7 @@ }, "x-appwrite": { "method": "createJWT", - "weight": 272, + "weight": 273, "cookies": false, "type": "", "deprecated": false, @@ -21505,7 +21944,7 @@ }, "x-appwrite": { "method": "updateLabels", - "weight": 250, + "weight": 251, "cookies": false, "type": "", "deprecated": false, @@ -21588,7 +22027,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 246, + "weight": 247, "cookies": false, "type": "", "deprecated": false, @@ -21662,7 +22101,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 245, + "weight": 246, "cookies": false, "type": "", "deprecated": false, @@ -21723,7 +22162,7 @@ }, "x-appwrite": { "method": "updateMfa", - "weight": 259, + "weight": 260, "cookies": false, "type": "", "deprecated": false, @@ -21796,7 +22235,7 @@ }, "x-appwrite": { "method": "deleteMfaAuthenticator", - "weight": 264, + "weight": 265, "cookies": false, "type": "", "deprecated": false, @@ -21872,7 +22311,7 @@ }, "x-appwrite": { "method": "listMfaFactors", - "weight": 260, + "weight": 261, "cookies": false, "type": "", "deprecated": false, @@ -21933,7 +22372,7 @@ }, "x-appwrite": { "method": "getMfaRecoveryCodes", - "weight": 261, + "weight": 262, "cookies": false, "type": "", "deprecated": false, @@ -21992,7 +22431,7 @@ }, "x-appwrite": { "method": "updateMfaRecoveryCodes", - "weight": 263, + "weight": 264, "cookies": false, "type": "", "deprecated": false, @@ -22051,7 +22490,7 @@ }, "x-appwrite": { "method": "createMfaRecoveryCodes", - "weight": 262, + "weight": 263, "cookies": false, "type": "", "deprecated": false, @@ -22112,7 +22551,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 252, + "weight": 253, "cookies": false, "type": "", "deprecated": false, @@ -22192,7 +22631,7 @@ }, "x-appwrite": { "method": "updatePassword", - "weight": 253, + "weight": 254, "cookies": false, "type": "", "deprecated": false, @@ -22272,7 +22711,7 @@ }, "x-appwrite": { "method": "updatePhone", - "weight": 255, + "weight": 256, "cookies": false, "type": "", "deprecated": false, @@ -22352,7 +22791,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 242, + "weight": 243, "cookies": false, "type": "", "deprecated": false, @@ -22411,7 +22850,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 257, + "weight": 258, "cookies": false, "type": "", "deprecated": false, @@ -22491,7 +22930,7 @@ }, "x-appwrite": { "method": "listSessions", - "weight": 244, + "weight": 245, "cookies": false, "type": "", "deprecated": false, @@ -22550,7 +22989,7 @@ }, "x-appwrite": { "method": "createSession", - "weight": 265, + "weight": 266, "cookies": false, "type": "", "deprecated": false, @@ -22602,7 +23041,7 @@ }, "x-appwrite": { "method": "deleteSessions", - "weight": 268, + "weight": 269, "cookies": false, "type": "", "deprecated": false, @@ -22656,7 +23095,7 @@ }, "x-appwrite": { "method": "deleteSession", - "weight": 267, + "weight": 268, "cookies": false, "type": "", "deprecated": false, @@ -22727,7 +23166,7 @@ }, "x-appwrite": { "method": "updateStatus", - "weight": 249, + "weight": 250, "cookies": false, "type": "", "deprecated": false, @@ -22807,7 +23246,7 @@ }, "x-appwrite": { "method": "listTargets", - "weight": 247, + "weight": 248, "cookies": false, "type": "", "deprecated": false, @@ -22880,7 +23319,7 @@ }, "x-appwrite": { "method": "createTarget", - "weight": 239, + "weight": 240, "cookies": false, "type": "", "deprecated": false, @@ -22990,7 +23429,7 @@ }, "x-appwrite": { "method": "getTarget", - "weight": 243, + "weight": 244, "cookies": false, "type": "", "deprecated": false, @@ -23060,7 +23499,7 @@ }, "x-appwrite": { "method": "updateTarget", - "weight": 258, + "weight": 259, "cookies": false, "type": "", "deprecated": false, @@ -23149,7 +23588,7 @@ }, "x-appwrite": { "method": "deleteTarget", - "weight": 270, + "weight": 271, "cookies": false, "type": "", "deprecated": false, @@ -23221,7 +23660,7 @@ }, "x-appwrite": { "method": "createToken", - "weight": 266, + "weight": 267, "cookies": false, "type": "", "deprecated": false, @@ -23303,7 +23742,7 @@ }, "x-appwrite": { "method": "updateEmailVerification", - "weight": 256, + "weight": 257, "cookies": false, "type": "", "deprecated": false, @@ -23383,7 +23822,7 @@ }, "x-appwrite": { "method": "updatePhoneVerification", - "weight": 251, + "weight": 252, "cookies": false, "type": "", "deprecated": false, @@ -27035,6 +27474,16 @@ "description": "Whether the deployment should be automatically activated.", "x-example": true }, + "screenshotLight": { + "type": "string", + "description": "Screenshot with light theme preference file ID.", + "x-example": "5e5ea5c16897e" + }, + "screenshotDark": { + "type": "string", + "description": "Screenshot with dark theme preference file ID.", + "x-example": "5e5ea5c16897e" + }, "status": { "type": "string", "description": "The deployment status. Possible values are \"processing\", \"building\", \"waiting\", \"ready\", and \"failed\".", @@ -27051,11 +27500,6 @@ "x-example": 128, "format": "int32" }, - "domain": { - "type": "string", - "description": "Preview domain.", - "x-example": "deploy1-project1.appwrite.site" - }, "providerRepositoryName": { "type": "string", "description": "The name of the vcs provider repository", @@ -27119,10 +27563,11 @@ "buildSize", "buildId", "activate", + "screenshotLight", + "screenshotDark", "status", "buildLogs", "buildTime", - "domain", "providerRepositoryName", "providerRepositoryOwner", "providerRepositoryUrl", diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index 1ee6677c92..f646dbd81a 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -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.", + "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.", "required": true, "type": "string", "x-example": "amex", @@ -3577,7 +3577,8 @@ "union-china-pay", "visa", "mir", - "maestro" + "maestro", + "rupay" ], "x-enum-name": "CreditCard", "x-enum-keys": [ @@ -3596,7 +3597,8 @@ "Union China Pay", "Visa", "MIR", - "Maestro" + "Maestro", + "Rupay" ], "in": "path" }, @@ -4547,7 +4549,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.", + "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", "responses": { "201": { "description": "Document", @@ -4927,7 +4929,7 @@ }, "x-appwrite": { "method": "listExecutions", - "weight": 300, + "weight": 301, "cookies": false, "type": "", "deprecated": false, @@ -5009,7 +5011,7 @@ }, "x-appwrite": { "method": "createExecution", - "weight": 299, + "weight": 300, "cookies": false, "type": "", "deprecated": false, @@ -5127,7 +5129,7 @@ }, "x-appwrite": { "method": "getExecution", - "weight": 301, + "weight": 302, "cookies": false, "type": "", "deprecated": false, @@ -5198,7 +5200,7 @@ }, "x-appwrite": { "method": "query", - "weight": 326, + "weight": 325, "cookies": false, "type": "graphql", "deprecated": false, @@ -5271,7 +5273,7 @@ }, "x-appwrite": { "method": "mutation", - "weight": 325, + "weight": 324, "cookies": false, "type": "graphql", "deprecated": false, @@ -5768,7 +5770,7 @@ }, "x-appwrite": { "method": "createSubscriber", - "weight": 371, + "weight": 370, "cookies": false, "type": "", "deprecated": false, @@ -5852,7 +5854,7 @@ }, "x-appwrite": { "method": "deleteSubscriber", - "weight": 375, + "weight": 374, "cookies": false, "type": "", "deprecated": false, @@ -5924,7 +5926,7 @@ }, "x-appwrite": { "method": "listFiles", - "weight": 207, + "weight": 208, "cookies": false, "type": "", "deprecated": false, @@ -6006,7 +6008,7 @@ }, "x-appwrite": { "method": "createFile", - "weight": 206, + "weight": 207, "cookies": false, "type": "upload", "deprecated": false, @@ -6097,7 +6099,7 @@ }, "x-appwrite": { "method": "getFile", - "weight": 208, + "weight": 209, "cookies": false, "type": "", "deprecated": false, @@ -6166,7 +6168,7 @@ }, "x-appwrite": { "method": "updateFile", - "weight": 213, + "weight": 214, "cookies": false, "type": "", "deprecated": false, @@ -6254,7 +6256,7 @@ }, "x-appwrite": { "method": "deleteFile", - "weight": 214, + "weight": 215, "cookies": false, "type": "", "deprecated": false, @@ -6325,7 +6327,7 @@ }, "x-appwrite": { "method": "getFileDownload", - "weight": 210, + "weight": 211, "cookies": false, "type": "location", "deprecated": false, @@ -6396,7 +6398,7 @@ }, "x-appwrite": { "method": "getFilePreview", - "weight": 209, + "weight": 210, "cookies": false, "type": "location", "deprecated": false, @@ -6595,7 +6597,7 @@ }, "x-appwrite": { "method": "getFileView", - "weight": 211, + "weight": 212, "cookies": false, "type": "location", "deprecated": false, @@ -6666,7 +6668,7 @@ }, "x-appwrite": { "method": "list", - "weight": 218, + "weight": 219, "cookies": false, "type": "", "deprecated": false, @@ -6740,7 +6742,7 @@ }, "x-appwrite": { "method": "create", - "weight": 217, + "weight": 218, "cookies": false, "type": "", "deprecated": false, @@ -6831,7 +6833,7 @@ }, "x-appwrite": { "method": "get", - "weight": 219, + "weight": 220, "cookies": false, "type": "", "deprecated": false, @@ -6892,7 +6894,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 221, + "weight": 222, "cookies": false, "type": "", "deprecated": false, @@ -6966,7 +6968,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 223, + "weight": 224, "cookies": false, "type": "", "deprecated": false, @@ -7029,7 +7031,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 225, + "weight": 226, "cookies": false, "type": "", "deprecated": false, @@ -7111,7 +7113,7 @@ }, "x-appwrite": { "method": "createMembership", - "weight": 224, + "weight": 225, "cookies": false, "type": "", "deprecated": false, @@ -7225,7 +7227,7 @@ }, "x-appwrite": { "method": "getMembership", - "weight": 226, + "weight": 227, "cookies": false, "type": "", "deprecated": false, @@ -7294,7 +7296,7 @@ }, "x-appwrite": { "method": "updateMembership", - "weight": 227, + "weight": 228, "cookies": false, "type": "", "deprecated": false, @@ -7379,7 +7381,7 @@ }, "x-appwrite": { "method": "deleteMembership", - "weight": 229, + "weight": 230, "cookies": false, "type": "", "deprecated": false, @@ -7450,7 +7452,7 @@ }, "x-appwrite": { "method": "updateMembershipStatus", - "weight": 228, + "weight": 229, "cookies": false, "type": "", "deprecated": false, @@ -7545,7 +7547,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 220, + "weight": 221, "cookies": false, "type": "", "deprecated": false, @@ -7605,7 +7607,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 222, + "weight": 223, "cookies": false, "type": "", "deprecated": false, diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index e1b2a4da74..8b6dfb954d 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -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.", + "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.", "required": true, "type": "string", "x-example": "amex", @@ -3597,7 +3597,8 @@ "union-china-pay", "visa", "mir", - "maestro" + "maestro", + "rupay" ], "x-enum-name": "CreditCard", "x-enum-keys": [ @@ -3616,7 +3617,8 @@ "Union China Pay", "Visa", "MIR", - "Maestro" + "Maestro", + "Rupay" ], "in": "path" }, @@ -4497,7 +4499,7 @@ }, "x-appwrite": { "method": "chat", - "weight": 328, + "weight": 327, "cookies": false, "type": "", "deprecated": false, @@ -4542,6 +4544,73 @@ ] } }, + "\/console\/resources": { + "get": { + "summary": "Check resource ID availability", + "operationId": "consoleGetResource", + "consumes": [ + "application\/json" + ], + "produces": [], + "tags": [ + "console" + ], + "description": "", + "responses": { + "204": { + "description": "No content" + } + }, + "x-appwrite": { + "method": "getResource", + "weight": 422, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "console\/get-resource.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCheck if a resource ID is available.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.read", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "parameters": [ + { + "name": "value", + "description": "Resource value.", + "required": true, + "type": "string", + "x-example": "", + "in": "query" + }, + { + "name": "type", + "description": "Resource type.", + "required": true, + "type": "string", + "x-example": "rules", + "enum": [ + "rules" + ], + "x-enum-name": "ConsoleResourceType", + "x-enum-keys": [], + "in": "query" + } + ] + } + }, "\/console\/variables": { "get": { "summary": "Get variables", @@ -4566,7 +4635,7 @@ }, "x-appwrite": { "method": "variables", - "weight": 327, + "weight": 326, "cookies": false, "type": "", "deprecated": false, @@ -8012,7 +8081,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.", + "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", "responses": { "201": { "description": "Document", @@ -9135,7 +9204,7 @@ }, "x-appwrite": { "method": "list", - "weight": 389, + "weight": 388, "cookies": false, "type": "", "deprecated": false, @@ -9206,7 +9275,7 @@ }, "x-appwrite": { "method": "create", - "weight": 387, + "weight": 386, "cookies": false, "type": "", "deprecated": false, @@ -9414,30 +9483,6 @@ "default": "", "x-example": "" }, - "templateRepository": { - "type": "string", - "description": "Repository name of the template.", - "default": "", - "x-example": "" - }, - "templateOwner": { - "type": "string", - "description": "The name of the owner of the template.", - "default": "", - "x-example": "" - }, - "templateRootDirectory": { - "type": "string", - "description": "Path to function code in the template repo.", - "default": "", - "x-example": "" - }, - "templateVersion": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "specification": { "type": "string", "description": "Runtime specification for the function and builds.", @@ -9479,7 +9524,7 @@ }, "x-appwrite": { "method": "listRuntimes", - "weight": 390, + "weight": 389, "cookies": false, "type": "", "deprecated": false, @@ -9529,7 +9574,7 @@ }, "x-appwrite": { "method": "listSpecifications", - "weight": 287, + "weight": 288, "cookies": false, "type": "", "deprecated": false, @@ -9580,7 +9625,7 @@ }, "x-appwrite": { "method": "listTemplates", - "weight": 308, + "weight": 309, "cookies": false, "type": "", "deprecated": false, @@ -9675,7 +9720,7 @@ }, "x-appwrite": { "method": "getTemplate", - "weight": 309, + "weight": 310, "cookies": false, "type": "", "deprecated": false, @@ -9734,7 +9779,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 290, + "weight": 291, "cookies": false, "type": "", "deprecated": false, @@ -9805,7 +9850,7 @@ }, "x-appwrite": { "method": "get", - "weight": 288, + "weight": 289, "cookies": false, "type": "", "deprecated": false, @@ -9863,7 +9908,7 @@ }, "x-appwrite": { "method": "update", - "weight": 388, + "weight": 387, "cookies": false, "type": "", "deprecated": false, @@ -10106,7 +10151,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 293, + "weight": 294, "cookies": false, "type": "", "deprecated": false, @@ -10166,7 +10211,7 @@ }, "x-appwrite": { "method": "listDeployments", - "weight": 294, + "weight": 295, "cookies": false, "type": "", "deprecated": false, @@ -10245,7 +10290,7 @@ }, "x-appwrite": { "method": "createDeployment", - "weight": 391, + "weight": 390, "cookies": false, "type": "upload", "deprecated": false, @@ -10312,6 +10357,207 @@ ] } }, + "\/functions\/{functionId}\/deployments\/template": { + "post": { + "summary": "Create template deployment", + "operationId": "functionsCreateTemplateDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "functions" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "x-appwrite": { + "method": "createTemplateDeployment", + "weight": 391, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "functions\/create-template-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment based on a template.\n\nUse this endpoint with combination of [listTemplates](https:\/\/appwrite.io\/docs\/server\/functions#listTemplates) to find the template details.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "functions.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "functionId", + "description": "Function ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "Repository name of the template.", + "default": null, + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "default": null, + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to function code in the template repo.", + "default": null, + "x-example": "" + }, + "version": { + "type": "string", + "description": "Version (tag) for the repo linked to the function template.", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": false, + "x-example": false + } + }, + "required": [ + "repository", + "owner", + "rootDirectory", + "version" + ] + } + } + ] + } + }, + "\/functions\/{functionId}\/deployments\/vcs": { + "post": { + "summary": "Create VCS deployment", + "operationId": "functionsCreateVcsDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "functions" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "x-appwrite": { + "method": "createVcsDeployment", + "weight": 392, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "functions\/create-vcs-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment when a function is connected to VCS.\n\nThis endpoint lets you create deployment from a branch, commit, or a tag.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "functions.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "functionId", + "description": "Function ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of reference passed. Allowed values are: branch", + "default": null, + "x-example": "branch", + "enum": [ + "branch" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "VCS reference to create deployment from. Depending on type this can be: branch name", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": false, + "x-example": false + } + }, + "required": [ + "type", + "reference" + ] + } + } + ] + } + }, "\/functions\/{functionId}\/deployments\/{deploymentId}": { "get": { "summary": "Get deployment", @@ -10336,7 +10582,7 @@ }, "x-appwrite": { "method": "getDeployment", - "weight": 295, + "weight": 296, "cookies": false, "type": "", "deprecated": false, @@ -10397,7 +10643,7 @@ }, "x-appwrite": { "method": "deleteDeployment", - "weight": 296, + "weight": 297, "cookies": false, "type": "", "deprecated": false, @@ -10462,7 +10708,7 @@ }, "x-appwrite": { "method": "createBuild", - "weight": 297, + "weight": 298, "cookies": false, "type": "", "deprecated": false, @@ -10545,7 +10791,7 @@ }, "x-appwrite": { "method": "getDeploymentDownload", - "weight": 291, + "weight": 292, "cookies": false, "type": "location", "deprecated": false, @@ -10615,7 +10861,7 @@ }, "x-appwrite": { "method": "listExecutions", - "weight": 300, + "weight": 301, "cookies": false, "type": "", "deprecated": false, @@ -10697,7 +10943,7 @@ }, "x-appwrite": { "method": "createExecution", - "weight": 299, + "weight": 300, "cookies": false, "type": "", "deprecated": false, @@ -10815,7 +11061,7 @@ }, "x-appwrite": { "method": "getExecution", - "weight": 301, + "weight": 302, "cookies": false, "type": "", "deprecated": false, @@ -10879,7 +11125,7 @@ }, "x-appwrite": { "method": "deleteExecution", - "weight": 302, + "weight": 303, "cookies": false, "type": "", "deprecated": false, @@ -10947,7 +11193,7 @@ }, "x-appwrite": { "method": "getFunctionUsage", - "weight": 289, + "weight": 290, "cookies": false, "type": "", "deprecated": false, @@ -11026,7 +11272,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 304, + "weight": 305, "cookies": false, "type": "", "deprecated": false, @@ -11084,7 +11330,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 303, + "weight": 304, "cookies": false, "type": "", "deprecated": false, @@ -11137,8 +11383,8 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", - "default": false, + "description": "Secret variables can be updated or deleted, but only functions can read them during build and runtime.", + "default": true, "x-example": false } }, @@ -11175,7 +11421,7 @@ }, "x-appwrite": { "method": "query", - "weight": 326, + "weight": 325, "cookies": false, "type": "graphql", "deprecated": false, @@ -11248,7 +11494,7 @@ }, "x-appwrite": { "method": "mutation", - "weight": 325, + "weight": 324, "cookies": false, "type": "graphql", "deprecated": false, @@ -11371,7 +11617,7 @@ }, "x-appwrite": { "method": "getAntivirus", - "weight": 146, + "weight": 147, "cookies": false, "type": "", "deprecated": false, @@ -11883,7 +12129,7 @@ }, "x-appwrite": { "method": "getFailedJobs", - "weight": 147, + "weight": 148, "cookies": false, "type": "", "deprecated": false, @@ -11920,8 +12166,9 @@ "v1-audits", "v1-mails", "v1-functions", - "v1-usage", - "v1-usage-dump", + "v1-stats-resources", + "v1-stats-usage", + "v1-stats-usage-dump", "v1-webhooks", "v1-certificates", "v1-builds", @@ -12249,10 +12496,10 @@ ] } }, - "\/health\/queue\/usage": { + "\/health\/queue\/stats-resources": { "get": { - "summary": "Get usage queue", - "operationId": "healthGetQueueUsage", + "summary": "Get stats resources queue", + "operationId": "healthGetQueueStatsResources", "consumes": [ "application\/json" ], @@ -12262,7 +12509,7 @@ "tags": [ "health" ], - "description": "Get the number of metrics that are waiting to be processed in the Appwrite internal queue server.", + "description": "Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue.", "responses": { "200": { "description": "Health Queue", @@ -12272,13 +12519,13 @@ } }, "x-appwrite": { - "method": "getQueueUsage", + "method": "getQueueStatsResources", "weight": 142, "cookies": false, "type": "", "deprecated": false, - "demo": "health\/get-queue-usage.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage.md", + "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}", @@ -12310,10 +12557,71 @@ ] } }, - "\/health\/queue\/usage-dump": { + "\/health\/queue\/stats-usage": { + "get": { + "summary": "Get stats usage queue", + "operationId": "healthGetQueueUsage", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "health" + ], + "description": "Get the number of metrics that are waiting to be processed in the Appwrite internal queue server.", + "responses": { + "200": { + "description": "Health Queue", + "schema": { + "$ref": "#\/definitions\/healthQueue" + } + } + }, + "x-appwrite": { + "method": "getQueueUsage", + "weight": 143, + "cookies": false, + "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", + "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-dump": { "get": { "summary": "Get usage dump queue", - "operationId": "healthGetQueueUsageDump", + "operationId": "healthGetQueueStatsUsageDump", "consumes": [ "application\/json" ], @@ -12333,13 +12641,13 @@ } }, "x-appwrite": { - "method": "getQueueUsageDump", - "weight": 143, + "method": "getQueueStatsUsageDump", + "weight": 144, "cookies": false, "type": "", "deprecated": false, - "demo": "health\/get-queue-usage-dump.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage-dump.md", + "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", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", @@ -12456,7 +12764,7 @@ }, "x-appwrite": { "method": "getStorage", - "weight": 145, + "weight": 146, "cookies": false, "type": "", "deprecated": false, @@ -12506,7 +12814,7 @@ }, "x-appwrite": { "method": "getStorageLocal", - "weight": 144, + "weight": 145, "cookies": false, "type": "", "deprecated": false, @@ -13030,7 +13338,7 @@ }, "x-appwrite": { "method": "listMessages", - "weight": 379, + "weight": 378, "cookies": false, "type": "", "deprecated": false, @@ -13104,7 +13412,7 @@ }, "x-appwrite": { "method": "createEmail", - "weight": 376, + "weight": 375, "cookies": false, "type": "", "deprecated": false, @@ -13261,7 +13569,7 @@ }, "x-appwrite": { "method": "updateEmail", - "weight": 383, + "weight": 382, "cookies": false, "type": "", "deprecated": false, @@ -13415,7 +13723,7 @@ }, "x-appwrite": { "method": "createPush", - "weight": 378, + "weight": 377, "cookies": false, "type": "", "deprecated": false, @@ -13609,7 +13917,7 @@ }, "x-appwrite": { "method": "updatePush", - "weight": 385, + "weight": 384, "cookies": false, "type": "", "deprecated": false, @@ -13802,7 +14110,7 @@ }, "x-appwrite": { "method": "createSms", - "weight": 377, + "weight": 376, "cookies": false, "type": "", "deprecated": false, @@ -13919,7 +14227,7 @@ }, "x-appwrite": { "method": "updateSms", - "weight": 384, + "weight": 383, "cookies": false, "type": "", "deprecated": false, @@ -14034,7 +14342,7 @@ }, "x-appwrite": { "method": "getMessage", - "weight": 382, + "weight": 381, "cookies": false, "type": "", "deprecated": false, @@ -14088,7 +14396,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 386, + "weight": 385, "cookies": false, "type": "", "deprecated": false, @@ -14149,7 +14457,7 @@ }, "x-appwrite": { "method": "listMessageLogs", - "weight": 380, + "weight": 379, "cookies": false, "type": "", "deprecated": false, @@ -14222,7 +14530,7 @@ }, "x-appwrite": { "method": "listTargets", - "weight": 381, + "weight": 380, "cookies": false, "type": "", "deprecated": false, @@ -14295,7 +14603,7 @@ }, "x-appwrite": { "method": "listProviders", - "weight": 351, + "weight": 350, "cookies": false, "type": "", "deprecated": false, @@ -14369,7 +14677,7 @@ }, "x-appwrite": { "method": "createApnsProvider", - "weight": 350, + "weight": 349, "cookies": false, "type": "", "deprecated": false, @@ -14483,7 +14791,7 @@ }, "x-appwrite": { "method": "updateApnsProvider", - "weight": 363, + "weight": 362, "cookies": false, "type": "", "deprecated": false, @@ -14595,7 +14903,7 @@ }, "x-appwrite": { "method": "createFcmProvider", - "weight": 349, + "weight": 348, "cookies": false, "type": "", "deprecated": false, @@ -14685,7 +14993,7 @@ }, "x-appwrite": { "method": "updateFcmProvider", - "weight": 362, + "weight": 361, "cookies": false, "type": "", "deprecated": false, @@ -14773,7 +15081,7 @@ }, "x-appwrite": { "method": "createMailgunProvider", - "weight": 341, + "weight": 340, "cookies": false, "type": "", "deprecated": false, @@ -14899,7 +15207,7 @@ }, "x-appwrite": { "method": "updateMailgunProvider", - "weight": 354, + "weight": 353, "cookies": false, "type": "", "deprecated": false, @@ -15023,7 +15331,7 @@ }, "x-appwrite": { "method": "createMsg91Provider", - "weight": 344, + "weight": 343, "cookies": false, "type": "", "deprecated": false, @@ -15125,7 +15433,7 @@ }, "x-appwrite": { "method": "updateMsg91Provider", - "weight": 357, + "weight": 356, "cookies": false, "type": "", "deprecated": false, @@ -15225,7 +15533,7 @@ }, "x-appwrite": { "method": "createSendgridProvider", - "weight": 342, + "weight": 341, "cookies": false, "type": "", "deprecated": false, @@ -15339,7 +15647,7 @@ }, "x-appwrite": { "method": "updateSendgridProvider", - "weight": 355, + "weight": 354, "cookies": false, "type": "", "deprecated": false, @@ -15451,7 +15759,7 @@ }, "x-appwrite": { "method": "createSmtpProvider", - "weight": 343, + "weight": 342, "cookies": false, "type": "", "deprecated": false, @@ -15609,7 +15917,7 @@ }, "x-appwrite": { "method": "updateSmtpProvider", - "weight": 356, + "weight": 355, "cookies": false, "type": "", "deprecated": false, @@ -15764,7 +16072,7 @@ }, "x-appwrite": { "method": "createTelesignProvider", - "weight": 345, + "weight": 344, "cookies": false, "type": "", "deprecated": false, @@ -15866,7 +16174,7 @@ }, "x-appwrite": { "method": "updateTelesignProvider", - "weight": 358, + "weight": 357, "cookies": false, "type": "", "deprecated": false, @@ -15966,7 +16274,7 @@ }, "x-appwrite": { "method": "createTextmagicProvider", - "weight": 346, + "weight": 345, "cookies": false, "type": "", "deprecated": false, @@ -16068,7 +16376,7 @@ }, "x-appwrite": { "method": "updateTextmagicProvider", - "weight": 359, + "weight": 358, "cookies": false, "type": "", "deprecated": false, @@ -16168,7 +16476,7 @@ }, "x-appwrite": { "method": "createTwilioProvider", - "weight": 347, + "weight": 346, "cookies": false, "type": "", "deprecated": false, @@ -16270,7 +16578,7 @@ }, "x-appwrite": { "method": "updateTwilioProvider", - "weight": 360, + "weight": 359, "cookies": false, "type": "", "deprecated": false, @@ -16370,7 +16678,7 @@ }, "x-appwrite": { "method": "createVonageProvider", - "weight": 348, + "weight": 347, "cookies": false, "type": "", "deprecated": false, @@ -16472,7 +16780,7 @@ }, "x-appwrite": { "method": "updateVonageProvider", - "weight": 361, + "weight": 360, "cookies": false, "type": "", "deprecated": false, @@ -16572,7 +16880,7 @@ }, "x-appwrite": { "method": "getProvider", - "weight": 353, + "weight": 352, "cookies": false, "type": "", "deprecated": false, @@ -16626,7 +16934,7 @@ }, "x-appwrite": { "method": "deleteProvider", - "weight": 364, + "weight": 363, "cookies": false, "type": "", "deprecated": false, @@ -16687,7 +16995,7 @@ }, "x-appwrite": { "method": "listProviderLogs", - "weight": 352, + "weight": 351, "cookies": false, "type": "", "deprecated": false, @@ -16760,7 +17068,7 @@ }, "x-appwrite": { "method": "listSubscriberLogs", - "weight": 373, + "weight": 372, "cookies": false, "type": "", "deprecated": false, @@ -16833,7 +17141,7 @@ }, "x-appwrite": { "method": "listTopics", - "weight": 366, + "weight": 365, "cookies": false, "type": "", "deprecated": false, @@ -16905,7 +17213,7 @@ }, "x-appwrite": { "method": "createTopic", - "weight": 365, + "weight": 364, "cookies": false, "type": "", "deprecated": false, @@ -16994,7 +17302,7 @@ }, "x-appwrite": { "method": "getTopic", - "weight": 368, + "weight": 367, "cookies": false, "type": "", "deprecated": false, @@ -17053,7 +17361,7 @@ }, "x-appwrite": { "method": "updateTopic", - "weight": 369, + "weight": 368, "cookies": false, "type": "", "deprecated": false, @@ -17131,7 +17439,7 @@ }, "x-appwrite": { "method": "deleteTopic", - "weight": 370, + "weight": 369, "cookies": false, "type": "", "deprecated": false, @@ -17192,7 +17500,7 @@ }, "x-appwrite": { "method": "listTopicLogs", - "weight": 367, + "weight": 366, "cookies": false, "type": "", "deprecated": false, @@ -17265,7 +17573,7 @@ }, "x-appwrite": { "method": "listSubscribers", - "weight": 372, + "weight": 371, "cookies": false, "type": "", "deprecated": false, @@ -17345,7 +17653,7 @@ }, "x-appwrite": { "method": "createSubscriber", - "weight": 371, + "weight": 370, "cookies": false, "type": "", "deprecated": false, @@ -17434,7 +17742,7 @@ }, "x-appwrite": { "method": "getSubscriber", - "weight": 374, + "weight": 373, "cookies": false, "type": "", "deprecated": false, @@ -17496,7 +17804,7 @@ }, "x-appwrite": { "method": "deleteSubscriber", - "weight": 375, + "weight": 374, "cookies": false, "type": "", "deprecated": false, @@ -17568,7 +17876,7 @@ }, "x-appwrite": { "method": "list", - "weight": 333, + "weight": 332, "cookies": false, "type": "", "deprecated": false, @@ -17640,7 +17948,7 @@ }, "x-appwrite": { "method": "createAppwriteMigration", - "weight": 329, + "weight": 328, "cookies": false, "type": "", "deprecated": false, @@ -17681,19 +17989,19 @@ }, "endpoint": { "type": "string", - "description": "Source's Appwrite Endpoint", + "description": "Source Appwrite endpoint", "default": null, "x-example": "https:\/\/example.com" }, "projectId": { "type": "string", - "description": "Source's Project ID", + "description": "Source Project ID", "default": null, "x-example": "" }, "apiKey": { "type": "string", - "description": "Source's API Key", + "description": "Source API Key", "default": null, "x-example": "" } @@ -17733,7 +18041,7 @@ }, "x-appwrite": { "method": "getAppwriteReport", - "weight": 335, + "weight": 334, "cookies": false, "type": "", "deprecated": false, @@ -17820,7 +18128,7 @@ }, "x-appwrite": { "method": "createFirebaseMigration", - "weight": 330, + "weight": 329, "cookies": false, "type": "", "deprecated": false, @@ -17899,7 +18207,7 @@ }, "x-appwrite": { "method": "getFirebaseReport", - "weight": 336, + "weight": 335, "cookies": false, "type": "", "deprecated": false, @@ -17969,7 +18277,7 @@ }, "x-appwrite": { "method": "createNHostMigration", - "weight": 332, + "weight": 331, "cookies": false, "type": "", "deprecated": false, @@ -18089,7 +18397,7 @@ }, "x-appwrite": { "method": "getNHostReport", - "weight": 338, + "weight": 337, "cookies": false, "type": "", "deprecated": false, @@ -18208,7 +18516,7 @@ }, "x-appwrite": { "method": "createSupabaseMigration", - "weight": 331, + "weight": 330, "cookies": false, "type": "", "deprecated": false, @@ -18321,7 +18629,7 @@ }, "x-appwrite": { "method": "getSupabaseReport", - "weight": 337, + "weight": 336, "cookies": false, "type": "", "deprecated": false, @@ -18433,7 +18741,7 @@ }, "x-appwrite": { "method": "get", - "weight": 334, + "weight": 333, "cookies": false, "type": "", "deprecated": false, @@ -18490,7 +18798,7 @@ }, "x-appwrite": { "method": "retry", - "weight": 339, + "weight": 338, "cookies": false, "type": "", "deprecated": false, @@ -18542,7 +18850,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 340, + "weight": 339, "cookies": false, "type": "", "deprecated": false, @@ -18601,7 +18909,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 195, + "weight": 196, "cookies": false, "type": "", "deprecated": false, @@ -18684,7 +18992,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 197, + "weight": 198, "cookies": false, "type": "", "deprecated": false, @@ -18731,7 +19039,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 196, + "weight": 197, "cookies": false, "type": "", "deprecated": false, @@ -18775,8 +19083,8 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", - "default": false, + "description": "Secret variables can be updated or deleted, but only projects can read them during build and runtime.", + "default": true, "x-example": false } }, @@ -18813,7 +19121,7 @@ }, "x-appwrite": { "method": "getVariable", - "weight": 198, + "weight": 199, "cookies": false, "type": "", "deprecated": false, @@ -18870,7 +19178,7 @@ }, "x-appwrite": { "method": "updateVariable", - "weight": 199, + "weight": 200, "cookies": false, "type": "", "deprecated": false, @@ -18919,6 +19227,12 @@ "description": "Variable value. Max length: 8192 chars.", "default": null, "x-example": "" + }, + "secret": { + "type": "boolean", + "description": "Secret variables can be updated or deleted, but only projects can read them during build and runtime.", + "default": null, + "x-example": false } }, "required": [ @@ -18946,7 +19260,7 @@ }, "x-appwrite": { "method": "deleteVariable", - "weight": 200, + "weight": 201, "cookies": false, "type": "", "deprecated": false, @@ -19005,7 +19319,7 @@ }, "x-appwrite": { "method": "list", - "weight": 150, + "weight": 151, "cookies": false, "type": "", "deprecated": false, @@ -19075,7 +19389,7 @@ }, "x-appwrite": { "method": "create", - "weight": 149, + "weight": 150, "cookies": false, "type": "", "deprecated": false, @@ -19224,7 +19538,7 @@ }, "x-appwrite": { "method": "get", - "weight": 151, + "weight": 152, "cookies": false, "type": "", "deprecated": false, @@ -19281,7 +19595,7 @@ }, "x-appwrite": { "method": "update", - "weight": 152, + "weight": 153, "cookies": false, "type": "", "deprecated": false, @@ -19405,7 +19719,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 169, + "weight": 170, "cookies": false, "type": "", "deprecated": false, @@ -19464,7 +19778,7 @@ }, "x-appwrite": { "method": "updateApiStatus", - "weight": 156, + "weight": 157, "cookies": false, "type": "", "deprecated": false, @@ -19555,7 +19869,7 @@ }, "x-appwrite": { "method": "updateApiStatusAll", - "weight": 157, + "weight": 158, "cookies": false, "type": "", "deprecated": false, @@ -19632,7 +19946,7 @@ }, "x-appwrite": { "method": "updateAuthDuration", - "weight": 162, + "weight": 163, "cookies": false, "type": "", "deprecated": false, @@ -19709,7 +20023,7 @@ }, "x-appwrite": { "method": "updateAuthLimit", - "weight": 161, + "weight": 162, "cookies": false, "type": "", "deprecated": false, @@ -19786,7 +20100,7 @@ }, "x-appwrite": { "method": "updateAuthSessionsLimit", - "weight": 167, + "weight": 168, "cookies": false, "type": "", "deprecated": false, @@ -19863,7 +20177,7 @@ }, "x-appwrite": { "method": "updateMembershipsPrivacy", - "weight": 160, + "weight": 161, "cookies": false, "type": "", "deprecated": false, @@ -19954,7 +20268,7 @@ }, "x-appwrite": { "method": "updateMockNumbers", - "weight": 168, + "weight": 169, "cookies": false, "type": "", "deprecated": false, @@ -20034,7 +20348,7 @@ }, "x-appwrite": { "method": "updateAuthPasswordDictionary", - "weight": 165, + "weight": 166, "cookies": false, "type": "", "deprecated": false, @@ -20111,7 +20425,7 @@ }, "x-appwrite": { "method": "updateAuthPasswordHistory", - "weight": 164, + "weight": 165, "cookies": false, "type": "", "deprecated": false, @@ -20188,7 +20502,7 @@ }, "x-appwrite": { "method": "updatePersonalDataCheck", - "weight": 166, + "weight": 167, "cookies": false, "type": "", "deprecated": false, @@ -20265,7 +20579,7 @@ }, "x-appwrite": { "method": "updateSessionAlerts", - "weight": 159, + "weight": 160, "cookies": false, "type": "", "deprecated": false, @@ -20342,7 +20656,7 @@ }, "x-appwrite": { "method": "updateAuthStatus", - "weight": 163, + "weight": 164, "cookies": false, "type": "", "deprecated": false, @@ -20438,7 +20752,7 @@ }, "x-appwrite": { "method": "createJWT", - "weight": 181, + "weight": 182, "cookies": false, "type": "", "deprecated": false, @@ -20524,7 +20838,7 @@ }, "x-appwrite": { "method": "listKeys", - "weight": 177, + "weight": 178, "cookies": false, "type": "", "deprecated": false, @@ -20581,7 +20895,7 @@ }, "x-appwrite": { "method": "createKey", - "weight": 176, + "weight": 177, "cookies": false, "type": "", "deprecated": false, @@ -20674,7 +20988,7 @@ }, "x-appwrite": { "method": "getKey", - "weight": 178, + "weight": 179, "cookies": false, "type": "", "deprecated": false, @@ -20739,7 +21053,7 @@ }, "x-appwrite": { "method": "updateKey", - "weight": 179, + "weight": 180, "cookies": false, "type": "", "deprecated": false, @@ -20833,7 +21147,7 @@ }, "x-appwrite": { "method": "deleteKey", - "weight": 180, + "weight": 181, "cookies": false, "type": "", "deprecated": false, @@ -20900,7 +21214,7 @@ }, "x-appwrite": { "method": "updateOAuth2", - "weight": 158, + "weight": 159, "cookies": false, "type": "", "deprecated": false, @@ -21038,7 +21352,7 @@ }, "x-appwrite": { "method": "listPlatforms", - "weight": 183, + "weight": 184, "cookies": false, "type": "", "deprecated": false, @@ -21095,7 +21409,7 @@ }, "x-appwrite": { "method": "createPlatform", - "weight": 182, + "weight": 183, "cookies": false, "type": "", "deprecated": false, @@ -21216,7 +21530,7 @@ }, "x-appwrite": { "method": "getPlatform", - "weight": 184, + "weight": 185, "cookies": false, "type": "", "deprecated": false, @@ -21281,7 +21595,7 @@ }, "x-appwrite": { "method": "updatePlatform", - "weight": 185, + "weight": 186, "cookies": false, "type": "", "deprecated": false, @@ -21377,7 +21691,7 @@ }, "x-appwrite": { "method": "deletePlatform", - "weight": 186, + "weight": 187, "cookies": false, "type": "", "deprecated": false, @@ -21444,7 +21758,7 @@ }, "x-appwrite": { "method": "updateServiceStatus", - "weight": 154, + "weight": 155, "cookies": false, "type": "", "deprecated": false, @@ -21544,7 +21858,7 @@ }, "x-appwrite": { "method": "updateServiceStatusAll", - "weight": 155, + "weight": 156, "cookies": false, "type": "", "deprecated": false, @@ -21621,7 +21935,7 @@ }, "x-appwrite": { "method": "updateSmtp", - "weight": 187, + "weight": 188, "cookies": false, "type": "", "deprecated": false, @@ -21749,7 +22063,7 @@ }, "x-appwrite": { "method": "createSmtpTest", - "weight": 188, + "weight": 189, "cookies": false, "type": "", "deprecated": false, @@ -21886,7 +22200,7 @@ }, "x-appwrite": { "method": "updateTeam", - "weight": 153, + "weight": 154, "cookies": false, "type": "", "deprecated": false, @@ -21963,7 +22277,7 @@ }, "x-appwrite": { "method": "getEmailTemplate", - "weight": 190, + "weight": 191, "cookies": false, "type": "", "deprecated": false, @@ -22182,7 +22496,7 @@ }, "x-appwrite": { "method": "updateEmailTemplate", - "weight": 192, + "weight": 193, "cookies": false, "type": "", "deprecated": false, @@ -22444,7 +22758,7 @@ }, "x-appwrite": { "method": "deleteEmailTemplate", - "weight": 194, + "weight": 195, "cookies": false, "type": "", "deprecated": false, @@ -22665,7 +22979,7 @@ }, "x-appwrite": { "method": "getSmsTemplate", - "weight": 189, + "weight": 190, "cookies": false, "type": "", "deprecated": false, @@ -22881,7 +23195,7 @@ }, "x-appwrite": { "method": "updateSmsTemplate", - "weight": 191, + "weight": 192, "cookies": false, "type": "", "deprecated": false, @@ -23115,7 +23429,7 @@ }, "x-appwrite": { "method": "deleteSmsTemplate", - "weight": 193, + "weight": 194, "cookies": false, "type": "", "deprecated": false, @@ -23333,7 +23647,7 @@ }, "x-appwrite": { "method": "listWebhooks", - "weight": 171, + "weight": 172, "cookies": false, "type": "", "deprecated": false, @@ -23390,7 +23704,7 @@ }, "x-appwrite": { "method": "createWebhook", - "weight": 170, + "weight": 171, "cookies": false, "type": "", "deprecated": false, @@ -23509,7 +23823,7 @@ }, "x-appwrite": { "method": "getWebhook", - "weight": 172, + "weight": 173, "cookies": false, "type": "", "deprecated": false, @@ -23574,7 +23888,7 @@ }, "x-appwrite": { "method": "updateWebhook", - "weight": 173, + "weight": 174, "cookies": false, "type": "", "deprecated": false, @@ -23694,7 +24008,7 @@ }, "x-appwrite": { "method": "deleteWebhook", - "weight": 175, + "weight": 176, "cookies": false, "type": "", "deprecated": false, @@ -23761,7 +24075,7 @@ }, "x-appwrite": { "method": "updateWebhookSignature", - "weight": 174, + "weight": 175, "cookies": false, "type": "", "deprecated": false, @@ -23854,7 +24168,7 @@ "parameters": [ { "name": "queries", - "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, resourceType, resourceId, url", + "description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, type, value, automation, url", "required": false, "type": "array", "collectionFormat": "multi", @@ -23874,10 +24188,12 @@ "in": "query" } ] - }, + } + }, + "\/proxy\/rules\/api": { "post": { - "summary": "Create rule", - "operationId": "proxyCreateRule", + "summary": "Create API rule", + "operationId": "proxyCreateAPIRule", "consumes": [ "application\/json" ], @@ -23887,7 +24203,7 @@ "tags": [ "proxy" ], - "description": "Create a new proxy rule.", + "description": "", "responses": { "201": { "description": "Rule", @@ -23897,16 +24213,85 @@ } }, "x-appwrite": { - "method": "createRule", - "weight": 310, + "method": "createAPIRule", + "weight": 423, "cookies": false, "type": "", "deprecated": false, - "demo": "proxy\/create-rule.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/proxy\/create-rule.md", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", + "demo": "proxy\/create-a-p-i-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite's API on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "default": null, + "x-example": null + } + }, + "required": [ + "domain" + ] + } + } + ] + } + }, + "\/proxy\/rules\/function": { + "post": { + "summary": "Create function rule", + "operationId": "proxyCreateFunctionRule", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "schema": { + "$ref": "#\/definitions\/proxyRule" + } + } + }, + "x-appwrite": { + "method": "createFunctionRule", + "weight": 425, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-function-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for executing Appwrite Function on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", "scope": "rules.write", "platforms": [ "console" @@ -23934,29 +24319,180 @@ "default": null, "x-example": null }, - "resourceType": { + "functionId": { "type": "string", - "description": "Action definition for the rule. Possible values are \"api\", \"function\" and \"site\"", + "description": "ID of function to be executed.", "default": null, - "x-example": "api", - "enum": [ - "api", - "function", - "site" - ], - "x-enum-name": null, - "x-enum-keys": [] + "x-example": "" }, - "resourceId": { + "branch": { "type": "string", - "description": "ID of resource for the action type. If resourceType is \"api\", leave empty. If resourceType is \"function\", provide ID of the function.", + "description": "Name of VCS branch to deploy changes automatically", "default": "", - "x-example": "" + "x-example": "" } }, "required": [ "domain", - "resourceType" + "functionId" + ] + } + } + ] + } + }, + "\/proxy\/rules\/redirect": { + "post": { + "summary": "Create Redirect rule", + "operationId": "proxyCreateRedirectRule", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "schema": { + "$ref": "#\/definitions\/proxyRule" + } + } + }, + "x-appwrite": { + "method": "createRedirectRule", + "weight": 426, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-redirect-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for to redirect from custom domain to another domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "default": null, + "x-example": null + }, + "target": { + "type": "string", + "description": "Target domain (hostname) of redirection", + "default": null, + "x-example": null + } + }, + "required": [ + "domain", + "target" + ] + } + } + ] + } + }, + "\/proxy\/rules\/site": { + "post": { + "summary": "Create site rule", + "operationId": "proxyCreateSiteRule", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "proxy" + ], + "description": "", + "responses": { + "201": { + "description": "Rule", + "schema": { + "$ref": "#\/definitions\/proxyRule" + } + } + }, + "x-appwrite": { + "method": "createSiteRule", + "weight": 424, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "proxy\/create-site-rule.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite Site on custom domain.", + "rate-limit": 10, + "rate-time": 60, + "rate-key": "userId:{userId}, url:{url}", + "scope": "rules.write", + "platforms": [ + "console" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain name.", + "default": null, + "x-example": null + }, + "siteId": { + "type": "string", + "description": "ID of site to be executed.", + "default": null, + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Name of VCS branch to deploy changes automatically", + "default": "", + "x-example": "" + } + }, + "required": [ + "domain", + "siteId" ] } } @@ -24157,7 +24693,7 @@ }, "x-appwrite": { "method": "list", - "weight": 394, + "weight": 395, "cookies": false, "type": "", "deprecated": false, @@ -24228,7 +24764,7 @@ }, "x-appwrite": { "method": "create", - "weight": 392, + "weight": 393, "cookies": false, "type": "", "deprecated": false, @@ -24278,7 +24814,9 @@ "x-example": "nextjs", "enum": [ "nextjs", + "react", "nuxt", + "vue", "sveltekit", "astro", "remix", @@ -24318,12 +24856,6 @@ "default": "", "x-example": "" }, - "subdomain": { - "type": "string", - "description": "Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.", - "default": "", - "x-example": "" - }, "buildRuntime": { "type": "string", "description": "Runtime to use during build step.", @@ -24398,9 +24930,15 @@ }, "adapter": { "type": "string", - "description": "Framework adapter. Allows: static, ssr", + "description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr", "default": "", - "x-example": "" + "x-example": "static", + "enum": [ + "static", + "ssr" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "installationId": { "type": "string", @@ -24438,30 +24976,6 @@ "default": "", "x-example": "" }, - "templateRepository": { - "type": "string", - "description": "Repository name of the template.", - "default": "", - "x-example": "" - }, - "templateOwner": { - "type": "string", - "description": "The name of the owner of the template.", - "default": "", - "x-example": "" - }, - "templateRootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "default": "", - "x-example": "" - }, - "templateVersion": { - "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "default": "", - "x-example": "" - }, "specification": { "type": "string", "description": "Framework specification for the site and builds.", @@ -24504,7 +25018,7 @@ }, "x-appwrite": { "method": "listFrameworks", - "weight": 397, + "weight": 398, "cookies": false, "type": "", "deprecated": false, @@ -24554,7 +25068,7 @@ }, "x-appwrite": { "method": "listTemplates", - "weight": 415, + "weight": 418, "cookies": false, "type": "", "deprecated": false, @@ -24649,7 +25163,7 @@ }, "x-appwrite": { "method": "getTemplate", - "weight": 416, + "weight": 419, "cookies": false, "type": "", "deprecated": false, @@ -24708,7 +25222,7 @@ }, "x-appwrite": { "method": "listUsage", - "weight": 417, + "weight": 420, "cookies": false, "type": "", "deprecated": false, @@ -24775,7 +25289,7 @@ }, "x-appwrite": { "method": "get", - "weight": 393, + "weight": 394, "cookies": false, "type": "", "deprecated": false, @@ -24833,7 +25347,7 @@ }, "x-appwrite": { "method": "update", - "weight": 395, + "weight": 396, "cookies": false, "type": "", "deprecated": false, @@ -24885,7 +25399,9 @@ "x-example": "nextjs", "enum": [ "nextjs", + "react", "nuxt", + "vue", "sveltekit", "astro", "remix", @@ -24999,9 +25515,15 @@ }, "adapter": { "type": "string", - "description": "Framework adapter. Usuallly allows: static, ssr", + "description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr", "default": "", - "x-example": "" + "x-example": "static", + "enum": [ + "static", + "ssr" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "fallbackFile": { "type": "string", @@ -25072,7 +25594,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 396, + "weight": 397, "cookies": false, "type": "", "deprecated": false, @@ -25132,7 +25654,7 @@ }, "x-appwrite": { "method": "listDeployments", - "weight": 400, + "weight": 403, "cookies": false, "type": "", "deprecated": false, @@ -25211,7 +25733,7 @@ }, "x-appwrite": { "method": "createDeployment", - "weight": 398, + "weight": 399, "cookies": false, "type": "upload", "deprecated": false, @@ -25286,6 +25808,207 @@ ] } }, + "\/sites\/{siteId}\/deployments\/template": { + "post": { + "summary": "Create deployment", + "operationId": "sitesCreateTemplateDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "sites" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "x-appwrite": { + "method": "createTemplateDeployment", + "weight": 400, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "sites\/create-template-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment based on a template.\n\nUse this endpoint with combination of [listTemplates](https:\/\/appwrite.io\/docs\/server\/sites#listTemplates) to find the template details.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "siteId", + "description": "Site ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "Repository name of the template.", + "default": null, + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "default": null, + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "default": null, + "x-example": "" + }, + "version": { + "type": "string", + "description": "Version (tag) for the repo linked to the site template.", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": false, + "x-example": false + } + }, + "required": [ + "repository", + "owner", + "rootDirectory", + "version" + ] + } + } + ] + } + }, + "\/sites\/{siteId}\/deployments\/vcs": { + "post": { + "summary": "Create VCS deployment", + "operationId": "sitesCreateVcsDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "sites" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "x-appwrite": { + "method": "createVcsDeployment", + "weight": 401, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "sites\/create-vcs-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment when a site is connected to VCS.\n\nThis endpoint lets you create deployment from a branch, commit, or a tag.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "siteId", + "description": "Site ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of reference passed. Allowed values are: branch", + "default": null, + "x-example": "branch", + "enum": [ + "branch" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "VCS reference to create deployment from. Depending on type this can be: branch name", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": false, + "x-example": false + } + }, + "required": [ + "type", + "reference" + ] + } + } + ] + } + }, "\/sites\/{siteId}\/deployments\/{deploymentId}": { "get": { "summary": "Get deployment", @@ -25310,7 +26033,7 @@ }, "x-appwrite": { "method": "getDeployment", - "weight": 399, + "weight": 402, "cookies": false, "type": "", "deprecated": false, @@ -25376,7 +26099,7 @@ }, "x-appwrite": { "method": "updateDeployment", - "weight": 401, + "weight": 404, "cookies": false, "type": "", "deprecated": false, @@ -25437,7 +26160,7 @@ }, "x-appwrite": { "method": "deleteDeployment", - "weight": 402, + "weight": 405, "cookies": false, "type": "", "deprecated": false, @@ -25502,7 +26225,7 @@ }, "x-appwrite": { "method": "createDeploymentBuild", - "weight": 405, + "weight": 408, "cookies": false, "type": "", "deprecated": false, @@ -25568,7 +26291,7 @@ }, "x-appwrite": { "method": "updateDeploymentBuild", - "weight": 406, + "weight": 409, "cookies": false, "type": "", "deprecated": false, @@ -25636,7 +26359,7 @@ }, "x-appwrite": { "method": "getDeploymentBuildDownload", - "weight": 404, + "weight": 407, "cookies": false, "type": "location", "deprecated": false, @@ -25706,7 +26429,7 @@ }, "x-appwrite": { "method": "getDeploymentDownload", - "weight": 403, + "weight": 406, "cookies": false, "type": "location", "deprecated": false, @@ -25776,7 +26499,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 408, + "weight": 411, "cookies": false, "type": "", "deprecated": false, @@ -25857,7 +26580,7 @@ }, "x-appwrite": { "method": "getLog", - "weight": 407, + "weight": 410, "cookies": false, "type": "", "deprecated": false, @@ -25920,7 +26643,7 @@ }, "x-appwrite": { "method": "deleteLog", - "weight": 409, + "weight": 412, "cookies": false, "type": "", "deprecated": false, @@ -25988,7 +26711,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 418, + "weight": 421, "cookies": false, "type": "", "deprecated": false, @@ -26067,7 +26790,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 412, + "weight": 415, "cookies": false, "type": "", "deprecated": false, @@ -26125,7 +26848,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 410, + "weight": 413, "cookies": false, "type": "", "deprecated": false, @@ -26178,8 +26901,8 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", - "default": false, + "description": "Secret variables can be updated or deleted, but only sites can read them during build and runtime.", + "default": true, "x-example": false } }, @@ -26216,7 +26939,7 @@ }, "x-appwrite": { "method": "getVariable", - "weight": 411, + "weight": 414, "cookies": false, "type": "", "deprecated": false, @@ -26282,7 +27005,7 @@ }, "x-appwrite": { "method": "updateVariable", - "weight": 413, + "weight": 416, "cookies": false, "type": "", "deprecated": false, @@ -26340,6 +27063,12 @@ "description": "Variable value. Max length: 8192 chars.", "default": null, "x-example": "" + }, + "secret": { + "type": "boolean", + "description": "Secret variables can be updated or deleted, but only sites can read them during build and runtime.", + "default": null, + "x-example": false } }, "required": [ @@ -26367,7 +27096,7 @@ }, "x-appwrite": { "method": "deleteVariable", - "weight": 414, + "weight": 417, "cookies": false, "type": "", "deprecated": false, @@ -26435,7 +27164,7 @@ }, "x-appwrite": { "method": "listBuckets", - "weight": 202, + "weight": 203, "cookies": false, "type": "", "deprecated": false, @@ -26506,7 +27235,7 @@ }, "x-appwrite": { "method": "createBucket", - "weight": 201, + "weight": 202, "cookies": false, "type": "", "deprecated": false, @@ -26644,7 +27373,7 @@ }, "x-appwrite": { "method": "getBucket", - "weight": 203, + "weight": 204, "cookies": false, "type": "", "deprecated": false, @@ -26702,7 +27431,7 @@ }, "x-appwrite": { "method": "updateBucket", - "weight": 204, + "weight": 205, "cookies": false, "type": "", "deprecated": false, @@ -26834,7 +27563,7 @@ }, "x-appwrite": { "method": "deleteBucket", - "weight": 205, + "weight": 206, "cookies": false, "type": "", "deprecated": false, @@ -26894,7 +27623,7 @@ }, "x-appwrite": { "method": "listFiles", - "weight": 207, + "weight": 208, "cookies": false, "type": "", "deprecated": false, @@ -26976,7 +27705,7 @@ }, "x-appwrite": { "method": "createFile", - "weight": 206, + "weight": 207, "cookies": false, "type": "upload", "deprecated": false, @@ -27067,7 +27796,7 @@ }, "x-appwrite": { "method": "getFile", - "weight": 208, + "weight": 209, "cookies": false, "type": "", "deprecated": false, @@ -27136,7 +27865,7 @@ }, "x-appwrite": { "method": "updateFile", - "weight": 213, + "weight": 214, "cookies": false, "type": "", "deprecated": false, @@ -27224,7 +27953,7 @@ }, "x-appwrite": { "method": "deleteFile", - "weight": 214, + "weight": 215, "cookies": false, "type": "", "deprecated": false, @@ -27295,7 +28024,7 @@ }, "x-appwrite": { "method": "getFileDownload", - "weight": 210, + "weight": 211, "cookies": false, "type": "location", "deprecated": false, @@ -27366,7 +28095,7 @@ }, "x-appwrite": { "method": "getFilePreview", - "weight": 209, + "weight": 210, "cookies": false, "type": "location", "deprecated": false, @@ -27565,7 +28294,7 @@ }, "x-appwrite": { "method": "getFileView", - "weight": 211, + "weight": 212, "cookies": false, "type": "location", "deprecated": false, @@ -27636,7 +28365,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 215, + "weight": 216, "cookies": false, "type": "", "deprecated": false, @@ -27707,7 +28436,7 @@ }, "x-appwrite": { "method": "getBucketUsage", - "weight": 216, + "weight": 217, "cookies": false, "type": "", "deprecated": false, @@ -27786,7 +28515,7 @@ }, "x-appwrite": { "method": "list", - "weight": 218, + "weight": 219, "cookies": false, "type": "", "deprecated": false, @@ -27860,7 +28589,7 @@ }, "x-appwrite": { "method": "create", - "weight": 217, + "weight": 218, "cookies": false, "type": "", "deprecated": false, @@ -27951,7 +28680,7 @@ }, "x-appwrite": { "method": "get", - "weight": 219, + "weight": 220, "cookies": false, "type": "", "deprecated": false, @@ -28012,7 +28741,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 221, + "weight": 222, "cookies": false, "type": "", "deprecated": false, @@ -28086,7 +28815,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 223, + "weight": 224, "cookies": false, "type": "", "deprecated": false, @@ -28149,7 +28878,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 230, + "weight": 231, "cookies": false, "type": "", "deprecated": false, @@ -28220,7 +28949,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 225, + "weight": 226, "cookies": false, "type": "", "deprecated": false, @@ -28302,7 +29031,7 @@ }, "x-appwrite": { "method": "createMembership", - "weight": 224, + "weight": 225, "cookies": false, "type": "", "deprecated": false, @@ -28416,7 +29145,7 @@ }, "x-appwrite": { "method": "getMembership", - "weight": 226, + "weight": 227, "cookies": false, "type": "", "deprecated": false, @@ -28485,7 +29214,7 @@ }, "x-appwrite": { "method": "updateMembership", - "weight": 227, + "weight": 228, "cookies": false, "type": "", "deprecated": false, @@ -28570,7 +29299,7 @@ }, "x-appwrite": { "method": "deleteMembership", - "weight": 229, + "weight": 230, "cookies": false, "type": "", "deprecated": false, @@ -28641,7 +29370,7 @@ }, "x-appwrite": { "method": "updateMembershipStatus", - "weight": 228, + "weight": 229, "cookies": false, "type": "", "deprecated": false, @@ -28735,7 +29464,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 220, + "weight": 221, "cookies": false, "type": "", "deprecated": false, @@ -28794,7 +29523,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 222, + "weight": 223, "cookies": false, "type": "", "deprecated": false, @@ -28873,7 +29602,7 @@ }, "x-appwrite": { "method": "list", - "weight": 240, + "weight": 241, "cookies": false, "type": "", "deprecated": false, @@ -28944,7 +29673,7 @@ }, "x-appwrite": { "method": "create", - "weight": 231, + "weight": 232, "cookies": false, "type": "", "deprecated": false, @@ -29038,7 +29767,7 @@ }, "x-appwrite": { "method": "createArgon2User", - "weight": 234, + "weight": 235, "cookies": false, "type": "", "deprecated": false, @@ -29128,7 +29857,7 @@ }, "x-appwrite": { "method": "createBcryptUser", - "weight": 232, + "weight": 233, "cookies": false, "type": "", "deprecated": false, @@ -29218,7 +29947,7 @@ }, "x-appwrite": { "method": "listIdentities", - "weight": 248, + "weight": 249, "cookies": false, "type": "", "deprecated": false, @@ -29286,7 +30015,7 @@ }, "x-appwrite": { "method": "deleteIdentity", - "weight": 271, + "weight": 272, "cookies": false, "type": "", "deprecated": false, @@ -29346,7 +30075,7 @@ }, "x-appwrite": { "method": "createMD5User", - "weight": 233, + "weight": 234, "cookies": false, "type": "", "deprecated": false, @@ -29436,7 +30165,7 @@ }, "x-appwrite": { "method": "createPHPassUser", - "weight": 236, + "weight": 237, "cookies": false, "type": "", "deprecated": false, @@ -29526,7 +30255,7 @@ }, "x-appwrite": { "method": "createScryptUser", - "weight": 237, + "weight": 238, "cookies": false, "type": "", "deprecated": false, @@ -29651,7 +30380,7 @@ }, "x-appwrite": { "method": "createScryptModifiedUser", - "weight": 238, + "weight": 239, "cookies": false, "type": "", "deprecated": false, @@ -29762,7 +30491,7 @@ }, "x-appwrite": { "method": "createSHAUser", - "weight": 235, + "weight": 236, "cookies": false, "type": "", "deprecated": false, @@ -29873,7 +30602,7 @@ }, "x-appwrite": { "method": "getUsage", - "weight": 273, + "weight": 274, "cookies": false, "type": "", "deprecated": false, @@ -29944,7 +30673,7 @@ }, "x-appwrite": { "method": "get", - "weight": 241, + "weight": 242, "cookies": false, "type": "", "deprecated": false, @@ -29997,7 +30726,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 269, + "weight": 270, "cookies": false, "type": "", "deprecated": false, @@ -30057,7 +30786,7 @@ }, "x-appwrite": { "method": "updateEmail", - "weight": 254, + "weight": 255, "cookies": false, "type": "", "deprecated": false, @@ -30135,7 +30864,7 @@ }, "x-appwrite": { "method": "createJWT", - "weight": 272, + "weight": 273, "cookies": false, "type": "", "deprecated": false, @@ -30216,7 +30945,7 @@ }, "x-appwrite": { "method": "updateLabels", - "weight": 250, + "weight": 251, "cookies": false, "type": "", "deprecated": false, @@ -30297,7 +31026,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 246, + "weight": 247, "cookies": false, "type": "", "deprecated": false, @@ -30369,7 +31098,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 245, + "weight": 246, "cookies": false, "type": "", "deprecated": false, @@ -30429,7 +31158,7 @@ }, "x-appwrite": { "method": "updateMfa", - "weight": 259, + "weight": 260, "cookies": false, "type": "", "deprecated": false, @@ -30502,7 +31231,7 @@ }, "x-appwrite": { "method": "deleteMfaAuthenticator", - "weight": 264, + "weight": 265, "cookies": false, "type": "", "deprecated": false, @@ -30575,7 +31304,7 @@ }, "x-appwrite": { "method": "listMfaFactors", - "weight": 260, + "weight": 261, "cookies": false, "type": "", "deprecated": false, @@ -30635,7 +31364,7 @@ }, "x-appwrite": { "method": "getMfaRecoveryCodes", - "weight": 261, + "weight": 262, "cookies": false, "type": "", "deprecated": false, @@ -30693,7 +31422,7 @@ }, "x-appwrite": { "method": "updateMfaRecoveryCodes", - "weight": 263, + "weight": 264, "cookies": false, "type": "", "deprecated": false, @@ -30751,7 +31480,7 @@ }, "x-appwrite": { "method": "createMfaRecoveryCodes", - "weight": 262, + "weight": 263, "cookies": false, "type": "", "deprecated": false, @@ -30811,7 +31540,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 252, + "weight": 253, "cookies": false, "type": "", "deprecated": false, @@ -30889,7 +31618,7 @@ }, "x-appwrite": { "method": "updatePassword", - "weight": 253, + "weight": 254, "cookies": false, "type": "", "deprecated": false, @@ -30967,7 +31696,7 @@ }, "x-appwrite": { "method": "updatePhone", - "weight": 255, + "weight": 256, "cookies": false, "type": "", "deprecated": false, @@ -31045,7 +31774,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 242, + "weight": 243, "cookies": false, "type": "", "deprecated": false, @@ -31103,7 +31832,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 257, + "weight": 258, "cookies": false, "type": "", "deprecated": false, @@ -31181,7 +31910,7 @@ }, "x-appwrite": { "method": "listSessions", - "weight": 244, + "weight": 245, "cookies": false, "type": "", "deprecated": false, @@ -31239,7 +31968,7 @@ }, "x-appwrite": { "method": "createSession", - "weight": 265, + "weight": 266, "cookies": false, "type": "", "deprecated": false, @@ -31292,7 +32021,7 @@ }, "x-appwrite": { "method": "deleteSessions", - "weight": 268, + "weight": 269, "cookies": false, "type": "", "deprecated": false, @@ -31347,7 +32076,7 @@ }, "x-appwrite": { "method": "deleteSession", - "weight": 267, + "weight": 268, "cookies": false, "type": "", "deprecated": false, @@ -31415,7 +32144,7 @@ }, "x-appwrite": { "method": "updateStatus", - "weight": 249, + "weight": 250, "cookies": false, "type": "", "deprecated": false, @@ -31493,7 +32222,7 @@ }, "x-appwrite": { "method": "listTargets", - "weight": 247, + "weight": 248, "cookies": false, "type": "", "deprecated": false, @@ -31564,7 +32293,7 @@ }, "x-appwrite": { "method": "createTarget", - "weight": 239, + "weight": 240, "cookies": false, "type": "", "deprecated": false, @@ -31676,7 +32405,7 @@ }, "x-appwrite": { "method": "getTarget", - "weight": 243, + "weight": 244, "cookies": false, "type": "", "deprecated": false, @@ -31743,7 +32472,7 @@ }, "x-appwrite": { "method": "updateTarget", - "weight": 258, + "weight": 259, "cookies": false, "type": "", "deprecated": false, @@ -31832,7 +32561,7 @@ }, "x-appwrite": { "method": "deleteTarget", - "weight": 270, + "weight": 271, "cookies": false, "type": "", "deprecated": false, @@ -31901,7 +32630,7 @@ }, "x-appwrite": { "method": "createToken", - "weight": 266, + "weight": 267, "cookies": false, "type": "", "deprecated": false, @@ -31982,7 +32711,7 @@ }, "x-appwrite": { "method": "updateEmailVerification", - "weight": 256, + "weight": 257, "cookies": false, "type": "", "deprecated": false, @@ -32060,7 +32789,7 @@ }, "x-appwrite": { "method": "updatePhoneVerification", - "weight": 251, + "weight": 252, "cookies": false, "type": "", "deprecated": false, @@ -32138,7 +32867,7 @@ }, "x-appwrite": { "method": "listRepositories", - "weight": 278, + "weight": 279, "cookies": false, "type": "", "deprecated": false, @@ -32204,7 +32933,7 @@ }, "x-appwrite": { "method": "createRepository", - "weight": 279, + "weight": 280, "cookies": false, "type": "", "deprecated": false, @@ -32288,7 +33017,7 @@ }, "x-appwrite": { "method": "getRepository", - "weight": 280, + "weight": 281, "cookies": false, "type": "", "deprecated": false, @@ -32355,7 +33084,7 @@ }, "x-appwrite": { "method": "listRepositoryBranches", - "weight": 281, + "weight": 282, "cookies": false, "type": "", "deprecated": false, @@ -32422,7 +33151,7 @@ }, "x-appwrite": { "method": "getRepositoryContents", - "weight": 276, + "weight": 277, "cookies": false, "type": "", "deprecated": false, @@ -32498,7 +33227,7 @@ }, "x-appwrite": { "method": "createRepositoryDetection", - "weight": 277, + "weight": 278, "cookies": false, "type": "", "deprecated": false, @@ -32577,7 +33306,7 @@ }, "x-appwrite": { "method": "updateExternalDeployments", - "weight": 286, + "weight": 287, "cookies": false, "type": "", "deprecated": false, @@ -32662,7 +33391,7 @@ }, "x-appwrite": { "method": "listInstallations", - "weight": 283, + "weight": 284, "cookies": false, "type": "", "deprecated": false, @@ -32734,7 +33463,7 @@ }, "x-appwrite": { "method": "getInstallation", - "weight": 284, + "weight": 285, "cookies": false, "type": "", "deprecated": false, @@ -32786,7 +33515,7 @@ }, "x-appwrite": { "method": "deleteInstallation", - "weight": 285, + "weight": 286, "cookies": false, "type": "", "deprecated": false, @@ -37229,6 +37958,16 @@ "description": "Whether the deployment should be automatically activated.", "x-example": true }, + "screenshotLight": { + "type": "string", + "description": "Screenshot with light theme preference file ID.", + "x-example": "5e5ea5c16897e" + }, + "screenshotDark": { + "type": "string", + "description": "Screenshot with dark theme preference file ID.", + "x-example": "5e5ea5c16897e" + }, "status": { "type": "string", "description": "The deployment status. Possible values are \"processing\", \"building\", \"waiting\", \"ready\", and \"failed\".", @@ -37245,11 +37984,6 @@ "x-example": 128, "format": "int32" }, - "domain": { - "type": "string", - "description": "Preview domain.", - "x-example": "deploy1-project1.appwrite.site" - }, "providerRepositoryName": { "type": "string", "description": "The name of the vcs provider repository", @@ -37313,10 +38047,11 @@ "buildSize", "buildId", "activate", + "screenshotLight", + "screenshotDark", "status", "buildLogs", "buildTime", - "domain", "providerRepositoryName", "providerRepositoryOwner", "providerRepositoryUrl", @@ -39927,15 +40662,20 @@ "description": "Domain name.", "x-example": "appwrite.company.com" }, - "resourceType": { + "type": { "type": "string", - "description": "Action definition for the rule. Possible values are \"api\", \"function\", or \"redirect\"", - "x-example": "function" + "description": "Action definition for the rule. Possible values are \"api\", \"deployment\", or \"redirect\"", + "x-example": "deployment" }, - "resourceId": { + "value": { "type": "string", - "description": "ID of resource for the action type. If resourceType is \"api\" or \"url\", it is empty. If resourceType is \"function\", it is ID of the function.", - "x-example": "myAwesomeFunction" + "description": "Detail specification for the type. If type is \"api\", this is empty. If type is \"redirect\", this is URL. If type is \"deployment\", this is deployment ID.", + "x-example": "67a9cf1a00150ee93abd" + }, + "automation": { + "type": "string", + "description": "Action that results in a rule update. If VCS branch, value can be of syntax \"branch=[name]\"", + "x-example": "branch=dev" }, "status": { "type": "string", @@ -39958,8 +40698,9 @@ "$createdAt", "$updatedAt", "domain", - "resourceType", - "resourceId", + "type", + "value", + "automation", "status", "logs", "renewAt" @@ -40091,6 +40832,11 @@ "type": "string", "description": "Defines if HTTPS is enforced for all requests.", "x-example": "enabled" + }, + "_APP_DOMAINS_NAMESERVERS": { + "type": "string", + "description": "Comma-separated list of nameservers.", + "x-example": "ns1.example.com,ns2.example.com" } }, "required": [ @@ -40102,7 +40848,8 @@ "_APP_DOMAIN_ENABLED", "_APP_ASSISTANT_ENABLED", "_APP_DOMAIN_SITES", - "_APP_OPTIONS_FORCE_HTTPS" + "_APP_OPTIONS_FORCE_HTTPS", + "_APP_DOMAINS_NAMESERVERS" ] }, "mfaChallenge": { diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index 642c4b6a70..8d7586da05 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -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.", + "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.", "required": true, "type": "string", "x-example": "amex", @@ -3281,7 +3281,8 @@ "union-china-pay", "visa", "mir", - "maestro" + "maestro", + "rupay" ], "x-enum-name": "CreditCard", "x-enum-keys": [ @@ -3300,7 +3301,8 @@ "Union China Pay", "Visa", "MIR", - "Maestro" + "Maestro", + "Rupay" ], "in": "path" }, @@ -7552,7 +7554,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.", + "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", "responses": { "201": { "description": "Document", @@ -8284,7 +8286,7 @@ }, "x-appwrite": { "method": "list", - "weight": 389, + "weight": 388, "cookies": false, "type": "", "deprecated": false, @@ -8356,7 +8358,7 @@ }, "x-appwrite": { "method": "create", - "weight": 387, + "weight": 386, "cookies": false, "type": "", "deprecated": false, @@ -8565,30 +8567,6 @@ "default": "", "x-example": "" }, - "templateRepository": { - "type": "string", - "description": "Repository name of the template.", - "default": "", - "x-example": "" - }, - "templateOwner": { - "type": "string", - "description": "The name of the owner of the template.", - "default": "", - "x-example": "" - }, - "templateRootDirectory": { - "type": "string", - "description": "Path to function code in the template repo.", - "default": "", - "x-example": "" - }, - "templateVersion": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "specification": { "type": "string", "description": "Runtime specification for the function and builds.", @@ -8630,7 +8608,7 @@ }, "x-appwrite": { "method": "listRuntimes", - "weight": 390, + "weight": 389, "cookies": false, "type": "", "deprecated": false, @@ -8681,7 +8659,7 @@ }, "x-appwrite": { "method": "listSpecifications", - "weight": 287, + "weight": 288, "cookies": false, "type": "", "deprecated": false, @@ -8733,7 +8711,7 @@ }, "x-appwrite": { "method": "get", - "weight": 288, + "weight": 289, "cookies": false, "type": "", "deprecated": false, @@ -8792,7 +8770,7 @@ }, "x-appwrite": { "method": "update", - "weight": 388, + "weight": 387, "cookies": false, "type": "", "deprecated": false, @@ -9036,7 +9014,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 293, + "weight": 294, "cookies": false, "type": "", "deprecated": false, @@ -9097,7 +9075,7 @@ }, "x-appwrite": { "method": "listDeployments", - "weight": 294, + "weight": 295, "cookies": false, "type": "", "deprecated": false, @@ -9177,7 +9155,7 @@ }, "x-appwrite": { "method": "createDeployment", - "weight": 391, + "weight": 390, "cookies": false, "type": "upload", "deprecated": false, @@ -9245,6 +9223,209 @@ ] } }, + "\/functions\/{functionId}\/deployments\/template": { + "post": { + "summary": "Create template deployment", + "operationId": "functionsCreateTemplateDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "functions" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "x-appwrite": { + "method": "createTemplateDeployment", + "weight": 391, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "functions\/create-template-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment based on a template.\n\nUse this endpoint with combination of [listTemplates](https:\/\/appwrite.io\/docs\/server\/functions#listTemplates) to find the template details.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "functions.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "functionId", + "description": "Function ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "Repository name of the template.", + "default": null, + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "default": null, + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to function code in the template repo.", + "default": null, + "x-example": "" + }, + "version": { + "type": "string", + "description": "Version (tag) for the repo linked to the function template.", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": false, + "x-example": false + } + }, + "required": [ + "repository", + "owner", + "rootDirectory", + "version" + ] + } + } + ] + } + }, + "\/functions\/{functionId}\/deployments\/vcs": { + "post": { + "summary": "Create VCS deployment", + "operationId": "functionsCreateVcsDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "functions" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "x-appwrite": { + "method": "createVcsDeployment", + "weight": 392, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "functions\/create-vcs-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment when a function is connected to VCS.\n\nThis endpoint lets you create deployment from a branch, commit, or a tag.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "functions.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "functionId", + "description": "Function ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of reference passed. Allowed values are: branch", + "default": null, + "x-example": "branch", + "enum": [ + "branch" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "VCS reference to create deployment from. Depending on type this can be: branch name", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": false, + "x-example": false + } + }, + "required": [ + "type", + "reference" + ] + } + } + ] + } + }, "\/functions\/{functionId}\/deployments\/{deploymentId}": { "get": { "summary": "Get deployment", @@ -9269,7 +9450,7 @@ }, "x-appwrite": { "method": "getDeployment", - "weight": 295, + "weight": 296, "cookies": false, "type": "", "deprecated": false, @@ -9331,7 +9512,7 @@ }, "x-appwrite": { "method": "deleteDeployment", - "weight": 296, + "weight": 297, "cookies": false, "type": "", "deprecated": false, @@ -9397,7 +9578,7 @@ }, "x-appwrite": { "method": "createBuild", - "weight": 297, + "weight": 298, "cookies": false, "type": "", "deprecated": false, @@ -9481,7 +9662,7 @@ }, "x-appwrite": { "method": "getDeploymentDownload", - "weight": 291, + "weight": 292, "cookies": false, "type": "location", "deprecated": false, @@ -9552,7 +9733,7 @@ }, "x-appwrite": { "method": "listExecutions", - "weight": 300, + "weight": 301, "cookies": false, "type": "", "deprecated": false, @@ -9636,7 +9817,7 @@ }, "x-appwrite": { "method": "createExecution", - "weight": 299, + "weight": 300, "cookies": false, "type": "", "deprecated": false, @@ -9756,7 +9937,7 @@ }, "x-appwrite": { "method": "getExecution", - "weight": 301, + "weight": 302, "cookies": false, "type": "", "deprecated": false, @@ -9822,7 +10003,7 @@ }, "x-appwrite": { "method": "deleteExecution", - "weight": 302, + "weight": 303, "cookies": false, "type": "", "deprecated": false, @@ -9891,7 +10072,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 304, + "weight": 305, "cookies": false, "type": "", "deprecated": false, @@ -9950,7 +10131,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 303, + "weight": 304, "cookies": false, "type": "", "deprecated": false, @@ -10004,8 +10185,8 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", - "default": false, + "description": "Secret variables can be updated or deleted, but only functions can read them during build and runtime.", + "default": true, "x-example": false } }, @@ -10042,7 +10223,7 @@ }, "x-appwrite": { "method": "query", - "weight": 326, + "weight": 325, "cookies": false, "type": "graphql", "deprecated": false, @@ -10117,7 +10298,7 @@ }, "x-appwrite": { "method": "mutation", - "weight": 325, + "weight": 324, "cookies": false, "type": "graphql", "deprecated": false, @@ -10243,7 +10424,7 @@ }, "x-appwrite": { "method": "getAntivirus", - "weight": 146, + "weight": 147, "cookies": false, "type": "", "deprecated": false, @@ -10764,7 +10945,7 @@ }, "x-appwrite": { "method": "getFailedJobs", - "weight": 147, + "weight": 148, "cookies": false, "type": "", "deprecated": false, @@ -10802,8 +10983,9 @@ "v1-audits", "v1-mails", "v1-functions", - "v1-usage", - "v1-usage-dump", + "v1-stats-resources", + "v1-stats-usage", + "v1-stats-usage-dump", "v1-webhooks", "v1-certificates", "v1-builds", @@ -11136,10 +11318,10 @@ ] } }, - "\/health\/queue\/usage": { + "\/health\/queue\/stats-resources": { "get": { - "summary": "Get usage queue", - "operationId": "healthGetQueueUsage", + "summary": "Get stats resources queue", + "operationId": "healthGetQueueStatsResources", "consumes": [ "application\/json" ], @@ -11149,7 +11331,7 @@ "tags": [ "health" ], - "description": "Get the number of metrics that are waiting to be processed in the Appwrite internal queue server.", + "description": "Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue.", "responses": { "200": { "description": "Health Queue", @@ -11159,13 +11341,13 @@ } }, "x-appwrite": { - "method": "getQueueUsage", + "method": "getQueueStatsResources", "weight": 142, "cookies": false, "type": "", "deprecated": false, - "demo": "health\/get-queue-usage.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage.md", + "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}", @@ -11198,10 +11380,72 @@ ] } }, - "\/health\/queue\/usage-dump": { + "\/health\/queue\/stats-usage": { + "get": { + "summary": "Get stats usage queue", + "operationId": "healthGetQueueUsage", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "health" + ], + "description": "Get the number of metrics that are waiting to be processed in the Appwrite internal queue server.", + "responses": { + "200": { + "description": "Health Queue", + "schema": { + "$ref": "#\/definitions\/healthQueue" + } + } + }, + "x-appwrite": { + "method": "getQueueUsage", + "weight": 143, + "cookies": false, + "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", + "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-dump": { "get": { "summary": "Get usage dump queue", - "operationId": "healthGetQueueUsageDump", + "operationId": "healthGetQueueStatsUsageDump", "consumes": [ "application\/json" ], @@ -11221,13 +11465,13 @@ } }, "x-appwrite": { - "method": "getQueueUsageDump", - "weight": 143, + "method": "getQueueStatsUsageDump", + "weight": 144, "cookies": false, "type": "", "deprecated": false, - "demo": "health\/get-queue-usage-dump.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/master\/docs\/references\/health\/get-queue-usage-dump.md", + "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", "rate-limit": 0, "rate-time": 3600, "rate-key": "url:{url},ip:{ip}", @@ -11346,7 +11590,7 @@ }, "x-appwrite": { "method": "getStorage", - "weight": 145, + "weight": 146, "cookies": false, "type": "", "deprecated": false, @@ -11397,7 +11641,7 @@ }, "x-appwrite": { "method": "getStorageLocal", - "weight": 144, + "weight": 145, "cookies": false, "type": "", "deprecated": false, @@ -11939,7 +12183,7 @@ }, "x-appwrite": { "method": "listMessages", - "weight": 379, + "weight": 378, "cookies": false, "type": "", "deprecated": false, @@ -12014,7 +12258,7 @@ }, "x-appwrite": { "method": "createEmail", - "weight": 376, + "weight": 375, "cookies": false, "type": "", "deprecated": false, @@ -12172,7 +12416,7 @@ }, "x-appwrite": { "method": "updateEmail", - "weight": 383, + "weight": 382, "cookies": false, "type": "", "deprecated": false, @@ -12327,7 +12571,7 @@ }, "x-appwrite": { "method": "createPush", - "weight": 378, + "weight": 377, "cookies": false, "type": "", "deprecated": false, @@ -12522,7 +12766,7 @@ }, "x-appwrite": { "method": "updatePush", - "weight": 385, + "weight": 384, "cookies": false, "type": "", "deprecated": false, @@ -12716,7 +12960,7 @@ }, "x-appwrite": { "method": "createSms", - "weight": 377, + "weight": 376, "cookies": false, "type": "", "deprecated": false, @@ -12834,7 +13078,7 @@ }, "x-appwrite": { "method": "updateSms", - "weight": 384, + "weight": 383, "cookies": false, "type": "", "deprecated": false, @@ -12950,7 +13194,7 @@ }, "x-appwrite": { "method": "getMessage", - "weight": 382, + "weight": 381, "cookies": false, "type": "", "deprecated": false, @@ -13005,7 +13249,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 386, + "weight": 385, "cookies": false, "type": "", "deprecated": false, @@ -13067,7 +13311,7 @@ }, "x-appwrite": { "method": "listMessageLogs", - "weight": 380, + "weight": 379, "cookies": false, "type": "", "deprecated": false, @@ -13141,7 +13385,7 @@ }, "x-appwrite": { "method": "listTargets", - "weight": 381, + "weight": 380, "cookies": false, "type": "", "deprecated": false, @@ -13215,7 +13459,7 @@ }, "x-appwrite": { "method": "listProviders", - "weight": 351, + "weight": 350, "cookies": false, "type": "", "deprecated": false, @@ -13290,7 +13534,7 @@ }, "x-appwrite": { "method": "createApnsProvider", - "weight": 350, + "weight": 349, "cookies": false, "type": "", "deprecated": false, @@ -13405,7 +13649,7 @@ }, "x-appwrite": { "method": "updateApnsProvider", - "weight": 363, + "weight": 362, "cookies": false, "type": "", "deprecated": false, @@ -13518,7 +13762,7 @@ }, "x-appwrite": { "method": "createFcmProvider", - "weight": 349, + "weight": 348, "cookies": false, "type": "", "deprecated": false, @@ -13609,7 +13853,7 @@ }, "x-appwrite": { "method": "updateFcmProvider", - "weight": 362, + "weight": 361, "cookies": false, "type": "", "deprecated": false, @@ -13698,7 +13942,7 @@ }, "x-appwrite": { "method": "createMailgunProvider", - "weight": 341, + "weight": 340, "cookies": false, "type": "", "deprecated": false, @@ -13825,7 +14069,7 @@ }, "x-appwrite": { "method": "updateMailgunProvider", - "weight": 354, + "weight": 353, "cookies": false, "type": "", "deprecated": false, @@ -13950,7 +14194,7 @@ }, "x-appwrite": { "method": "createMsg91Provider", - "weight": 344, + "weight": 343, "cookies": false, "type": "", "deprecated": false, @@ -14053,7 +14297,7 @@ }, "x-appwrite": { "method": "updateMsg91Provider", - "weight": 357, + "weight": 356, "cookies": false, "type": "", "deprecated": false, @@ -14154,7 +14398,7 @@ }, "x-appwrite": { "method": "createSendgridProvider", - "weight": 342, + "weight": 341, "cookies": false, "type": "", "deprecated": false, @@ -14269,7 +14513,7 @@ }, "x-appwrite": { "method": "updateSendgridProvider", - "weight": 355, + "weight": 354, "cookies": false, "type": "", "deprecated": false, @@ -14382,7 +14626,7 @@ }, "x-appwrite": { "method": "createSmtpProvider", - "weight": 343, + "weight": 342, "cookies": false, "type": "", "deprecated": false, @@ -14541,7 +14785,7 @@ }, "x-appwrite": { "method": "updateSmtpProvider", - "weight": 356, + "weight": 355, "cookies": false, "type": "", "deprecated": false, @@ -14697,7 +14941,7 @@ }, "x-appwrite": { "method": "createTelesignProvider", - "weight": 345, + "weight": 344, "cookies": false, "type": "", "deprecated": false, @@ -14800,7 +15044,7 @@ }, "x-appwrite": { "method": "updateTelesignProvider", - "weight": 358, + "weight": 357, "cookies": false, "type": "", "deprecated": false, @@ -14901,7 +15145,7 @@ }, "x-appwrite": { "method": "createTextmagicProvider", - "weight": 346, + "weight": 345, "cookies": false, "type": "", "deprecated": false, @@ -15004,7 +15248,7 @@ }, "x-appwrite": { "method": "updateTextmagicProvider", - "weight": 359, + "weight": 358, "cookies": false, "type": "", "deprecated": false, @@ -15105,7 +15349,7 @@ }, "x-appwrite": { "method": "createTwilioProvider", - "weight": 347, + "weight": 346, "cookies": false, "type": "", "deprecated": false, @@ -15208,7 +15452,7 @@ }, "x-appwrite": { "method": "updateTwilioProvider", - "weight": 360, + "weight": 359, "cookies": false, "type": "", "deprecated": false, @@ -15309,7 +15553,7 @@ }, "x-appwrite": { "method": "createVonageProvider", - "weight": 348, + "weight": 347, "cookies": false, "type": "", "deprecated": false, @@ -15412,7 +15656,7 @@ }, "x-appwrite": { "method": "updateVonageProvider", - "weight": 361, + "weight": 360, "cookies": false, "type": "", "deprecated": false, @@ -15513,7 +15757,7 @@ }, "x-appwrite": { "method": "getProvider", - "weight": 353, + "weight": 352, "cookies": false, "type": "", "deprecated": false, @@ -15568,7 +15812,7 @@ }, "x-appwrite": { "method": "deleteProvider", - "weight": 364, + "weight": 363, "cookies": false, "type": "", "deprecated": false, @@ -15630,7 +15874,7 @@ }, "x-appwrite": { "method": "listProviderLogs", - "weight": 352, + "weight": 351, "cookies": false, "type": "", "deprecated": false, @@ -15704,7 +15948,7 @@ }, "x-appwrite": { "method": "listSubscriberLogs", - "weight": 373, + "weight": 372, "cookies": false, "type": "", "deprecated": false, @@ -15778,7 +16022,7 @@ }, "x-appwrite": { "method": "listTopics", - "weight": 366, + "weight": 365, "cookies": false, "type": "", "deprecated": false, @@ -15851,7 +16095,7 @@ }, "x-appwrite": { "method": "createTopic", - "weight": 365, + "weight": 364, "cookies": false, "type": "", "deprecated": false, @@ -15941,7 +16185,7 @@ }, "x-appwrite": { "method": "getTopic", - "weight": 368, + "weight": 367, "cookies": false, "type": "", "deprecated": false, @@ -16001,7 +16245,7 @@ }, "x-appwrite": { "method": "updateTopic", - "weight": 369, + "weight": 368, "cookies": false, "type": "", "deprecated": false, @@ -16080,7 +16324,7 @@ }, "x-appwrite": { "method": "deleteTopic", - "weight": 370, + "weight": 369, "cookies": false, "type": "", "deprecated": false, @@ -16142,7 +16386,7 @@ }, "x-appwrite": { "method": "listTopicLogs", - "weight": 367, + "weight": 366, "cookies": false, "type": "", "deprecated": false, @@ -16216,7 +16460,7 @@ }, "x-appwrite": { "method": "listSubscribers", - "weight": 372, + "weight": 371, "cookies": false, "type": "", "deprecated": false, @@ -16297,7 +16541,7 @@ }, "x-appwrite": { "method": "createSubscriber", - "weight": 371, + "weight": 370, "cookies": false, "type": "", "deprecated": false, @@ -16388,7 +16632,7 @@ }, "x-appwrite": { "method": "getSubscriber", - "weight": 374, + "weight": 373, "cookies": false, "type": "", "deprecated": false, @@ -16451,7 +16695,7 @@ }, "x-appwrite": { "method": "deleteSubscriber", - "weight": 375, + "weight": 374, "cookies": false, "type": "", "deprecated": false, @@ -16525,7 +16769,7 @@ }, "x-appwrite": { "method": "list", - "weight": 394, + "weight": 395, "cookies": false, "type": "", "deprecated": false, @@ -16597,7 +16841,7 @@ }, "x-appwrite": { "method": "create", - "weight": 392, + "weight": 393, "cookies": false, "type": "", "deprecated": false, @@ -16648,7 +16892,9 @@ "x-example": "nextjs", "enum": [ "nextjs", + "react", "nuxt", + "vue", "sveltekit", "astro", "remix", @@ -16688,12 +16934,6 @@ "default": "", "x-example": "" }, - "subdomain": { - "type": "string", - "description": "Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.", - "default": "", - "x-example": "" - }, "buildRuntime": { "type": "string", "description": "Runtime to use during build step.", @@ -16768,9 +17008,15 @@ }, "adapter": { "type": "string", - "description": "Framework adapter. Allows: static, ssr", + "description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr", "default": "", - "x-example": "" + "x-example": "static", + "enum": [ + "static", + "ssr" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "installationId": { "type": "string", @@ -16808,30 +17054,6 @@ "default": "", "x-example": "" }, - "templateRepository": { - "type": "string", - "description": "Repository name of the template.", - "default": "", - "x-example": "" - }, - "templateOwner": { - "type": "string", - "description": "The name of the owner of the template.", - "default": "", - "x-example": "" - }, - "templateRootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "default": "", - "x-example": "" - }, - "templateVersion": { - "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "default": "", - "x-example": "" - }, "specification": { "type": "string", "description": "Framework specification for the site and builds.", @@ -16874,7 +17096,7 @@ }, "x-appwrite": { "method": "listFrameworks", - "weight": 397, + "weight": 398, "cookies": false, "type": "", "deprecated": false, @@ -16925,7 +17147,7 @@ }, "x-appwrite": { "method": "get", - "weight": 393, + "weight": 394, "cookies": false, "type": "", "deprecated": false, @@ -16984,7 +17206,7 @@ }, "x-appwrite": { "method": "update", - "weight": 395, + "weight": 396, "cookies": false, "type": "", "deprecated": false, @@ -17037,7 +17259,9 @@ "x-example": "nextjs", "enum": [ "nextjs", + "react", "nuxt", + "vue", "sveltekit", "astro", "remix", @@ -17151,9 +17375,15 @@ }, "adapter": { "type": "string", - "description": "Framework adapter. Usuallly allows: static, ssr", + "description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr", "default": "", - "x-example": "" + "x-example": "static", + "enum": [ + "static", + "ssr" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "fallbackFile": { "type": "string", @@ -17224,7 +17454,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 396, + "weight": 397, "cookies": false, "type": "", "deprecated": false, @@ -17285,7 +17515,7 @@ }, "x-appwrite": { "method": "listDeployments", - "weight": 400, + "weight": 403, "cookies": false, "type": "", "deprecated": false, @@ -17365,7 +17595,7 @@ }, "x-appwrite": { "method": "createDeployment", - "weight": 398, + "weight": 399, "cookies": false, "type": "upload", "deprecated": false, @@ -17441,6 +17671,209 @@ ] } }, + "\/sites\/{siteId}\/deployments\/template": { + "post": { + "summary": "Create deployment", + "operationId": "sitesCreateTemplateDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "sites" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "x-appwrite": { + "method": "createTemplateDeployment", + "weight": 400, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "sites\/create-template-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment based on a template.\n\nUse this endpoint with combination of [listTemplates](https:\/\/appwrite.io\/docs\/server\/sites#listTemplates) to find the template details.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "siteId", + "description": "Site ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "Repository name of the template.", + "default": null, + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "default": null, + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "default": null, + "x-example": "" + }, + "version": { + "type": "string", + "description": "Version (tag) for the repo linked to the site template.", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": false, + "x-example": false + } + }, + "required": [ + "repository", + "owner", + "rootDirectory", + "version" + ] + } + } + ] + } + }, + "\/sites\/{siteId}\/deployments\/vcs": { + "post": { + "summary": "Create VCS deployment", + "operationId": "sitesCreateVcsDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "sites" + ], + "description": "", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "x-appwrite": { + "method": "createVcsDeployment", + "weight": 401, + "cookies": false, + "type": "", + "deprecated": false, + "demo": "sites\/create-vcs-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment when a site is connected to VCS.\n\nThis endpoint lets you create deployment from a branch, commit, or a tag.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "siteId", + "description": "Site ID.", + "required": true, + "type": "string", + "x-example": "", + "in": "path" + }, + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of reference passed. Allowed values are: branch", + "default": null, + "x-example": "branch", + "enum": [ + "branch" + ], + "x-enum-name": null, + "x-enum-keys": [] + }, + "reference": { + "type": "string", + "description": "VCS reference to create deployment from. Depending on type this can be: branch name", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": false, + "x-example": false + } + }, + "required": [ + "type", + "reference" + ] + } + } + ] + } + }, "\/sites\/{siteId}\/deployments\/{deploymentId}": { "get": { "summary": "Get deployment", @@ -17465,7 +17898,7 @@ }, "x-appwrite": { "method": "getDeployment", - "weight": 399, + "weight": 402, "cookies": false, "type": "", "deprecated": false, @@ -17532,7 +17965,7 @@ }, "x-appwrite": { "method": "updateDeployment", - "weight": 401, + "weight": 404, "cookies": false, "type": "", "deprecated": false, @@ -17594,7 +18027,7 @@ }, "x-appwrite": { "method": "deleteDeployment", - "weight": 402, + "weight": 405, "cookies": false, "type": "", "deprecated": false, @@ -17660,7 +18093,7 @@ }, "x-appwrite": { "method": "createDeploymentBuild", - "weight": 405, + "weight": 408, "cookies": false, "type": "", "deprecated": false, @@ -17727,7 +18160,7 @@ }, "x-appwrite": { "method": "updateDeploymentBuild", - "weight": 406, + "weight": 409, "cookies": false, "type": "", "deprecated": false, @@ -17796,7 +18229,7 @@ }, "x-appwrite": { "method": "getDeploymentBuildDownload", - "weight": 404, + "weight": 407, "cookies": false, "type": "location", "deprecated": false, @@ -17867,7 +18300,7 @@ }, "x-appwrite": { "method": "getDeploymentDownload", - "weight": 403, + "weight": 406, "cookies": false, "type": "location", "deprecated": false, @@ -17938,7 +18371,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 408, + "weight": 411, "cookies": false, "type": "", "deprecated": false, @@ -18020,7 +18453,7 @@ }, "x-appwrite": { "method": "getLog", - "weight": 407, + "weight": 410, "cookies": false, "type": "", "deprecated": false, @@ -18084,7 +18517,7 @@ }, "x-appwrite": { "method": "deleteLog", - "weight": 409, + "weight": 412, "cookies": false, "type": "", "deprecated": false, @@ -18153,7 +18586,7 @@ }, "x-appwrite": { "method": "listVariables", - "weight": 412, + "weight": 415, "cookies": false, "type": "", "deprecated": false, @@ -18212,7 +18645,7 @@ }, "x-appwrite": { "method": "createVariable", - "weight": 410, + "weight": 413, "cookies": false, "type": "", "deprecated": false, @@ -18266,8 +18699,8 @@ }, "secret": { "type": "boolean", - "description": "Is secret? Secret variables can only be updated or deleted, they cannot be read.", - "default": false, + "description": "Secret variables can be updated or deleted, but only sites can read them during build and runtime.", + "default": true, "x-example": false } }, @@ -18304,7 +18737,7 @@ }, "x-appwrite": { "method": "getVariable", - "weight": 411, + "weight": 414, "cookies": false, "type": "", "deprecated": false, @@ -18371,7 +18804,7 @@ }, "x-appwrite": { "method": "updateVariable", - "weight": 413, + "weight": 416, "cookies": false, "type": "", "deprecated": false, @@ -18430,6 +18863,12 @@ "description": "Variable value. Max length: 8192 chars.", "default": null, "x-example": "" + }, + "secret": { + "type": "boolean", + "description": "Secret variables can be updated or deleted, but only sites can read them during build and runtime.", + "default": null, + "x-example": false } }, "required": [ @@ -18457,7 +18896,7 @@ }, "x-appwrite": { "method": "deleteVariable", - "weight": 414, + "weight": 417, "cookies": false, "type": "", "deprecated": false, @@ -18526,7 +18965,7 @@ }, "x-appwrite": { "method": "listBuckets", - "weight": 202, + "weight": 203, "cookies": false, "type": "", "deprecated": false, @@ -18598,7 +19037,7 @@ }, "x-appwrite": { "method": "createBucket", - "weight": 201, + "weight": 202, "cookies": false, "type": "", "deprecated": false, @@ -18737,7 +19176,7 @@ }, "x-appwrite": { "method": "getBucket", - "weight": 203, + "weight": 204, "cookies": false, "type": "", "deprecated": false, @@ -18796,7 +19235,7 @@ }, "x-appwrite": { "method": "updateBucket", - "weight": 204, + "weight": 205, "cookies": false, "type": "", "deprecated": false, @@ -18929,7 +19368,7 @@ }, "x-appwrite": { "method": "deleteBucket", - "weight": 205, + "weight": 206, "cookies": false, "type": "", "deprecated": false, @@ -18990,7 +19429,7 @@ }, "x-appwrite": { "method": "listFiles", - "weight": 207, + "weight": 208, "cookies": false, "type": "", "deprecated": false, @@ -19074,7 +19513,7 @@ }, "x-appwrite": { "method": "createFile", - "weight": 206, + "weight": 207, "cookies": false, "type": "upload", "deprecated": false, @@ -19167,7 +19606,7 @@ }, "x-appwrite": { "method": "getFile", - "weight": 208, + "weight": 209, "cookies": false, "type": "", "deprecated": false, @@ -19238,7 +19677,7 @@ }, "x-appwrite": { "method": "updateFile", - "weight": 213, + "weight": 214, "cookies": false, "type": "", "deprecated": false, @@ -19328,7 +19767,7 @@ }, "x-appwrite": { "method": "deleteFile", - "weight": 214, + "weight": 215, "cookies": false, "type": "", "deprecated": false, @@ -19401,7 +19840,7 @@ }, "x-appwrite": { "method": "getFileDownload", - "weight": 210, + "weight": 211, "cookies": false, "type": "location", "deprecated": false, @@ -19474,7 +19913,7 @@ }, "x-appwrite": { "method": "getFilePreview", - "weight": 209, + "weight": 210, "cookies": false, "type": "location", "deprecated": false, @@ -19675,7 +20114,7 @@ }, "x-appwrite": { "method": "getFileView", - "weight": 211, + "weight": 212, "cookies": false, "type": "location", "deprecated": false, @@ -19748,7 +20187,7 @@ }, "x-appwrite": { "method": "list", - "weight": 218, + "weight": 219, "cookies": false, "type": "", "deprecated": false, @@ -19824,7 +20263,7 @@ }, "x-appwrite": { "method": "create", - "weight": 217, + "weight": 218, "cookies": false, "type": "", "deprecated": false, @@ -19917,7 +20356,7 @@ }, "x-appwrite": { "method": "get", - "weight": 219, + "weight": 220, "cookies": false, "type": "", "deprecated": false, @@ -19980,7 +20419,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 221, + "weight": 222, "cookies": false, "type": "", "deprecated": false, @@ -20056,7 +20495,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 223, + "weight": 224, "cookies": false, "type": "", "deprecated": false, @@ -20121,7 +20560,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 225, + "weight": 226, "cookies": false, "type": "", "deprecated": false, @@ -20205,7 +20644,7 @@ }, "x-appwrite": { "method": "createMembership", - "weight": 224, + "weight": 225, "cookies": false, "type": "", "deprecated": false, @@ -20321,7 +20760,7 @@ }, "x-appwrite": { "method": "getMembership", - "weight": 226, + "weight": 227, "cookies": false, "type": "", "deprecated": false, @@ -20392,7 +20831,7 @@ }, "x-appwrite": { "method": "updateMembership", - "weight": 227, + "weight": 228, "cookies": false, "type": "", "deprecated": false, @@ -20479,7 +20918,7 @@ }, "x-appwrite": { "method": "deleteMembership", - "weight": 229, + "weight": 230, "cookies": false, "type": "", "deprecated": false, @@ -20552,7 +20991,7 @@ }, "x-appwrite": { "method": "updateMembershipStatus", - "weight": 228, + "weight": 229, "cookies": false, "type": "", "deprecated": false, @@ -20648,7 +21087,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 220, + "weight": 221, "cookies": false, "type": "", "deprecated": false, @@ -20709,7 +21148,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 222, + "weight": 223, "cookies": false, "type": "", "deprecated": false, @@ -20790,7 +21229,7 @@ }, "x-appwrite": { "method": "list", - "weight": 240, + "weight": 241, "cookies": false, "type": "", "deprecated": false, @@ -20862,7 +21301,7 @@ }, "x-appwrite": { "method": "create", - "weight": 231, + "weight": 232, "cookies": false, "type": "", "deprecated": false, @@ -20957,7 +21396,7 @@ }, "x-appwrite": { "method": "createArgon2User", - "weight": 234, + "weight": 235, "cookies": false, "type": "", "deprecated": false, @@ -21048,7 +21487,7 @@ }, "x-appwrite": { "method": "createBcryptUser", - "weight": 232, + "weight": 233, "cookies": false, "type": "", "deprecated": false, @@ -21139,7 +21578,7 @@ }, "x-appwrite": { "method": "listIdentities", - "weight": 248, + "weight": 249, "cookies": false, "type": "", "deprecated": false, @@ -21208,7 +21647,7 @@ }, "x-appwrite": { "method": "deleteIdentity", - "weight": 271, + "weight": 272, "cookies": false, "type": "", "deprecated": false, @@ -21269,7 +21708,7 @@ }, "x-appwrite": { "method": "createMD5User", - "weight": 233, + "weight": 234, "cookies": false, "type": "", "deprecated": false, @@ -21360,7 +21799,7 @@ }, "x-appwrite": { "method": "createPHPassUser", - "weight": 236, + "weight": 237, "cookies": false, "type": "", "deprecated": false, @@ -21451,7 +21890,7 @@ }, "x-appwrite": { "method": "createScryptUser", - "weight": 237, + "weight": 238, "cookies": false, "type": "", "deprecated": false, @@ -21577,7 +22016,7 @@ }, "x-appwrite": { "method": "createScryptModifiedUser", - "weight": 238, + "weight": 239, "cookies": false, "type": "", "deprecated": false, @@ -21689,7 +22128,7 @@ }, "x-appwrite": { "method": "createSHAUser", - "weight": 235, + "weight": 236, "cookies": false, "type": "", "deprecated": false, @@ -21801,7 +22240,7 @@ }, "x-appwrite": { "method": "get", - "weight": 241, + "weight": 242, "cookies": false, "type": "", "deprecated": false, @@ -21855,7 +22294,7 @@ }, "x-appwrite": { "method": "delete", - "weight": 269, + "weight": 270, "cookies": false, "type": "", "deprecated": false, @@ -21916,7 +22355,7 @@ }, "x-appwrite": { "method": "updateEmail", - "weight": 254, + "weight": 255, "cookies": false, "type": "", "deprecated": false, @@ -21995,7 +22434,7 @@ }, "x-appwrite": { "method": "createJWT", - "weight": 272, + "weight": 273, "cookies": false, "type": "", "deprecated": false, @@ -22077,7 +22516,7 @@ }, "x-appwrite": { "method": "updateLabels", - "weight": 250, + "weight": 251, "cookies": false, "type": "", "deprecated": false, @@ -22159,7 +22598,7 @@ }, "x-appwrite": { "method": "listLogs", - "weight": 246, + "weight": 247, "cookies": false, "type": "", "deprecated": false, @@ -22232,7 +22671,7 @@ }, "x-appwrite": { "method": "listMemberships", - "weight": 245, + "weight": 246, "cookies": false, "type": "", "deprecated": false, @@ -22293,7 +22732,7 @@ }, "x-appwrite": { "method": "updateMfa", - "weight": 259, + "weight": 260, "cookies": false, "type": "", "deprecated": false, @@ -22367,7 +22806,7 @@ }, "x-appwrite": { "method": "deleteMfaAuthenticator", - "weight": 264, + "weight": 265, "cookies": false, "type": "", "deprecated": false, @@ -22441,7 +22880,7 @@ }, "x-appwrite": { "method": "listMfaFactors", - "weight": 260, + "weight": 261, "cookies": false, "type": "", "deprecated": false, @@ -22502,7 +22941,7 @@ }, "x-appwrite": { "method": "getMfaRecoveryCodes", - "weight": 261, + "weight": 262, "cookies": false, "type": "", "deprecated": false, @@ -22561,7 +23000,7 @@ }, "x-appwrite": { "method": "updateMfaRecoveryCodes", - "weight": 263, + "weight": 264, "cookies": false, "type": "", "deprecated": false, @@ -22620,7 +23059,7 @@ }, "x-appwrite": { "method": "createMfaRecoveryCodes", - "weight": 262, + "weight": 263, "cookies": false, "type": "", "deprecated": false, @@ -22681,7 +23120,7 @@ }, "x-appwrite": { "method": "updateName", - "weight": 252, + "weight": 253, "cookies": false, "type": "", "deprecated": false, @@ -22760,7 +23199,7 @@ }, "x-appwrite": { "method": "updatePassword", - "weight": 253, + "weight": 254, "cookies": false, "type": "", "deprecated": false, @@ -22839,7 +23278,7 @@ }, "x-appwrite": { "method": "updatePhone", - "weight": 255, + "weight": 256, "cookies": false, "type": "", "deprecated": false, @@ -22918,7 +23357,7 @@ }, "x-appwrite": { "method": "getPrefs", - "weight": 242, + "weight": 243, "cookies": false, "type": "", "deprecated": false, @@ -22977,7 +23416,7 @@ }, "x-appwrite": { "method": "updatePrefs", - "weight": 257, + "weight": 258, "cookies": false, "type": "", "deprecated": false, @@ -23056,7 +23495,7 @@ }, "x-appwrite": { "method": "listSessions", - "weight": 244, + "weight": 245, "cookies": false, "type": "", "deprecated": false, @@ -23115,7 +23554,7 @@ }, "x-appwrite": { "method": "createSession", - "weight": 265, + "weight": 266, "cookies": false, "type": "", "deprecated": false, @@ -23169,7 +23608,7 @@ }, "x-appwrite": { "method": "deleteSessions", - "weight": 268, + "weight": 269, "cookies": false, "type": "", "deprecated": false, @@ -23225,7 +23664,7 @@ }, "x-appwrite": { "method": "deleteSession", - "weight": 267, + "weight": 268, "cookies": false, "type": "", "deprecated": false, @@ -23294,7 +23733,7 @@ }, "x-appwrite": { "method": "updateStatus", - "weight": 249, + "weight": 250, "cookies": false, "type": "", "deprecated": false, @@ -23373,7 +23812,7 @@ }, "x-appwrite": { "method": "listTargets", - "weight": 247, + "weight": 248, "cookies": false, "type": "", "deprecated": false, @@ -23445,7 +23884,7 @@ }, "x-appwrite": { "method": "createTarget", - "weight": 239, + "weight": 240, "cookies": false, "type": "", "deprecated": false, @@ -23558,7 +23997,7 @@ }, "x-appwrite": { "method": "getTarget", - "weight": 243, + "weight": 244, "cookies": false, "type": "", "deprecated": false, @@ -23626,7 +24065,7 @@ }, "x-appwrite": { "method": "updateTarget", - "weight": 258, + "weight": 259, "cookies": false, "type": "", "deprecated": false, @@ -23716,7 +24155,7 @@ }, "x-appwrite": { "method": "deleteTarget", - "weight": 270, + "weight": 271, "cookies": false, "type": "", "deprecated": false, @@ -23786,7 +24225,7 @@ }, "x-appwrite": { "method": "createToken", - "weight": 266, + "weight": 267, "cookies": false, "type": "", "deprecated": false, @@ -23868,7 +24307,7 @@ }, "x-appwrite": { "method": "updateEmailVerification", - "weight": 256, + "weight": 257, "cookies": false, "type": "", "deprecated": false, @@ -23947,7 +24386,7 @@ }, "x-appwrite": { "method": "updatePhoneVerification", - "weight": 251, + "weight": 252, "cookies": false, "type": "", "deprecated": false, @@ -27601,6 +28040,16 @@ "description": "Whether the deployment should be automatically activated.", "x-example": true }, + "screenshotLight": { + "type": "string", + "description": "Screenshot with light theme preference file ID.", + "x-example": "5e5ea5c16897e" + }, + "screenshotDark": { + "type": "string", + "description": "Screenshot with dark theme preference file ID.", + "x-example": "5e5ea5c16897e" + }, "status": { "type": "string", "description": "The deployment status. Possible values are \"processing\", \"building\", \"waiting\", \"ready\", and \"failed\".", @@ -27617,11 +28066,6 @@ "x-example": 128, "format": "int32" }, - "domain": { - "type": "string", - "description": "Preview domain.", - "x-example": "deploy1-project1.appwrite.site" - }, "providerRepositoryName": { "type": "string", "description": "The name of the vcs provider repository", @@ -27685,10 +28129,11 @@ "buildSize", "buildId", "activate", + "screenshotLight", + "screenshotDark", "status", "buildLogs", "buildTime", - "domain", "providerRepositoryName", "providerRepositoryOwner", "providerRepositoryUrl", diff --git a/app/config/storage/inputs.php b/app/config/storage/inputs.php index 4532279b31..713801cd7c 100644 --- a/app/config/storage/inputs.php +++ b/app/config/storage/inputs.php @@ -7,4 +7,5 @@ return [ "gif" => "image/gif", "png" => "image/png", "heic" => "image/heic", + "webp" => "image/webp", ]; diff --git a/app/config/variables.php b/app/config/variables.php index 57810525c7..98dd9ffec1 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -572,7 +572,7 @@ return [ ], [ 'name' => '_APP_STORAGE_S3_ACCESS_KEY', - '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', + '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', 'introduction' => '0.13.0', 'default' => '', 'required' => false, @@ -580,7 +580,7 @@ return [ ], [ 'name' => '_APP_STORAGE_S3_SECRET', - '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.', + '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.', 'introduction' => '0.13.0', 'default' => '', 'required' => false, @@ -588,7 +588,7 @@ return [ ], [ 'name' => '_APP_STORAGE_S3_REGION', - '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.', + '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.', 'introduction' => '0.13.0', 'default' => 'us-east-1', 'required' => false, @@ -596,12 +596,20 @@ return [ ], [ 'name' => '_APP_STORAGE_S3_BUCKET', - 'description' => 'AWS S3 storage bucket. Required when storage adapter is set to S3. You can create buckets in your AWS console.', + 'description' => 'S3 storage bucket. Required when storage adapter is set to S3. You can create buckets in your S3 storage provider.', '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.', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index bd9562110f..21a04800b5 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -17,7 +17,7 @@ use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; -use Appwrite\Event\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\Hooks\Hooks; use Appwrite\Network\Validator\Email; @@ -2432,9 +2432,9 @@ App::post('/v1/account/tokens/phone') ->inject('queueForMessaging') ->inject('locale') ->inject('timelimit') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, Usage $queueForUsage, array $plan) { + ->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) { 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)) { - $queueForUsage + $queueForStatsUsage ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); } } - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) ->setProject($project) ->trigger(); @@ -3678,9 +3678,9 @@ App::post('/v1/account/verification/phone') ->inject('project') ->inject('locale') ->inject('timelimit') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, Usage $queueForUsage, array $plan) { + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) { if (empty(System::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -3775,11 +3775,11 @@ App::post('/v1/account/verification/phone') $countryCode = $helper->parse($phone)->getCountryCode(); if (!empty($countryCode)) { - $queueForUsage + $queueForStatsUsage ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); } } - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) ->setProject($project) ->trigger(); @@ -4310,9 +4310,9 @@ App::post('/v1/account/mfa/challenge') ->inject('queueForMessaging') ->inject('queueForMails') ->inject('timelimit') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Locale $locale, Document $project, Request $request, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails, callable $timelimit, Usage $queueForUsage, array $plan) { + ->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) { $expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM); $code = Auth::codeGenerator(); @@ -4383,11 +4383,11 @@ App::post('/v1/account/mfa/challenge') $countryCode = $helper->parse($phone)->getCountryCode(); if (!empty($countryCode)) { - $queueForUsage + $queueForStatsUsage ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); } } - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) ->setProject($project) ->trigger(); diff --git a/app/controllers/api/console.php b/app/controllers/api/console.php index d83cdc79f8..d4b191cd62 100644 --- a/app/controllers/api/console.php +++ b/app/controllers/api/console.php @@ -62,7 +62,8 @@ App::get('/v1/console/variables') '_APP_DOMAIN_ENABLED' => $isDomainEnabled, '_APP_ASSISTANT_ENABLED' => $isAssistantEnabled, '_APP_DOMAIN_SITES' => System::getEnv('_APP_DOMAIN_SITES'), - '_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS') + '_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS'), + '_APP_DOMAINS_NAMESERVERS' => System::getEnv('_APP_DOMAINS_NAMESERVERS'), ]); $response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES); diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index df46c1890b..c563d1e8e0 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -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\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\Network\Validator\Email; use Appwrite\SDK\AuthType; @@ -27,6 +27,7 @@ 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; @@ -393,6 +394,8 @@ 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()); } } @@ -476,8 +479,8 @@ App::post('/v1/databases') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('queueForUsage') - ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents, Usage $queueForUsage) { + ->inject('queueForStatsUsage') + ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { $databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId; @@ -527,7 +530,7 @@ App::post('/v1/databases') } $queueForEvents->setParam('databaseId', $database->getId()); - $queueForUsage->addMetric(str_replace(['{databaseInternalId}'], [$database->getInternalId()], METRIC_DATABASE_ID_STORAGE), 1); // per database + $queueForStatsUsage->addMetric(str_replace(['{databaseInternalId}'], [$database->getInternalId()], METRIC_DATABASE_ID_STORAGE), 1); // per database $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -797,8 +800,8 @@ App::delete('/v1/databases/:databaseId') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('queueForUsage') - ->action(function (string $databaseId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage) { + ->inject('queueForStatsUsage') + ->action(function (string $databaseId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) { $database = $dbForProject->getDocument('databases', $databaseId); @@ -821,7 +824,7 @@ App::delete('/v1/databases/:databaseId') ->setParam('databaseId', $database->getId()) ->setPayload($response->output($database, Response::MODEL_DATABASE)); - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_DATABASES_STORAGE, 1); // Global, deletion forces full recalculation $response->noContent(); @@ -2618,8 +2621,8 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('queueForUsage') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Usage $queueForUsage) { + ->inject('queueForStatsUsage') + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) { $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); @@ -2716,7 +2719,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->setContext('database', $db) ->setPayload($response->output($attribute, $model)); - $queueForUsage + $queueForStatsUsage ->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$db->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_STORAGE), 1); // per collection $response->noContent(); @@ -2822,7 +2825,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); + throw new Exception(Exception::ATTRIBUTE_UNKNOWN, 'Unknown attribute: ' . $attribute . '. Verify the attribute name or create the attribute.'); } $attributeStatus = $oldAttributes[$attributeIndex]['status']; @@ -3134,9 +3137,8 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('dbForProject') ->inject('user') ->inject('queueForEvents') - ->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) { + ->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) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -3339,7 +3341,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $processDocument($collection, $document); - $queueForUsage + $queueForStatsUsage ->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 @@ -3391,9 +3393,8 @@ 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('mode') - ->inject('queueForUsage') - ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, string $mode, Usage $queueForUsage) { + ->inject('queueForStatsUsage') + ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -3505,10 +3506,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') $processDocument($collection, $document); } - $queueForUsage + $queueForStatsUsage ->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); @@ -3570,9 +3570,8 @@ 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('mode') - ->inject('queueForUsage') - ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, string $mode, Usage $queueForUsage) { + ->inject('queueForStatsUsage') + ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -3648,10 +3647,9 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen $processDocument($collection, $document); - $queueForUsage + $queueForStatsUsage ->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); @@ -3804,9 +3802,8 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->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) { + ->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) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -3946,10 +3943,9 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $setCollection($collection, $newDocument); - $queueForUsage + $queueForStatsUsage ->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); @@ -4058,9 +4054,8 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('queueForUsage') - ->inject('mode') - ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, Usage $queueForUsage, string $mode) { + ->inject('queueForStatsUsage') + ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); @@ -4129,7 +4124,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu $processDocument($collection, $document); - $queueForUsage + $queueForStatsUsage ->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 diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php deleted file mode 100644 index 888ff35382..0000000000 --- a/app/controllers/api/functions.php +++ /dev/null @@ -1,1901 +0,0 @@ -groups(['api', 'functions']) - ->desc('List available function runtime specifications') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'listSpecifications', - description: '/docs/references/functions/list-specifications.md', - auth: [AuthType::KEY, AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_SPECIFICATION_LIST, - ) - ] - )) - ->inject('response') - ->inject('plan') - ->action(function (Response $response, array $plan) { - $allRuntimeSpecs = Config::getParam('runtime-specifications', []); - - $runtimeSpecs = []; - foreach ($allRuntimeSpecs as $spec) { - $spec['enabled'] = true; - - if (array_key_exists('runtimeSpecifications', $plan)) { - $spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']); - } - - // Only add specs that are within the limits set by environment variables - if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) { - $runtimeSpecs[] = $spec; - } - } - - $response->dynamic(new Document([ - 'specifications' => $runtimeSpecs, - 'total' => count($runtimeSpecs) - ]), Response::MODEL_SPECIFICATION_LIST); - }); - -App::get('/v1/functions/:functionId') - ->groups(['api', 'functions']) - ->desc('Get function') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'get', - description: '/docs/references/functions/get-function.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_FUNCTION, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, Response $response, Database $dbForProject) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $response->dynamic($function, Response::MODEL_FUNCTION); - }); - -App::get('/v1/functions/:functionId/usage') - ->desc('Get function usage') - ->groups(['api', 'functions', 'usage']) - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getFunctionUsage', - description: '/docs/references/functions/get-function-usage.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_USAGE_FUNCTION, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, string $range, Response $response, Database $dbForProject) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $periods = Config::getParam('usage', []); - $stats = $usage = []; - $days = $periods[$range]; - $metrics = [ - str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS), - str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), - str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS) - ]; - - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { - foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ - Query::equal('metric', [$metric]), - Query::equal('period', ['inf']) - ]); - - $stats[$metric]['total'] = $result['value'] ?? 0; - $limit = $days['limit']; - $period = $days['period']; - $results = $dbForProject->find('stats', [ - Query::equal('metric', [$metric]), - Query::equal('period', [$period]), - Query::limit($limit), - Query::orderDesc('time'), - ]); - $stats[$metric]['data'] = []; - foreach ($results as $result) { - $stats[$metric]['data'][$result->getAttribute('time')] = [ - 'value' => $result->getAttribute('value'), - ]; - } - } - }); - - $format = match ($days['period']) { - '1h' => 'Y-m-d\TH:00:00.000P', - '1d' => 'Y-m-d\T00:00:00.000P', - }; - - foreach ($metrics as $metric) { - $usage[$metric]['total'] = $stats[$metric]['total']; - $usage[$metric]['data'] = []; - $leap = time() - ($days['limit'] * $days['factor']); - while ($leap < time()) { - $leap += $days['factor']; - $formatDate = date($format, $leap); - $usage[$metric]['data'][] = [ - 'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0, - 'date' => $formatDate, - ]; - } - } - - $response->dynamic(new Document([ - 'range' => $range, - 'deploymentsTotal' => $usage[$metrics[0]]['total'], - 'deploymentsStorageTotal' => $usage[$metrics[1]]['total'], - 'buildsTotal' => $usage[$metrics[2]]['total'], - 'buildsStorageTotal' => $usage[$metrics[3]]['total'], - 'buildsTimeTotal' => $usage[$metrics[4]]['total'], - 'executionsTotal' => $usage[$metrics[5]]['total'], - 'executionsTimeTotal' => $usage[$metrics[6]]['total'], - 'deployments' => $usage[$metrics[0]]['data'], - 'deploymentsStorage' => $usage[$metrics[1]]['data'], - 'builds' => $usage[$metrics[2]]['data'], - 'buildsStorage' => $usage[$metrics[3]]['data'], - 'buildsTime' => $usage[$metrics[4]]['data'], - 'executions' => $usage[$metrics[5]]['data'], - 'executionsTime' => $usage[$metrics[6]]['data'], - 'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'], - 'buildsMbSeconds' => $usage[$metrics[7]]['data'], - 'executionsMbSeconds' => $usage[$metrics[8]]['data'], - 'executionsMbSecondsTotal' => $usage[$metrics[8]]['total'] - ]), Response::MODEL_USAGE_FUNCTION); - }); - -App::get('/v1/functions/usage') - ->desc('Get functions usage') - ->groups(['api', 'functions', 'usage']) - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getUsage', - description: '/docs/references/functions/get-functions-usage.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_USAGE_FUNCTIONS, - ) - ] - )) - ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $range, Response $response, Database $dbForProject) { - - $periods = Config::getParam('usage', []); - $stats = $usage = []; - $days = $periods[$range]; - $metrics = [ - METRIC_FUNCTIONS, - METRIC_DEPLOYMENTS, - METRIC_DEPLOYMENTS_STORAGE, - METRIC_BUILDS, - METRIC_BUILDS_STORAGE, - METRIC_BUILDS_COMPUTE, - METRIC_EXECUTIONS, - METRIC_EXECUTIONS_COMPUTE, - METRIC_BUILDS_MB_SECONDS, - METRIC_EXECUTIONS_MB_SECONDS, - ]; - - Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { - foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ - Query::equal('metric', [$metric]), - Query::equal('period', ['inf']) - ]); - - $stats[$metric]['total'] = $result['value'] ?? 0; - $limit = $days['limit']; - $period = $days['period']; - $results = $dbForProject->find('stats', [ - Query::equal('metric', [$metric]), - Query::equal('period', [$period]), - Query::limit($limit), - Query::orderDesc('time'), - ]); - $stats[$metric]['data'] = []; - foreach ($results as $result) { - $stats[$metric]['data'][$result->getAttribute('time')] = [ - 'value' => $result->getAttribute('value'), - ]; - } - } - }); - - $format = match ($days['period']) { - '1h' => 'Y-m-d\TH:00:00.000P', - '1d' => 'Y-m-d\T00:00:00.000P', - }; - - foreach ($metrics as $metric) { - $usage[$metric]['total'] = $stats[$metric]['total']; - $usage[$metric]['data'] = []; - $leap = time() - ($days['limit'] * $days['factor']); - while ($leap < time()) { - $leap += $days['factor']; - $formatDate = date($format, $leap); - $usage[$metric]['data'][] = [ - 'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0, - 'date' => $formatDate, - ]; - } - } - $response->dynamic(new Document([ - 'range' => $range, - 'functionsTotal' => $usage[$metrics[0]]['total'], - 'deploymentsTotal' => $usage[$metrics[1]]['total'], - 'deploymentsStorageTotal' => $usage[$metrics[2]]['total'], - 'buildsTotal' => $usage[$metrics[3]]['total'], - 'buildsStorageTotal' => $usage[$metrics[4]]['total'], - 'buildsTimeTotal' => $usage[$metrics[5]]['total'], - 'executionsTotal' => $usage[$metrics[6]]['total'], - 'executionsTimeTotal' => $usage[$metrics[7]]['total'], - 'functions' => $usage[$metrics[0]]['data'], - 'deployments' => $usage[$metrics[1]]['data'], - 'deploymentsStorage' => $usage[$metrics[2]]['data'], - 'builds' => $usage[$metrics[3]]['data'], - 'buildsStorage' => $usage[$metrics[4]]['data'], - 'buildsTime' => $usage[$metrics[5]]['data'], - 'executions' => $usage[$metrics[6]]['data'], - 'executionsTime' => $usage[$metrics[7]]['data'], - 'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'], - 'buildsMbSeconds' => $usage[$metrics[8]]['data'], - 'executionsMbSeconds' => $usage[$metrics[9]]['data'], - 'executionsMbSecondsTotal' => $usage[$metrics[9]]['total'], - ]), Response::MODEL_USAGE_FUNCTIONS); - }); - -App::get('/v1/functions/:functionId/deployments/:deploymentId/download') - ->groups(['api', 'functions']) - ->desc('Download deployment') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getDeploymentDownload', - description: '/docs/references/functions/get-deployment-download.md', - auth: [AuthType::KEY, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::ANY, - type: MethodType::LOCATION - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('request') - ->inject('dbForProject') - ->inject('deviceForFunctions') - ->action(function (string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions) { - - $function = $dbForProject->getDocument('functions', $functionId); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - if ($deployment->getAttribute('resourceId') !== $function->getId()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $path = $deployment->getAttribute('path', ''); - if (!$deviceForFunctions->exists($path)) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $response - ->setContentType('application/gzip') - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->addHeader('X-Peak', \memory_get_peak_usage()) - ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); - - $size = $deviceForFunctions->getFileSize($path); - $rangeHeader = $request->getHeader('range'); - - if (!empty($rangeHeader)) { - $start = $request->getRangeStart(); - $end = $request->getRangeEnd(); - $unit = $request->getRangeUnit(); - - if ($end === null) { - $end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1)); - } - - if ($unit !== 'bytes' || $start >= $end || $end >= $size) { - throw new Exception(Exception::STORAGE_INVALID_RANGE); - } - - $response - ->addHeader('Accept-Ranges', 'bytes') - ->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size) - ->addHeader('Content-Length', $end - $start + 1) - ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); - - $response->send($deviceForFunctions->read($path, $start, ($end - $start + 1))); - } - - if ($size > APP_STORAGE_READ_BUFFER) { - for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { - $response->chunk( - $deviceForFunctions->read( - $path, - ($i * MAX_OUTPUT_CHUNK_SIZE), - min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) - ), - (($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size - ); - } - } else { - $response->send($deviceForFunctions->read($path)); - } - }); - -App::patch('/v1/functions/:functionId/deployments/:deploymentId') - ->groups(['api', 'functions']) - ->desc('Update deployment') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].deployments.[deploymentId].update') - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'updateDeployment') - ->label('sdk.description', '/docs/references/functions/update-function-deployment.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_FUNCTION) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->inject('dbForPlatform') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) { - - $function = $dbForProject->getDocument('functions', $functionId); - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - if ($build->isEmpty()) { - throw new Exception(Exception::BUILD_NOT_FOUND); - } - - if ($build->getAttribute('status') !== 'ready') { - throw new Exception(Exception::BUILD_NOT_READY); - } - - $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ - 'deploymentInternalId' => $deployment->getInternalId(), - 'deployment' => $deployment->getId(), - ]))); - - // Inform scheduler if function is still active - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $function->getAttribute('schedule')) - ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->dynamic($function, Response::MODEL_FUNCTION); - }); - -App::delete('/v1/functions/:functionId') - ->groups(['api', 'functions']) - ->desc('Delete function') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].delete') - ->label('audits.event', 'function.delete') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'delete', - description: '/docs/references/functions/delete-function.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDeletes') - ->inject('queueForEvents') - ->inject('dbForPlatform') - ->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForPlatform) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - if (!$dbForProject->deleteDocument('functions', $function->getId())) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove function from DB'); - } - - // Inform scheduler to no longer run function - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('active', false); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($function); - - $queueForEvents->setParam('functionId', $function->getId()); - - $response->noContent(); - }); - -App::get('/v1/functions/:functionId/deployments') - ->groups(['api', 'functions']) - ->desc('List deployments') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'listDeployments', - description: '/docs/references/functions/list-deployments.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DEPLOYMENT_LIST, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('queries', [], new Deployments(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true) - ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - try { - $queries = Query::parseQueries($queries); - } catch (QueryException $e) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); - } - - if (!empty($search)) { - $queries[] = Query::search('search', $search); - } - - // Set resource queries - $queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]); - $queries[] = Query::equal('resourceType', ['functions']); - - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ - - $validator = new Cursor(); - if (!$validator->isValid($cursor)) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); - } - - $deploymentId = $cursor->getValue(); - $cursorDocument = $dbForProject->getDocument('deployments', $deploymentId); - - if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found."); - } - - $cursor->setValue($cursorDocument); - } - - $filterQueries = Query::groupByType($queries)['filters']; - - $results = $dbForProject->find('deployments', $queries); - $total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT); - - foreach ($results as $result) { - $build = $dbForProject->getDocument('builds', $result->getAttribute('buildId', '')); - $result->setAttribute('status', $build->getAttribute('status', 'processing')); - $result->setAttribute('buildLogs', $build->getAttribute('logs', '')); - $result->setAttribute('buildTime', $build->getAttribute('duration', 0)); - $result->setAttribute('buildSize', $build->getAttribute('size', 0)); - $result->setAttribute('size', $result->getAttribute('size', 0)); - } - - $response->dynamic(new Document([ - 'deployments' => $results, - 'total' => $total, - ]), Response::MODEL_DEPLOYMENT_LIST); - }); - -App::get('/v1/functions/:functionId/deployments/:deploymentId') - ->groups(['api', 'functions']) - ->desc('Get deployment') - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getDeployment', - description: '/docs/references/functions/get-deployment.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DEPLOYMENT, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - - if ($deployment->getAttribute('resourceId') !== $function->getId()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); - $deployment->setAttribute('status', $build->getAttribute('status', 'waiting')); - $deployment->setAttribute('buildLogs', $build->getAttribute('logs', '')); - $deployment->setAttribute('buildTime', $build->getAttribute('duration', 0)); - $deployment->setAttribute('buildSize', $build->getAttribute('size', 0)); - $deployment->setAttribute('size', $deployment->getAttribute('size', 0)); - - $response->dynamic($deployment, Response::MODEL_DEPLOYMENT); - }); - -App::delete('/v1/functions/:functionId/deployments/:deploymentId') - ->groups(['api', 'functions']) - ->desc('Delete deployment') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].deployments.[deploymentId].delete') - ->label('audits.event', 'deployment.delete') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'deleteDeployment', - description: '/docs/references/functions/delete-deployment.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('queueForDeletes') - ->inject('queueForEvents') - ->inject('deviceForFunctions') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Device $deviceForFunctions) { - - $function = $dbForProject->getDocument('functions', $functionId); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - if ($deployment->getAttribute('resourceId') !== $function->getId()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - if (!$dbForProject->deleteDocument('deployments', $deployment->getId())) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB'); - } - - if (!empty($deployment->getAttribute('path', ''))) { - if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage'); - } - } - - if ($function->getAttribute('deployment') === $deployment->getId()) { // Reset function deployment - $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ - 'deployment' => '', - 'deploymentInternalId' => '', - ]))); - } - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $queueForDeletes - ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($deployment); - - $response->noContent(); - }); - -App::post('/v1/functions/:functionId/deployments/:deploymentId/build') - ->alias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') - ->groups(['api', 'functions']) - ->desc('Rebuild deployment') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].deployments.[deploymentId].update') - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'createBuild', - description: '/docs/references/functions/create-build.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility - ->inject('request') - ->inject('response') - ->inject('dbForProject') - ->inject('project') - ->inject('queueForEvents') - ->inject('queueForBuilds') - ->inject('deviceForFunctions') - ->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $path = $deployment->getAttribute('path'); - if (empty($path) || !$deviceForFunctions->exists($path)) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $deploymentId = ID::unique(); - - $destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - $deviceForFunctions->transfer($path, $destination, $deviceForFunctions); - - $deployment->removeAttribute('$internalId'); - $deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([ - '$internalId' => '', - '$id' => $deploymentId, - 'buildId' => '', - 'buildInternalId' => '', - 'path' => $destination, - 'entrypoint' => $function->getAttribute('entrypoint'), - 'commands' => $function->getAttribute('commands', ''), - 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]), - ])); - - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($function) - ->setDeployment($deployment); - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->noContent(); - }); - -App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') - ->groups(['api', 'functions']) - ->desc('Cancel deployment') - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'updateDeploymentBuild') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_BUILD) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('project') - ->inject('queueForEvents') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); - - if ($build->isEmpty()) { - $buildId = ID::unique(); - $build = $dbForProject->createDocument('builds', new Document([ - '$id' => $buildId, - '$permissions' => [], - 'startTime' => DateTime::now(), - 'deploymentInternalId' => $deployment->getInternalId(), - 'deploymentId' => $deployment->getId(), - 'status' => 'canceled', - 'path' => '', - 'runtime' => $function->getAttribute('runtime'), - 'source' => $deployment->getAttribute('path', ''), - 'sourceType' => '', - 'logs' => '', - 'duration' => 0, - 'size' => 0 - ])); - - $deployment->setAttribute('buildId', $build->getId()); - $deployment->setAttribute('buildInternalId', $build->getInternalId()); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - } else { - if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) { - throw new Exception(Exception::BUILD_ALREADY_COMPLETED); - } - - $startTime = new \DateTime($build->getAttribute('startTime')); - $endTime = new \DateTime('now'); - $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); - - $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([ - 'endTime' => DateTime::now(), - 'duration' => $duration, - 'status' => 'canceled' - ])); - } - - $dbForProject->purgeCachedDocument('deployments', $deployment->getId()); - - try { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); - $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); - } catch (\Throwable $th) { - // Don't throw if the deployment doesn't exist - if ($th->getCode() !== 404) { - throw $th; - } - } - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->dynamic($build, Response::MODEL_BUILD); - }); - -App::post('/v1/functions/:functionId/executions') - ->groups(['api', 'functions']) - ->desc('Create execution') - ->label('scope', 'execution.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].executions.[executionId].create') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'createExecution', - description: '/docs/references/functions/create-execution.md', - auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_EXECUTION, - ) - ], - contentType: ContentType::MULTIPART, - requestType: 'application/json', - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true) - ->param('async', false, new Boolean(true), 'Execute code in the background. Default value is false.', true) - ->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true) - ->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'HTTP method of execution. Default value is GET.', true) - ->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true) - ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_MINUTES, offset: 60), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true) - ->inject('response') - ->inject('request') - ->inject('project') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('user') - ->inject('queueForEvents') - ->inject('queueForUsage') - ->inject('queueForFunctions') - ->inject('geodb') - ->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) { - $async = \strval($async) === 'true' || \strval($async) === '1'; - - if (!$async && !is_null($scheduledAt)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Scheduled executions must run asynchronously. Set scheduledAt to a future date, or set async to true.'); - } - - /** - * @var array $headers - */ - $assocParams = ['headers']; - foreach ($assocParams as $assocParam) { - if (!empty('headers') && !is_array($$assocParam)) { - $$assocParam = \json_decode($$assocParam, true); - } - } - - $booleanParams = ['async']; - foreach ($booleanParams as $booleamParam) { - if (!empty($$booleamParam) && !is_bool($$booleamParam)) { - $$booleamParam = $$booleamParam === "true" ? true : false; - } - } - - // 'headers' validator - $validator = new Headers(); - if (!$validator->isValid($headers)) { - throw new Exception($validator->getDescription(), 400); - } - - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - - if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $version = $function->getAttribute('version', 'v2'); - $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); - $spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; - - $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; - - if (\is_null($runtime)) { - throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); - } - - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); - - if ($deployment->getAttribute('resourceId') !== $function->getId()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - - /** Check if build has completed */ - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); - if ($build->isEmpty()) { - throw new Exception(Exception::BUILD_NOT_FOUND); - } - - if ($build->getAttribute('status') !== 'ready') { - throw new Exception(Exception::BUILD_NOT_READY); - } - - $validator = new Authorization('execute'); - - if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function - throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); - } - - $jwt = ''; // initialize - if (!$user->isEmpty()) { // If userId exists, generate a JWT for function - $sessions = $user->getAttribute('sessions', []); - $current = new Document(); - - foreach ($sessions as $session) { - /** @var Utopia\Database\Document $session */ - if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too - $current = $session; - } - } - - if (!$current->isEmpty()) { - $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); - $jwt = $jwtObj->encode([ - 'userId' => $user->getId(), - 'sessionId' => $current->getId(), - ]); - } - } - - $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); - $apiKey = $jwtObj->encode([ - 'projectId' => $project->getId(), - 'scopes' => $function->getAttribute('scopes', []) - ]); - - $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; - $headers['x-appwrite-trigger'] = 'http'; - $headers['x-appwrite-user-id'] = $user->getId() ?? ''; - $headers['x-appwrite-user-jwt'] = $jwt ?? ''; - $headers['x-appwrite-country-code'] = ''; - $headers['x-appwrite-continent-code'] = ''; - $headers['x-appwrite-continent-eu'] = 'false'; - - $ip = $headers['x-real-ip'] ?? ''; - if (!empty($ip)) { - $record = $geodb->get($ip); - - if ($record) { - $eu = Config::getParam('locale-eu'); - - $headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? ''; - $headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? ''; - $headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false'; - } - } - - $headersFiltered = []; - foreach ($headers as $key => $value) { - if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { - $headersFiltered[] = ['name' => $key, 'value' => $value]; - } - } - - $executionId = ID::unique(); - - $status = $async ? 'waiting' : 'processing'; - - if (!is_null($scheduledAt)) { - $status = 'scheduled'; - } - - $execution = new Document([ - '$id' => $executionId, - '$permissions' => !$user->isEmpty() ? [Permission::read(Role::user($user->getId()))] : [], - 'resourceInternalId' => $function->getInternalId(), - 'resourceId' => $function->getId(), - 'resourceType' => 'functions', - 'deploymentInternalId' => $deployment->getInternalId(), - 'deploymentId' => $deployment->getId(), - 'trigger' => (!is_null($scheduledAt)) ? 'schedule' : 'http', - 'status' => $status, // waiting / processing / completed / failed / scheduled - 'responseStatusCode' => 0, - 'responseHeaders' => [], - 'requestPath' => $path, - 'requestMethod' => $method, - 'requestHeaders' => $headersFiltered, - 'errors' => '', - 'logs' => '', - 'duration' => 0.0, - 'search' => implode(' ', [$functionId, $executionId]), - ]); - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('executionId', $execution->getId()) - ->setContext('function', $function); - - if ($async) { - if (is_null($scheduledAt)) { - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); - $queueForFunctions - ->setType('http') - ->setExecution($execution) - ->setFunction($function) - ->setBody($body) - ->setHeaders($headers) - ->setPath($path) - ->setMethod($method) - ->setJWT($jwt) - ->setProject($project) - ->setUser($user) - ->setParam('functionId', $function->getId()) - ->setParam('executionId', $execution->getId()) - ->trigger(); - } else { - $data = [ - 'headers' => $headers, - 'path' => $path, - 'method' => $method, - 'body' => $body, - 'userId' => $user->getId() - ]; - - $schedule = $dbForPlatform->createDocument('schedules', new Document([ - 'region' => System::getEnv('_APP_REGION', 'default'), - 'resourceType' => ScheduleExecutions::getSupportedResource(), - 'resourceId' => $execution->getId(), - 'resourceInternalId' => $execution->getInternalId(), - 'resourceUpdatedAt' => DateTime::now(), - 'projectId' => $project->getId(), - 'schedule' => $scheduledAt, - 'data' => $data, - 'active' => true, - ])); - - $execution = $execution - ->setAttribute('scheduleId', $schedule->getId()) - ->setAttribute('scheduleInternalId', $schedule->getInternalId()) - ->setAttribute('scheduledAt', $scheduledAt); - - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); - } - - return $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($execution, Response::MODEL_EXECUTION); - } - - $durationStart = \microtime(true); - - $vars = []; - - // V2 vars - if ($version === 'v2') { - $vars = \array_merge($vars, [ - 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', - 'APPWRITE_FUNCTION_DATA' => $body ?? '', - 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', - 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' - ]); - } - - // Shared vars - foreach ($function->getAttribute('varsProject', []) as $var) { - $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); - } - - // Function vars - foreach ($function->getAttribute('vars', []) as $var) { - $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); - } - - $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; - $hostname = System::getEnv('_APP_DOMAIN'); - $endpoint = $protocol . '://' . $hostname . "/v1"; - - // Appwrite vars - $vars = \array_merge($vars, [ - 'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint, - 'APPWRITE_FUNCTION_ID' => $functionId, - 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'), - 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), - 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), - 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', - 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', - 'APPWRITE_COMPUTE_CPUS' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - 'APPWRITE_COMPUTE_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - 'APPWRITE_VERSION' => APP_VERSION_STABLE, - 'APPWRITE_REGION' => $project->getAttribute('region'), - 'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''), - 'APPWRITE_VCS_REPOSITORY_ID' => $deployment->getAttribute('providerRepositoryId', ''), - 'APPWRITE_VCS_REPOSITORY_NAME' => $deployment->getAttribute('providerRepositoryName', ''), - 'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment->getAttribute('providerRepositoryOwner', ''), - 'APPWRITE_VCS_REPOSITORY_URL' => $deployment->getAttribute('providerRepositoryUrl', ''), - 'APPWRITE_VCS_REPOSITORY_BRANCH' => $deployment->getAttribute('providerBranch', ''), - 'APPWRITE_VCS_REPOSITORY_BRANCH_URL' => $deployment->getAttribute('providerBranchUrl', ''), - 'APPWRITE_VCS_COMMIT_HASH' => $deployment->getAttribute('providerCommitHash', ''), - 'APPWRITE_VCS_COMMIT_MESSAGE' => $deployment->getAttribute('providerCommitMessage', ''), - 'APPWRITE_VCS_COMMIT_URL' => $deployment->getAttribute('providerCommitUrl', ''), - 'APPWRITE_VCS_COMMIT_AUTHOR_NAME' => $deployment->getAttribute('providerCommitAuthor', ''), - 'APPWRITE_VCS_COMMIT_AUTHOR_URL' => $deployment->getAttribute('providerCommitAuthorUrl', ''), - 'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''), - ]); - - /** Execute function */ - $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); - try { - $version = $function->getAttribute('version', 'v2'); - $command = $runtime['startCommand']; - $command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'; - $executionResponse = $executor->createExecution( - projectId: $project->getId(), - deploymentId: $deployment->getId(), - body: \strlen($body) > 0 ? $body : null, - variables: $vars, - timeout: $function->getAttribute('timeout', 0), - image: $runtime['image'], - source: $build->getAttribute('path', ''), - entrypoint: $deployment->getAttribute('entrypoint', ''), - version: $version, - path: $path, - method: $method, - headers: $headers, - runtimeEntrypoint: $command, - cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, - logging: $function->getAttribute('logging', true), - requestTimeout: 30 - ); - - $headersFiltered = []; - foreach ($executionResponse['headers'] as $key => $value) { - if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { - $headersFiltered[] = ['name' => $key, 'value' => $value]; - } - } - - /** Update execution status */ - $status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed'; - $execution->setAttribute('status', $status); - $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); - $execution->setAttribute('responseHeaders', $headersFiltered); - $execution->setAttribute('logs', $executionResponse['logs']); - $execution->setAttribute('errors', $executionResponse['errors']); - $execution->setAttribute('duration', $executionResponse['duration']); - } catch (\Throwable $th) { - $durationEnd = \microtime(true); - - $execution - ->setAttribute('duration', $durationEnd - $durationStart) - ->setAttribute('status', 'failed') - ->setAttribute('responseStatusCode', 500) - ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); - Console::error($th->getMessage()); - - if ($th instanceof AppwriteException) { - throw $th; - } - } finally { - $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 - ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function - ->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) - ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) - ; - - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); - } - - $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); - - if (!$isPrivilegedUser && !$isAppUser) { - $execution->setAttribute('logs', ''); - $execution->setAttribute('errors', ''); - } - - $headers = []; - foreach (($executionResponse['headers'] ?? []) as $key => $value) { - $headers[] = ['name' => $key, 'value' => $value]; - } - - $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); - $execution->setAttribute('responseHeaders', $headers); - - $acceptTypes = \explode(', ', $request->getHeader('accept')); - foreach ($acceptTypes as $acceptType) { - if (\str_starts_with($acceptType, 'application/json') || \str_starts_with($acceptType, 'application/*')) { - $response->setContentType(Response::CONTENT_TYPE_JSON); - break; - } elseif (\str_starts_with($acceptType, 'multipart/form-data') || \str_starts_with($acceptType, 'multipart/*')) { - $response->setContentType(Response::CONTENT_TYPE_MULTIPART); - break; - } - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($execution, Response::MODEL_EXECUTION); - }); - -App::get('/v1/functions/:functionId/executions') - ->groups(['api', 'functions']) - ->desc('List executions') - ->label('scope', 'execution.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'listExecutions', - description: '/docs/references/functions/list-executions.md', - auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_EXECUTION_LIST, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('queries', [], new Executions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Executions::ALLOWED_ATTRIBUTES), true) - ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('mode') - ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - - if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - try { - $queries = Query::parseQueries($queries); - } catch (QueryException $e) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); - } - - if (!empty($search)) { - $queries[] = Query::search('search', $search); - } - - // Set internal queries - $queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]); - $queries[] = Query::equal('resourceType', ['functions']); - - /** - * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries - */ - $cursor = \array_filter($queries, function ($query) { - return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); - }); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ - - $validator = new Cursor(); - if (!$validator->isValid($cursor)) { - throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); - } - - $executionId = $cursor->getValue(); - $cursorDocument = $dbForProject->getDocument('executions', $executionId); - - if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found."); - } - - $cursor->setValue($cursorDocument); - } - - $filterQueries = Query::groupByType($queries)['filters']; - - $results = $dbForProject->find('executions', $queries); - $total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT); - - $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); - if (!$isPrivilegedUser && !$isAppUser) { - $results = array_map(function ($execution) { - $execution->setAttribute('logs', ''); - $execution->setAttribute('errors', ''); - return $execution; - }, $results); - } - - $response->dynamic(new Document([ - 'executions' => $results, - 'total' => $total, - ]), Response::MODEL_EXECUTION_LIST); - }); - -App::get('/v1/functions/:functionId/executions/:executionId') - ->groups(['api', 'functions']) - ->desc('Get execution') - ->label('scope', 'execution.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getExecution', - description: '/docs/references/functions/get-execution.md', - auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_EXECUTION, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('executionId', '', new UID(), 'Execution ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('mode') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode) { - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - - if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $execution = $dbForProject->getDocument('executions', $executionId); - - if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { - throw new Exception(Exception::EXECUTION_NOT_FOUND); - } - - if ($execution->isEmpty()) { - throw new Exception(Exception::EXECUTION_NOT_FOUND); - } - - $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); - if (!$isPrivilegedUser && !$isAppUser) { - $execution->setAttribute('logs', ''); - $execution->setAttribute('errors', ''); - } - - $response->dynamic($execution, Response::MODEL_EXECUTION); - }); - -App::delete('/v1/functions/:functionId/executions/:executionId') - ->groups(['api', 'functions']) - ->desc('Delete execution') - ->label('scope', 'execution.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].executions.[executionId].delete') - ->label('audits.event', 'executions.delete') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'deleteExecution', - description: '/docs/references/functions/delete-execution.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_NOCONTENT, - model: Response::MODEL_NONE, - ) - ], - contentType: ContentType::NONE - )) - ->param('functionId', '', new UID(), 'Function ID.') - ->param('executionId', '', new UID(), 'Execution ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('queueForEvents') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $execution = $dbForProject->getDocument('executions', $executionId); - if ($execution->isEmpty()) { - throw new Exception(Exception::EXECUTION_NOT_FOUND); - } - - if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { - throw new Exception(Exception::EXECUTION_NOT_FOUND); - } - $status = $execution->getAttribute('status'); - - if (!in_array($status, ['completed', 'failed', 'scheduled'])) { - throw new Exception(Exception::EXECUTION_IN_PROGRESS); - } - - if (!$dbForProject->deleteDocument('executions', $execution->getId())) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove execution from DB'); - } - - if ($status === 'scheduled') { - $schedule = $dbForPlatform->findOne('schedules', [ - Query::equal('resourceId', [$execution->getId()]), - Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]), - Query::equal('active', [true]), - ]); - - if (!$schedule->isEmpty()) { - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('active', false); - - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - } - } - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('executionId', $execution->getId()) - ->setPayload($response->output($execution, Response::MODEL_EXECUTION)); - - $response->noContent(); - }); - -// Variables - -App::post('/v1/functions/:functionId/variables') - ->desc('Create variable') - ->groups(['api', 'functions']) - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('audits.event', 'variable.create') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'createVariable', - description: '/docs/references/functions/create-variable.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_VARIABLE, - ) - ] - )) - ->param('functionId', '', new UID(), 'Function unique ID.', false) - ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) - ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) - ->param('secret', false, new Boolean(), 'Is secret? Secret variables can only be updated or deleted, they cannot be read.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->action(function (string $functionId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $variableId = ID::unique(); - - $variable = new Document([ - '$id' => $variableId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceInternalId' => $function->getInternalId(), - 'resourceId' => $function->getId(), - 'resourceType' => 'function', - 'key' => $key, - 'value' => $value, - 'secret' => $secret, - 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']), - ]); - - try { - $variable = $dbForProject->createDocument('variables', $variable); - } catch (DuplicateException $th) { - throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); - } - - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); - - // Inform scheduler to pull the latest changes - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $function->getAttribute('schedule')) - ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($variable, Response::MODEL_VARIABLE); - }); - -App::get('/v1/functions/:functionId/variables') - ->desc('List variables') - ->groups(['api', 'functions']) - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label( - 'sdk', - new Method( - namespace: 'functions', - name: 'listVariables', - description: '/docs/references/functions/list-variables.md', - auth: [AuthType::KEY], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_VARIABLE_LIST, - ) - ], - ) - ) - ->param('functionId', '', new UID(), 'Function unique ID.', false) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, Response $response, Database $dbForProject) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $response->dynamic(new Document([ - 'variables' => $function->getAttribute('vars', []), - 'total' => \count($function->getAttribute('vars', [])), - ]), Response::MODEL_VARIABLE_LIST); - }); - -App::get('/v1/functions/:functionId/variables/:variableId') - ->desc('Get variable') - ->groups(['api', 'functions']) - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'getVariable') - ->label('sdk.description', '/docs/references/functions/get-variable.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_VARIABLE) - ->param('functionId', '', new UID(), 'Function unique ID.', false) - ->param('variableId', '', new UID(), 'Variable unique ID.', false) - ->inject('response') - ->inject('dbForProject') - ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $variable = $dbForProject->getDocument('variables', $variableId); - if ( - $variable === false || - $variable->isEmpty() || - $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || - $variable->getAttribute('resourceType') !== 'function' - ) { - throw new Exception(Exception::VARIABLE_NOT_FOUND); - } - - if ($variable === false || $variable->isEmpty()) { - throw new Exception(Exception::VARIABLE_NOT_FOUND); - } - - $response->dynamic($variable, Response::MODEL_VARIABLE); - }); - -App::put('/v1/functions/:functionId/variables/:variableId') - ->desc('Update variable') - ->groups(['api', 'functions']) - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('audits.event', 'variable.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'updateVariable') - ->label('sdk.description', '/docs/references/functions/update-variable.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_VARIABLE) - ->param('functionId', '', new UID(), 'Function unique ID.', false) - ->param('variableId', '', new UID(), 'Variable unique ID.', false) - ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false) - ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true) - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForPlatform) { - - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $variable = $dbForProject->getDocument('variables', $variableId); - if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { - throw new Exception(Exception::VARIABLE_NOT_FOUND); - } - - if ($variable === false || $variable->isEmpty()) { - throw new Exception(Exception::VARIABLE_NOT_FOUND); - } - - $variable - ->setAttribute('key', $key) - ->setAttribute('value', $value ?? $variable->getAttribute('value')) - ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); - - try { - $dbForProject->updateDocument('variables', $variable->getId(), $variable); - } catch (DuplicateException $th) { - throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); - } - - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); - - // Inform scheduler to pull the latest changes - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $function->getAttribute('schedule')) - ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $response->dynamic($variable, Response::MODEL_VARIABLE); - }); - -App::delete('/v1/functions/:functionId/variables/:variableId') - ->desc('Delete variable') - ->groups(['api', 'functions']) - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('audits.event', 'variable.delete') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'functions') - ->label('sdk.method', 'deleteVariable') - ->label('sdk.description', '/docs/references/functions/delete-variable.md') - ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) - ->label('sdk.response.model', Response::MODEL_NONE) - ->param('functionId', '', new UID(), 'Function unique ID.', false) - ->param('variableId', '', new UID(), 'Variable unique ID.', false) - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForPlatform) { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $variable = $dbForProject->getDocument('variables', $variableId); - if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { - throw new Exception(Exception::VARIABLE_NOT_FOUND); - } - - if ($variable === false || $variable->isEmpty()) { - throw new Exception(Exception::VARIABLE_NOT_FOUND); - } - - $dbForProject->deleteDocument('variables', $variable->getId()); - - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); - - // Inform scheduler to pull the latest changes - $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $function->getAttribute('schedule')) - ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); - Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); - - $response->noContent(); - }); - -App::get('/v1/functions/templates') - ->groups(['api']) - ->desc('List function templates') - ->label('scope', 'public') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'listTemplates', - description: '/docs/references/functions/list-templates.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_TEMPLATE_FUNCTION_LIST, - ) - ] - )) - ->param('runtimes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('runtimes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of runtimes allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' runtimes are allowed.', true) - ->param('useCases', [], new ArrayList(new WhiteList(['dev-tools','starter','databases','ai','messaging','utilities']), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true) - ->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true) - ->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true) - ->inject('response') - ->action(function (array $runtimes, array $usecases, int $limit, int $offset, Response $response) { - $templates = Config::getParam('function-templates', []); - - if (!empty($runtimes)) { - $templates = \array_filter($templates, function ($template) use ($runtimes) { - return \count(\array_intersect($runtimes, \array_column($template['runtimes'], 'name'))) > 0; - }); - } - - if (!empty($usecases)) { - $templates = \array_filter($templates, function ($template) use ($usecases) { - return \count(\array_intersect($usecases, $template['useCases'])) > 0; - }); - } - - $responseTemplates = \array_slice($templates, $offset, $limit); - $response->dynamic(new Document([ - 'templates' => $responseTemplates, - 'total' => \count($responseTemplates), - ]), Response::MODEL_TEMPLATE_FUNCTION_LIST); - }); - -App::get('/v1/functions/templates/:templateId') - ->desc('Get function template') - ->label('scope', 'public') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getTemplate', - description: '/docs/references/functions/get-template.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_TEMPLATE_FUNCTION, - ) - ] - )) - ->param('templateId', '', new Text(128), 'Template ID.') - ->inject('response') - ->action(function (string $templateId, Response $response) { - $templates = Config::getParam('function-templates', []); - - $filtered = \array_filter($templates, function ($template) use ($templateId) { - return $template['id'] === $templateId; - }); - - $template = array_shift($filtered); - - if (empty($template)) { - throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND); - } - - $response->dynamic(new Document($template), Response::MODEL_TEMPLATE_FUNCTION); - }); diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 305444abc5..94ec2059c9 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -692,15 +692,15 @@ App::get('/v1/health/queue/functions') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/usage') - ->desc('Get usage queue') +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: 'getQueueUsage', - description: '/docs/references/health/get-queue-usage.md', + name: 'getQueueStatsResources', + description: '/docs/references/health/get-queue-stats-resources.md', responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -715,7 +715,7 @@ App::get('/v1/health/queue/usage') ->action(function (int|string $threshold, Publisher $publisher, Response $response) { $threshold = \intval($threshold); - $size = $publisher->getQueueSize(new Queue(Event::USAGE_QUEUE_NAME)); + $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}."); @@ -724,15 +724,15 @@ App::get('/v1/health/queue/usage') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }); -App::get('/v1/health/queue/usage-dump') - ->desc('Get usage dump queue') +App::get('/v1/health/queue/stats-usage') + ->desc('Get stats usage queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( auth: [AuthType::KEY], namespace: 'health', - name: 'getQueueUsageDump', - description: '/docs/references/health/get-queue-usage-dump.md', + name: 'getQueueUsage', + description: '/docs/references/health/get-queue-stats-usage.md', responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -747,7 +747,39 @@ App::get('/v1/health/queue/usage-dump') ->action(function (int|string $threshold, Publisher $publisher, Response $response) { $threshold = \intval($threshold); - $size = $publisher->getQueueSize(new Queue(Event::USAGE_DUMP_QUEUE_NAME)); + $size = $publisher->getQueueSize(new Queue(Event::STATS_USAGE_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-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', + 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_USAGE_DUMP_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}."); @@ -825,9 +857,10 @@ App::get('/v1/health/storage') ->inject('response') ->inject('deviceForFiles') ->inject('deviceForFunctions') + ->inject('deviceForSites') ->inject('deviceForBuilds') - ->action(function (Response $response, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds) { - $devices = [$deviceForFiles, $deviceForFunctions, $deviceForBuilds]; + ->action(function (Response $response, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForSites, Device $deviceForBuilds) { + $devices = [$deviceForFiles, $deviceForFunctions, $deviceForSites, $deviceForBuilds]; $checkStart = \microtime(true); foreach ($devices as $device) { @@ -920,8 +953,9 @@ App::get('/v1/health/queue/failed/:name') Event::AUDITS_QUEUE_NAME, Event::MAILS_QUEUE_NAME, Event::FUNCTIONS_QUEUE_NAME, - Event::USAGE_QUEUE_NAME, - Event::USAGE_DUMP_QUEUE_NAME, + Event::STATS_RESOURCES_QUEUE_NAME, + Event::STATS_USAGE_QUEUE_NAME, + Event::STATS_USAGE_DUMP_QUEUE_NAME, Event::WEBHOOK_QUEUE_NAME, Event::CERTIFICATES_QUEUE_NAME, Event::BUILDS_QUEUE_NAME, diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index ac149ac8eb..75afc7ed2c 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -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's Appwrite Endpoint") - ->param('projectId', '', new UID(), "Source's Project ID") - ->param('apiKey', '', new Text(512), "Source's API Key") + ->param('endpoint', '', new URL(), 'Source Appwrite endpoint') + ->param('projectId', '', new UID(), 'Source Project ID') + ->param('apiKey', '', new Text(512), 'Source API Key') ->inject('response') ->inject('dbForProject') ->inject('project') diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index 0544760e73..22c3337203 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -388,7 +388,7 @@ App::post('/v1/project/variables') )) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) - ->param('secret', false, new Boolean(), 'Is secret? Secret variables can only be updated or deleted, they cannot be read.', true) + ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true) ->inject('project') ->inject('response') ->inject('dbForProject') @@ -509,19 +509,25 @@ App::put('/v1/project/variables/:variableId') ->param('variableId', '', new UID(), 'Variable unique ID.', false) ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true) + ->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true) ->inject('project') ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') - ->action(function (string $variableId, string $key, ?string $value, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) { + ->action(function (string $variableId, string $key, ?string $value, ?bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) { $variable = $dbForProject->getDocument('variables', $variableId); if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') { throw new Exception(Exception::VARIABLE_NOT_FOUND); } + if ($variable->getAttribute('secret') === true && $secret === false) { + throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET); + } + $variable ->setAttribute('key', $key) ->setAttribute('value', $value ?? $variable->getAttribute('value')) + ->setAttribute('secret', $secret ?? $variable->getAttribute('secret')) ->setAttribute('search', implode(' ', [$variableId, $key, 'project'])); try { diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index e691077adf..e7aa3acb6c 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -15,176 +15,13 @@ use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Query as QueryException; -use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; use Utopia\Logger\Log; use Utopia\System\System; -use Utopia\Validator\Domain as ValidatorDomain; use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; - -App::post('/v1/proxy/rules') - ->groups(['api', 'proxy']) - ->desc('Create rule') - ->label('scope', 'rules.write') - ->label('event', 'rules.[ruleId].create') - ->label('audits.event', 'rule.create') - ->label('audits.resource', 'rule/{response.$id}') - ->label('sdk', new Method( - namespace: 'proxy', - name: 'createRule', - description: '/docs/references/proxy/create-rule.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_PROXY_RULE, - ) - ] - )) - ->param('domain', null, new ValidatorDomain(), 'Domain name.') - ->param('resourceType', null, new WhiteList(['api', 'function', 'site']), 'Action definition for the rule. Possible values are "api", "function" and "site"') - ->param('resourceId', '', new UID(), 'ID of resource for the action type. If resourceType is "api", leave empty. If resourceType is "function", provide ID of the function.', true) - ->inject('response') - ->inject('project') - ->inject('queueForCertificates') - ->inject('queueForEvents') - ->inject('dbForPlatform') - ->inject('dbForProject') - ->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) { - $mainDomain = System::getEnv('_APP_DOMAIN', ''); - if ($domain === $mainDomain) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.'); - } - - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - - if ( - ($functionsDomain !== '' && str_ends_with($domain, $functionsDomain)) || - ($sitesDomain !== '' && str_ends_with($domain, $sitesDomain)) - ) { - // TODO: Refactor later - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your functions or sites domain or their subdomains to a specific resource. Please use a different domain.'); - } - - if ($domain === 'localhost' || $domain === APP_HOSTNAME_INTERNAL) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); - } - - // TODO: @christyjacob remove once we migrate the rules in 1.7.x - if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { - $document = $dbForPlatform->getDocument('rules', md5($domain)); - } else { - $document = $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain]), - ]); - } - - - if (!$document->isEmpty()) { - if ($document->getAttribute('projectId') === $project->getId()) { - $resourceType = $document->getAttribute('resourceType'); - $resourceId = $document->getAttribute('resourceId'); - $message = "Domain already assigned to '{$resourceType}' service"; - if (!empty($resourceId)) { - $message .= " with ID '{$resourceId}'"; - } - - $message .= '.'; - } else { - $message = 'Domain already assigned to different project.'; - } - - throw new Exception(Exception::RULE_ALREADY_EXISTS, $message); - } - - $resourceInternalId = ''; - - switch ($resourceType) { - case 'function': - if (empty($resourceId)) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $function = $dbForProject->getDocument('functions', $resourceId); - - if ($function->isEmpty()) { - throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); - } - - $resourceInternalId = $function->getInternalId(); - break; - case 'site': - if (empty($resourceId)) { - throw new Exception(Exception::SITE_NOT_FOUND); - } - - $site = $dbForProject->getDocument('sites', $resourceId); - - if ($site->isEmpty()) { - throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); - } - - $resourceInternalId = $site->getInternalId(); - break; - } - - try { - $domain = new Domain($domain); - } catch (\Throwable) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); - } - - // TODO: @christyjacob remove once we migrate the rules in 1.7.x - $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); - - $rule = new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain->get(), - 'resourceType' => $resourceType, - 'resourceId' => $resourceId, - 'resourceInternalId' => $resourceInternalId, - 'certificateId' => '', - ]); - - $status = 'created'; - - if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { - $status = 'verified'; - } - - if ($status === 'created') { - $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); - $validator = new CNAME($target->get()); // Verify Domain with DNS records - - if ($validator->isValid($domain->get())) { - $status = 'verifying'; - - $queueForCertificates - ->setDomain(new Document([ - 'domain' => $rule->getAttribute('domain') - ])) - ->trigger(); - } - } - - $rule->setAttribute('status', $status); - $rule = $dbForPlatform->createDocument('rules', $rule); - - $queueForEvents->setParam('ruleId', $rule->getId()); - - $rule->setAttribute('logs', ''); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($rule, Response::MODEL_PROXY_RULE); - }); App::get('/v1/proxy/rules') ->groups(['api', 'proxy']) @@ -411,34 +248,3 @@ App::patch('/v1/proxy/rules/:ruleId/verification') $response->dynamic($rule, Response::MODEL_PROXY_RULE); }); - -App::get('/v1/proxy/subdomains') - ->desc('Check if subdomain is available') - ->groups(['api', 'proxy']) - ->label('scope', 'rules.read') - ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) - ->label('sdk.namespace', 'proxy') - ->label('sdk.method', 'checkSubdomain') - ->label('sdk.description', '/docs/references/proxy/check-subdomain.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_NONE) - ->param('resourceType', null, new WhiteList(['function', 'site']), 'Action definition for the rule. Possible values are "function" and "site"') - ->param('subdomain', '', new Text(256), 'Subdomain name.') - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $resourceType, string $subdomain, Response $response, Database $dbForPlatform) { - //TODO: Add tests for this endpoint - $resourceDomain = $resourceType === 'site' ? System::getEnv('_APP_DOMAIN_SITES', '') : System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $domain = $subdomain . '.' . $resourceDomain; - - $document = $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain]), - ]); - - if ($document && !$document->isEmpty()) { - throw new Exception(Exception::RULE_ALREADY_EXISTS, 'Subdomain already assigned to different project.'); - } - - $response->noContent(); - }); diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index b5ddc94c9d..f180c22acf 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -6,7 +6,7 @@ use Appwrite\Auth\Auth; use Appwrite\ClamAV\Network; use Appwrite\Event\Delete; use Appwrite\Event\Event; -use Appwrite\Event\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\OpenSSL\OpenSSL; use Appwrite\SDK\AuthType; @@ -942,8 +942,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->inject('mode') ->inject('deviceForFiles') ->inject('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) { + ->inject('queueForStatsUsage') + ->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, StatsUsage $queueForStatsUsage) { if (!\extension_loaded('imagick')) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); @@ -1071,7 +1071,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg']; - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_FILES_TRANSFORMATIONS, 1) ->addMetric(str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_TRANSFORMATIONS), 1) ; diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 7f988d726f..06e653c105 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -8,7 +8,7 @@ use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; -use Appwrite\Event\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception; use Appwrite\Network\Validator\Email; use Appwrite\Platform\Workers\Deletes; @@ -466,9 +466,9 @@ App::post('/v1/teams/:teamId/memberships') ->inject('queueForMessaging') ->inject('queueForEvents') ->inject('timelimit') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('plan') - ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, Usage $queueForUsage, array $plan) { + ->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()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -588,9 +588,8 @@ 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, @@ -618,7 +617,8 @@ App::post('/v1/teams/:teamId/memberships') $dbForProject->createDocument('memberships', $membership); Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); - } else { + } elseif ($membership->getAttribute('confirm') === false) { + $membership->setAttribute('secret', Auth::hash($secret)); $membership->setAttribute('invited', DateTime::now()); if ($isPrivilegedUser || $isAppUser) { @@ -629,9 +629,10 @@ 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 { @@ -757,11 +758,11 @@ App::post('/v1/teams/:teamId/memberships') $countryCode = $helper->parse($phone)->getCountryCode(); if (!empty($countryCode)) { - $queueForUsage + $queueForStatsUsage ->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1); } } - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_AUTH_METHOD_PHONE, 1) ->setProject($project) ->trigger(); diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 921a840c0a..0f277a4661 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -19,6 +19,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\Exception\Query as QueryException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; @@ -231,27 +232,74 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId 'activate' => $activate, ])); - // Preview deployments for sites if ($resource->getCollection() === 'sites') { $projectId = $project->getId(); + // Deployment preview $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); + + // VCS branch preview + if (!empty($providerBranch)) { + $domain = "branch-{$providerBranch}-{$resource->getId()}-{$project->getId()}.{$sitesDomain}"; + $ruleId = md5($domain); + try { + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'type' => 'deployment', + 'value' => $deployment->getId(), + 'automation' => 'branch=' . $providerBranch, + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + ])) + ); + } catch (Duplicate $err) { + // Ignore, rule already exists; will be updated by builds worker + } + } + + // VCS commit preview + if (!empty($providerCommitHash)) { + $domain = "commit-{$providerCommitHash}-{$resource->getId()}-{$project->getId()}.{$sitesDomain}"; + $ruleId = md5($domain); + try { + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'type' => 'deployment', + 'value' => $deployment->getId(), + 'automation' => 'commit=' . $providerCommitHash, + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + ])) + ); + } catch (Duplicate $err) { + // Ignore, rule already exists; will be updated by builds worker + } + } } if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) { diff --git a/app/controllers/general.php b/app/controllers/general.php index 68584edd10..4067a6e597 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -5,17 +5,17 @@ require_once __DIR__ . '/../init.php'; use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Auth\Auth; +use Appwrite\Auth\Key; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Event\Func; -use Appwrite\Event\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Network\Validator\Origin; use Appwrite\Platform\Appwrite; -use Appwrite\SDK\AuthType; -use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; -use Appwrite\SDK\Response as SDKResponse; +use Appwrite\Transformation\Adapter\Preview; +use Appwrite\Transformation\Transformation; use Appwrite\Utopia\Request; use Appwrite\Utopia\Request\Filters\V16 as RequestV16; use Appwrite\Utopia\Request\Filters\V17 as RequestV17; @@ -53,7 +53,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, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) +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, ?Key $apiKey) { $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); @@ -120,44 +120,12 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if (\str_starts_with($path, '/.well-known/acme-challenge')) { return false; } + + $type = $rule->getAttribute('type', ''); - $type = $rule->getAttribute('resourceType'); - - if ($type === 'function' || $type === 'site' || $type === 'deployment') { - $resourceCollection = match($type) { - 'function' => 'functions', - 'site' => 'sites', - 'deployment' => 'deployments', - }; - } - - if ($type === 'function' || $type === 'site' || $type === 'deployment') { - $method = $utopia->getRoute()?->getLabel('sdk', null); - - if (empty($method)) { - $utopia->getRoute()?->label('sdk', new Method( - namespace: 'functions', - name: 'createExecution', - description: '/docs/references/functions/create-execution.md', - auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_EXECUTION, - ) - ], - contentType: ContentType::MULTIPART, - requestType: 'application/json', - )); - } else { - /** @var Method $method */ - $method->setNamespace('functions'); - $method->setMethodName('createExecution'); - $utopia->getRoute()?->label('sdk', $method); - } - + if ($type === 'deployment') { if (System::getEnv('_APP_OPTIONS_COMPUTE_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if ($request->getProtocol() !== 'https') { + if ($request->getProtocol() !== 'https' && $request->getHostname() !== APP_HOSTNAME_INTERNAL) { if ($request->getMethod() !== Request::METHOD_GET) { throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.'); } @@ -165,8 +133,22 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw } } - $resourceId = $rule->getAttribute('resourceId'); - $projectId = $rule->getAttribute('projectId'); + /** @var Database $dbForProject */ + $dbForProject = $getProjectDB($project); + + $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('value'))); + + if ($deployment->getAttribute('resourceType', '') === 'functions') { + $type = 'function'; + } elseif ($deployment->getAttribute('resourceType', '') === 'sites') { + $type = 'site'; + } + + $resource = $type === 'function' ? + Authorization::skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) : + Authorization::skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', ''))); + + $isPreview = $type === 'function' ? false : (!\str_starts_with($rule->getAttribute('automation', ''), 'site=')); $path = ($swooleRequest->server['request_uri'] ?? '/'); $query = ($swooleRequest->server['query_string'] ?? ''); @@ -251,42 +233,29 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $requestHeaders = $request->getHeaders(); - $project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId)); - - /** @var Database $dbForProject */ - $dbForProject = $getProjectDB($project); - - if ($resourceCollection === 'deployments') { - $subResource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); - $resource = Authorization::skip(fn () => $dbForProject->getDocument($subResource->getAttribute('resourceType'), $subResource->getAttribute('resourceId'))); - } else { - $resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId)); - } - if ($resource->isEmpty() || !$resource->getAttribute('enabled')) { throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); } - if ($isResourceBlocked($project, RESOURCE_TYPE_FUNCTIONS, $resourceId)) { + if ($isResourceBlocked($project, $type === 'function' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES, $resource->getId())) { throw new AppwriteException(AppwriteException::GENERAL_RESOURCE_BLOCKED); } - $version = match($type) { + $version = match ($type) { 'function' => $resource->getAttribute('version', 'v2'), 'site' => 'v4', - 'deployment' => 'v4' }; $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); $spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; - $runtime = match($type) { + $runtime = match ($type) { 'function' => $runtimes[$resource->getAttribute('runtime')] ?? null, 'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null, - 'deployment' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null, default => null }; + // Static site enforced runtime if ($resource->getAttribute('adapter', '') === 'static') { $runtime = $runtimes['static-1'] ?? null; } @@ -295,22 +264,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported'); } - $deploymentId = match($type) { - 'function' => $resource->getAttribute('deployment', ''), - 'site' => $resource->getAttribute('deploymentId', ''), - 'deployment' => $subResource->getId() - }; - - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $deploymentId)); - - if ($deployment->getAttribute('resourceId') !== $resource->getId()) { - throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - - if ($deployment->isEmpty()) { - throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - /** Check if build has completed */ $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { @@ -321,10 +274,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw throw new AppwriteException(AppwriteException::BUILD_NOT_READY); } - //todo: figure out for sites/functions if ($type === 'function') { $permissions = $resource->getAttribute('execute'); - if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) { throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"'); } @@ -336,18 +287,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; - //todo: check if this would work for sites - if ($type === 'function') { - $jwtExpiry = $resource->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); - $apiKey = $jwtObj->encode([ - 'projectId' => $project->getId(), - 'scopes' => $resource->getAttribute('scopes', []) - ]); - $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; - $headers['x-appwrite-trigger'] = 'http'; - $headers['x-appwrite-user-jwt'] = ''; - } + $jwtExpiry = $resource->getAttribute('timeout', 900); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); + $jwtKey = $jwtObj->encode([ + 'projectId' => $project->getId(), + 'scopes' => $resource->getAttribute('scopes', []) + ]); + $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey; + $headers['x-appwrite-trigger'] = 'http'; + $headers['x-appwrite-user-jwt'] = ''; $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { @@ -386,21 +334,26 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw 'errors' => '', 'logs' => '', 'duration' => 0.0, - 'search' => implode(' ', [$resourceId, $executionId]), + 'search' => implode(' ', [$resource->getId(), $executionId]), ]); if ($type === 'function') { $execution->setAttribute('resourceType', 'functions'); $execution->setAttribute('trigger', 'http'); // http / schedule / event $execution->setAttribute('status', 'processing'); // waiting / processing / completed / failed + + $queueForEvents + ->setParam('functionId', $resource->getId()) + ->setParam('executionId', $execution->getId()) + ->setContext('function', $resource); } elseif ($type === 'site') { $execution->setAttribute('resourceType', 'sites'); - } - $queueForEvents - ->setParam('functionId', $resource->getId()) - ->setParam('executionId', $execution->getId()) - ->setContext('function', $resource); + $queueForEvents + ->setParam('siteId', $resource->getId()) + ->setParam('executionId', $execution->getId()) + ->setContext('site', $resource); + } $durationStart = \microtime(true); @@ -433,14 +386,14 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw // Appwrite vars $vars = \array_merge($vars, [ 'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint, - 'APPWRITE_FUNCTION_ID' => $resourceId, + 'APPWRITE_FUNCTION_ID' => $resource->getId(), 'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'), 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', - 'APPWRITE_COMPUTE_CPUS' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, - 'APPWRITE_COMPUTE_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + 'APPWRITE_FUNCTION_CPUS' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + 'APPWRITE_FUNCTION_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, 'APPWRITE_VERSION' => APP_VERSION_STABLE, 'APPWRITE_REGION' => $project->getAttribute('region'), 'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''), @@ -458,18 +411,21 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw 'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''), ]); + // SPA fallbackFile override + if ($resource->getAttribute('adapter', '') === 'static' && $resource->getAttribute('fallbackFile', '') !== '') { + $vars['OPEN_RUNTIMES_STATIC_FALLBACK'] = $resource->getAttribute('fallbackFile', ''); + } + /** Execute function */ $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); try { - $version = match($type) { + $version = match ($type) { 'function' => $resource->getAttribute('version', 'v2'), 'site' => 'v4', - 'deployment' => 'v4' }; - $entrypoint = match($type) { + $entrypoint = match ($type) { 'function' => $deployment->getAttribute('entrypoint', ''), 'site' => '', - 'deployment' => '' }; if ($type === 'function') { @@ -477,7 +433,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw 'v2' => '', default => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $runtime['startCommand'] . '"' }; - } elseif ($type === 'site' || $type === 'deployment') { + } elseif ($type === 'site') { $frameworks = Config::getParam('frameworks', []); $framework = $frameworks[$resource->getAttribute('framework', '')] ?? null; @@ -493,10 +449,9 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $runtimeEntrypoint = 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $startCommand . '"'; } - $entrypoint = match($type) { + $entrypoint = match ($type) { 'function' => $deployment->getAttribute('entrypoint', ''), 'site' => '', - 'deployment' => '' }; $executionResponse = $executor->createExecution( @@ -519,6 +474,30 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw requestTimeout: 30 ); + // Branded 404 override + if ($executionResponse['statusCode'] === 404 && $resource->getAttribute('adapter', '') === 'static') { + $layout = new View(__DIR__ . '/../views/general/404.phtml'); + $executionResponse['body'] = $layout->render(); + $executionResponse['headers']['content-length'] = \strlen($executionResponse['body']); + } + + // Branded banner for previews + if (\is_null($apiKey) || $apiKey->isBannerDisabled() === false) { + $transformation = new Transformation(); + $transformation->addAdapter(new Preview()); + $transformation->setInput($executionResponse['body']); + $transformation->setTraits($executionResponse['headers']); + if ($isPreview && $transformation->transform()) { + $executionResponse['body'] = $transformation->getOutput(); + + foreach ($executionResponse['headers'] as $key => $value) { + if (\strtolower($key) === 'content-length') { + $executionResponse['headers'][$key] = \strlen($executionResponse['body']); + } + } + } + } + $headersFiltered = []; foreach ($executionResponse['headers'] as $key => $value) { if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { @@ -528,9 +507,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw /** Update execution status */ $status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed'; - if ($type === 'function') { - $execution->setAttribute('status', $status); - } + $execution->setAttribute('status', $status); $execution->setAttribute('logs', $executionResponse['logs']); $execution->setAttribute('errors', $executionResponse['errors']); $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); @@ -545,8 +522,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if ($type === 'function') { $execution - ->setAttribute('status', 'failed') - ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); + ->setAttribute('status', 'failed') + ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); } Console::error($th->getMessage()); @@ -602,7 +579,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; } - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_NETWORK_REQUESTS, 1) ->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize) ->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize()) @@ -613,13 +590,23 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw ->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) ->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) ->setProject($project) - ->trigger() - ; + ->trigger(); return true; } elseif ($type === 'api') { $utopia->getRoute()?->label('error', ''); return false; + } elseif ($type === 'redirect') { + $path = ($swooleRequest->server['request_uri'] ?? '/'); + $query = ($swooleRequest->server['query_string'] ?? ''); + if (!empty($query)) { + $path .= '?' . $query; + } + + $url = 'https://' . $rule->getAttribute('value', '') . $path; + + $response->redirect($url); + return true; } else { throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type); } @@ -641,7 +628,7 @@ App::init() */ App::init() - ->groups(['database', 'functions', 'storage', 'messaging']) + ->groups(['database', 'functions', 'sites', 'messaging']) ->inject('project') ->inject('request') ->action(function (Document $project, Request $request) { @@ -667,13 +654,14 @@ App::init() ->inject('localeCodes') ->inject('clients') ->inject('geodb') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->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, Usage $queueForUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname) { + ->inject('apiKey') + ->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, ?Key $apiKey) { /* * Appwrite Router */ @@ -681,9 +669,8 @@ 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, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); - return; } } @@ -752,14 +739,16 @@ App::init() } if ($domainDocument->isEmpty()) { + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); $domainDocument = new Document([ // TODO: @christyjacob remove once we migrate the rules in 1.7.x - '$id' => System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(), + '$id' => $ruleId, 'domain' => $domain->get(), - 'resourceType' => 'api', + 'type' => 'api', 'status' => 'verifying', 'projectId' => 'console', - 'projectInternalId' => 'console' + 'projectInternalId' => 'console', + 'search' => implode(' ', [$ruleId, $domain->get()]), ]); $domainDocument = $dbForPlatform->createDocument('rules', $domainDocument); @@ -793,6 +782,22 @@ App::init() $validator = new Hostname($clients); if ($validator->isValid($origin)) { $refDomainOrigin = $origin; + } elseif (!empty($origin)) { + // Auto-allow domains with linked rule + if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { + $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? ''))); + } else { + $rule = Authorization::skip( + fn () => $dbForPlatform->find('rules', [ + Query::equal('domain', [$origin]), + Query::limit(1) + ]) + )[0] ?? new Document(); + } + + if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getInternalId()) { + $refDomainOrigin = $origin; + } } $refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : ''); @@ -842,7 +847,7 @@ App::init() $response->addFilter(new ResponseV18()); } if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) { - $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is ". APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); + $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); } } @@ -901,12 +906,13 @@ App::options() ->inject('dbForPlatform') ->inject('getProjectDB') ->inject('queueForEvents') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('queueForFunctions') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) { + ->inject('apiKey') + ->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, ?Key $apiKey) { /* * Appwrite Router */ @@ -914,9 +920,8 @@ 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, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); - return; } } @@ -940,8 +945,8 @@ App::error() ->inject('project') ->inject('logger') ->inject('log') - ->inject('queueForUsage') - ->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Usage $queueForUsage) { + ->inject('queueForStatsUsage') + ->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); $route = $utopia->getRoute(); $class = \get_class($error); @@ -1052,18 +1057,17 @@ App::error() $fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; } - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_NETWORK_REQUESTS, 1) ->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize) ->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize()); } - $queueForUsage + $queueForStatsUsage ->setProject($project) ->trigger(); } - if ($logger && $publish) { try { /** @var Utopia\Database\Document $user */ @@ -1211,12 +1215,13 @@ App::get('/robots.txt') ->inject('dbForPlatform') ->inject('getProjectDB') ->inject('queueForEvents') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('queueForFunctions') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) { + ->inject('apiKey') + ->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, ?Key $apiKey) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -1224,7 +1229,7 @@ App::get('/robots.txt') $template = new View(__DIR__ . '/../views/general/robots.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1241,12 +1246,13 @@ App::get('/humans.txt') ->inject('dbForPlatform') ->inject('getProjectDB') ->inject('queueForEvents') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('queueForFunctions') ->inject('geodb') ->inject('isResourceBlocked') ->inject('previewHostname') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) { + ->inject('apiKey') + ->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, ?Key $apiKey) { $host = $request->getHostname() ?? ''; $mainDomain = System::getEnv('_APP_DOMAIN', ''); @@ -1254,7 +1260,7 @@ App::get('/humans.txt') $template = new View(__DIR__ . '/../views/general/humans.phtml'); $response->text($template->render(false)); } else { - if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { + if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) { $utopia->getRoute()?->label('router', true); } } @@ -1323,7 +1329,7 @@ App::get('/v1/ping') ->inject('dbForPlatform') ->inject('queueForEvents') ->action(function (Response $response, Document $project, Database $dbForPlatform, Event $queueForEvents) { - if ($project->isEmpty()) { + if ($project->isEmpty() || $project->getId() === 'console') { throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND); } @@ -1348,13 +1354,7 @@ App::get('/v1/ping') App::wildcard() ->groups(['api']) ->label('scope', 'global') - ->inject('utopia') - ->action(function (App $utopia) { - $handeledByRouter = $utopia->getRoute()?->getLabel('router', false); - if ($handeledByRouter === true) { - return; - } - + ->action(function () { throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); }); diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 58a77f243a..f8371ed8e6 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -1,8 +1,7 @@ getCollection() === 'teams': - $queueForUsage - ->addMetric(METRIC_TEAMS, $value); // per project + $queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project break; case $document->getCollection() === 'users': - $queueForUsage - ->addMetric(METRIC_USERS, $value); // per project + $queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project if ($event === Database::EVENT_DOCUMENT_DELETE) { - $queueForUsage - ->addReduce($document); + $queueForStatsUsage->addReduce($document); } break; case $document->getCollection() === 'sessions': // sessions - $queueForUsage - ->addMetric(METRIC_SESSIONS, $value); //per project + $queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project break; case $document->getCollection() === 'databases': // databases - $queueForUsage - ->addMetric(METRIC_DATABASES, $value); // per project + $queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project if ($event === Database::EVENT_DOCUMENT_DELETE) { - $queueForUsage - ->addReduce($document); + $queueForStatsUsage->addReduce($document); } break; case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections $parts = explode('_', $document->getCollection()); $databaseInternalId = $parts[1] ?? 0; - $queueForUsage + $queueForStatsUsage ->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) { - $queueForUsage - ->addReduce($document); + $queueForStatsUsage->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; - $queueForUsage + $queueForStatsUsage ->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 - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_BUCKETS, $value); // per project if ($event === Database::EVENT_DOCUMENT_DELETE) { - $queueForUsage + $queueForStatsUsage ->addReduce($document); } break; case str_starts_with($document->getCollection(), 'bucket_'): // files $parts = explode('_', $document->getCollection()); $bucketInternalId = $parts[1]; - $queueForUsage + $queueForStatsUsage ->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': - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_FUNCTIONS, $value); // per project if ($event === Database::EVENT_DOCUMENT_DELETE) { - $queueForUsage + $queueForStatsUsage ->addReduce($document); } break; case $document->getCollection() === 'sites': - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_SITES, $value); // per project if ($event === Database::EVENT_DOCUMENT_DELETE) { - $queueForUsage + $queueForStatsUsage ->addReduce($document); } break; case $document->getCollection() === 'deployments': - $queueForUsage + $queueForStatsUsage ->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 @@ -204,117 +196,79 @@ App::init() ->inject('servers') ->inject('mode') ->inject('team') - ->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team) { + ->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) { $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); } - - // Remove after migration - if (!\str_contains($apiKey, '_')) { - $keyType = API_KEY_STANDARD; - $authKey = $apiKey; - } else { - [ $keyType, $authKey ] = \explode('_', $apiKey, 2); + if ($apiKey->isExpired()) { + throw new Exception(Exception::PROJECT_KEY_EXPIRED); } - if ($keyType === API_KEY_DYNAMIC) { - // Dynamic key + $role = $apiKey->getRole(); + $scopes = $apiKey->getScopes(); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400, 0); + // Disable authorization checks for API keys + Authorization::setDefaultStatus(false); - try { - $payload = $jwtObj->decode($authKey); - } catch (JWTException $error) { - throw new Exception(Exception::API_KEY_EXPIRED); - } + 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(), + ]); - $projectId = $payload['projectId'] ?? ''; - $tokenScopes = $payload['scopes'] ?? []; + $queueForAudits->setUser($user); + } - // 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 ($apiKey->getType() === API_KEY_STANDARD) { + $dbKey = $project->find( + key: 'secret', + find: $request->getHeader('x-appwrite-key', ''), + subject: 'keys' + ); - $role = Auth::USER_ROLE_APPS; - $scopes = \array_merge($roles[$role]['scopes'], $tokenScopes); + if ($dbKey) { + $accessedAt = $dbKey->getAttribute('accessedAt', ''); - 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) { - $key->setAttribute('accessedAt', DateTime::now()); - $dbForPlatform->updateDocument('keys', $key->getId(), $key); + $dbKey->setAttribute('accessedAt', DateTime::now()); + $dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); } $sdkValidator = new WhiteList($servers, true); $sdk = $request->getHeader('x-sdk-name', 'UNKNOWN'); + if ($sdkValidator->isValid($sdk)) { - $sdks = $key->getAttribute('sdks', []); + $sdks = $dbKey->getAttribute('sdks', []); + if (!in_array($sdk, $sdks)) { - array_push($sdks, $sdk); - $key->setAttribute('sdks', $sdks); + $sdks[] = $sdk; + $dbKey->setAttribute('sdks', $sdks); /** Update access time as well */ - $key->setAttribute('accessedAt', Datetime::now()); - $dbForPlatform->updateDocument('keys', $key->getId(), $key); + $dbKey->setAttribute('accessedAt', Datetime::now()); + $dbForPlatform->updateDocument('keys', $dbKey->getId(), $dbKey); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); } } @@ -322,8 +276,7 @@ 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 = []; @@ -339,7 +292,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']); } @@ -354,9 +307,7 @@ 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) { @@ -365,9 +316,7 @@ 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) { @@ -381,18 +330,18 @@ App::init() } } - /** Do not allow access to disabled services */ /** - * @var ?\Appwrite\SDK\Method $method + * @var ?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] @@ -402,13 +351,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); } @@ -445,11 +394,12 @@ App::init() ->inject('queueForDeletes') ->inject('queueForDatabase') ->inject('queueForBuilds') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('dbForProject') ->inject('timelimit') ->inject('mode') - ->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, Usage $queueForUsage, Database $dbForProject, callable $timelimit, string $mode) use ($usageDatabaseListener, $eventDatabaseListener) { + ->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) { $route = $utopia->getRoute(); @@ -480,7 +430,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; } @@ -546,6 +496,12 @@ 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); @@ -559,8 +515,8 @@ App::init() $queueForRealtime = new Realtime(); $dbForProject - ->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, '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, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener( $project, $document, @@ -589,10 +545,7 @@ App::init() $bucketId = $parts[1] ?? null; $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); - $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); - - if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } @@ -634,8 +587,7 @@ 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'); } } }); @@ -703,7 +655,7 @@ App::shutdown() ->inject('user') ->inject('queueForEvents') ->inject('queueForAudits') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('queueForDeletes') ->inject('queueForDatabase') ->inject('queueForBuilds') @@ -712,7 +664,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, Usage $queueForUsage, 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, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -796,6 +748,7 @@ App::shutdown() foreach ($queueForEvents->getParams() as $key => $value) { $queueForAudits->setParam($key, $value); } + $queueForAudits->trigger(); } @@ -815,9 +768,7 @@ App::shutdown() $queueForMessaging->trigger(); } - /** - * Cache label - */ + // Cache label $useCache = $route->getLabel('cache', false); if ($useCache) { $resource = $resourceType = null; @@ -869,13 +820,13 @@ App::shutdown() $fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size']; } - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_NETWORK_REQUESTS, 1) ->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize) ->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize()); } - $queueForUsage + $queueForStatsUsage ->setProject($project) ->trigger(); } diff --git a/app/http.php b/app/http.php index 608ac2ec12..4712d4ebf9 100644 --- a/app/http.php +++ b/app/http.php @@ -13,11 +13,11 @@ use Swoole\Table; use Utopia\App; use Utopia\Audit\Audit; use Utopia\CLI\Console; +use Utopia\Compression\Compression; 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; @@ -29,6 +29,8 @@ use Utopia\Pools\Group; use Utopia\Swoole\Files; use Utopia\System\System; +Files::load(__DIR__.'/../public'); + const DOMAIN_SYNC_TIMER = 30; // 30 seconds $domains = new Table(1_000_000); // 1 million rows @@ -156,6 +158,79 @@ 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'); @@ -164,140 +239,122 @@ $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', []); - $consoleCollections = $collections['console']; - foreach ($consoleCollections as $key => $collection) { - if (($collection['$collection'] ?? '') !== Database::METADATA) { - continue; - } - if (!$dbForPlatform->getCollection($key)->isEmpty()) { - continue; + + // 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(); } - Console::success('[Setup] - Creating console collection: ' . $collection['$id'] . '...'); + if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty()) { + 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', + ])); - $attributes = \array_map(fn ($attribute) => new Document($attribute), $collection['attributes']); - $indexes = \array_map(fn (array $index) => new Document($index), $collection['indexes']); + $bucket = $dbForPlatform->getDocument('buckets', 'default'); - $dbForPlatform->createCollection($key, $attributes, $indexes); - } - - 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', - ])); - - $bucket = $dbForPlatform->getDocument('buckets', 'default'); - - 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', '')); - $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); - $sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1); - - $cache = $app->getResource('cache'); - - foreach ($sharedTablesV2 as $hostname) { - $adapter = $pools - ->get($hostname) - ->pop() - ->getResource(); - - $dbForProject = (new Database($adapter, $cache)) - ->setDatabase('appwrite') - ->setSharedTables(true) - ->setTenant(null) - ->setNamespace(System::getEnv('_APP_DATABASE_SHARED_NAMESPACE', '')); - - try { - Console::success('[Setup] - Creating project database: ' . $hostname . '...'); - $dbForProject->create(); - } catch (Duplicate) { - Console::success('[Setup] - Skip: metadata table already exists'); - } - - foreach ($projectCollections as $key => $collection) { - if (($collection['$collection'] ?? '') !== Database::METADATA) { - continue; - } - if (!$dbForProject->getCollection($key)->isEmpty()) { - continue; + Console::info(" └── 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), $collection['attributes']); - $indexes = \array_map(fn (array $index) => new Document($index), $collection['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']); - Console::success('[Setup] - Creating project collection: ' . $collection['$id'] . '...'); + $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']); - $dbForProject->createCollection($key, $attributes, $indexes); + $dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); } - } + + if (Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) { + Console::info(" └── Creating screenshots bucket..."); + Authorization::skip(fn () => $dbForPlatform->createDocument('buckets', new Document([ + '$id' => ID::custom('screenshots'), + '$collection' => ID::custom('buckets'), + 'name' => 'Screenshots', + 'maximumFileSize' => 5000000, // ~5MB + 'allowedFileExtensions' => [ 'png' ], + 'enabled' => true, + 'compression' => Compression::GZIP, + 'encryption' => false, + 'antivirus' => false, + 'fileSecurity' => true, + '$permissions' => [], + 'search' => 'buckets Screenshots', + ]))); + + $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + + Console::info(" └── Creating files collection for screenshots bucket..."); + $files = $collections['buckets']['files'] ?? []; + if (empty($files)) { + throw new Exception('Files collection is not configured.'); + } + + $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']); + + $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']); + + Authorization::skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes)); + } + }); $pools->reclaim(); - Console::success('[Setup] - Server database init completed...'); }); @@ -459,7 +516,6 @@ $http->on('Task', function () use ($register, $domains) { if ($lastSyncUpdate != null) { $queries[] = Query::greaterThanEqual('$updatedAt', $lastSyncUpdate); } - $queries[] = Query::equal('resourceType', ['function']); $results = []; try { $results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries)); diff --git a/app/init.php b/app/init.php index fbffc4b3ad..3291b160b9 100644 --- a/app/init.php +++ b/app/init.php @@ -21,6 +21,7 @@ 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; @@ -32,7 +33,7 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; use Appwrite\Event\Realtime; -use Appwrite\Event\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; use Appwrite\Extend\Exception; use Appwrite\Functions\Specification; @@ -239,8 +240,6 @@ 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'; @@ -271,6 +270,8 @@ 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_SITES = 'sites'; @@ -318,6 +319,18 @@ const METRIC_SITE_ID_BUILDS_MB_SECONDS = '{siteInternalId}.builds.mbSeconds'; 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_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 @@ -1198,6 +1211,9 @@ App::setResource('queueForWebhooks', function (Queue\Publisher $publisher) { 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']); @@ -1568,6 +1584,39 @@ 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 = []; @@ -1635,6 +1684,7 @@ function getDevice(string $root, string $connection = ''): Device $accessSecret = ''; $bucket = ''; $region = ''; + $url = App::getEnv('_APP_STORAGE_S3_ENDPOINT', ''); try { $dsn = new DSN($connection); @@ -1649,7 +1699,7 @@ function getDevice(string $root, string $connection = ''): Device switch ($device) { case Storage::DEVICE_S3: - return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl); + return new S3($root, $accessKey, $accessSecret, $bucket, $region, $acl, $url); case STORAGE::DEVICE_DO_SPACES: $device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl); $device->setHttpVersion(S3::HTTP_VERSION_1_1); @@ -1675,7 +1725,8 @@ function getDevice(string $root, string $connection = ''): Device $s3Region = System::getEnv('_APP_STORAGE_S3_REGION', ''); $s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', ''); $s3Acl = 'private'; - return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl); + $s3EndpointUrl = App::getEnv('_APP_STORAGE_S3_ENDPOINT', ''); + return new S3($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl, $s3EndpointUrl); case Storage::DEVICE_DO_SPACES: $doSpacesAccessKey = System::getEnv('_APP_STORAGE_DO_SPACES_ACCESS_KEY', ''); $doSpacesSecretKey = System::getEnv('_APP_STORAGE_DO_SPACES_SECRET', ''); @@ -1910,13 +1961,32 @@ App::setResource( fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false ); -App::setResource('previewHostname', function (Request $request) { +App::setResource('previewHostname', function (Request $request, ?Key $apiKey) { + $allowed = false; + if (App::isDevelopment()) { - $host = $request->getQuery('appwrite-hostname') ?? ''; + $allowed = true; + } elseif (!\is_null($apiKey) && $apiKey->getHostnameOverride() === true) { + $allowed = true; + } + + if ($allowed) { + $host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')); if (!empty($host)) { return $host; } } + return ''; -}, ['request']); +}, ['request', 'apiKey']); + +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']); diff --git a/app/views/general/404.phtml b/app/views/general/404.phtml new file mode 100644 index 0000000000..7ec1cfbf21 --- /dev/null +++ b/app/views/general/404.phtml @@ -0,0 +1,185 @@ + + + + + + + 404 Not Found + + + + + +
+
+
Page not found
+

The page you’re looking for doesn’t exist.

+ +
+
+ +
+

Powered by

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index aa06406f14..41384e1f02 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -116,6 +116,7 @@ $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 @@ -320,6 +321,7 @@ $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,6 +417,7 @@ $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 @@ -570,6 +573,7 @@ $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 @@ -651,10 +655,69 @@ $image = $this->getParam('image', ''); - _APP_MAINTENANCE_RETENTION_USAGE_HOURLY - _APP_MAINTENANCE_RETENTION_SCHEDULES - appwrite-worker-usage: + appwrite-task-stats-resources: image: /: - entrypoint: worker-usage - container_name: appwrite-worker-usage + 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: /: + 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: /: + entrypoint: worker-stats-usage + container_name: appwrite-worker-stats-usage <<: *x-logging restart: unless-stopped networks: @@ -679,11 +742,11 @@ $image = $this->getParam('image', ''); - _APP_LOGGING_CONFIG - _APP_USAGE_AGGREGATION_INTERVAL - appwrite-worker-usage-dump: + appwrite-worker-stats-usage-dump: image: /: - entrypoint: worker-usage-dump + entrypoint: worker-stats-usage-dump <<: *x-logging - container_name: appwrite-worker-usage-dump + container_name: appwrite-worker-stats-usage-dump restart: unless-stopped networks: - appwrite @@ -824,6 +887,7 @@ $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 diff --git a/app/worker.php b/app/worker.php index 8c7e24b748..28ee4057cf 100644 --- a/app/worker.php +++ b/app/worker.php @@ -13,8 +13,12 @@ use Appwrite\Event\Func; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; +use Appwrite\Event\StatsUsage; +use Appwrite\Event\StatsUsageDump; +/** remove */ use Appwrite\Event\Usage; use Appwrite\Event\UsageDump; +/** /remove */ use Appwrite\Platform\Appwrite; use Swoole\Runtime; use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis; @@ -173,6 +177,39 @@ 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); }); @@ -240,6 +277,14 @@ Server::setResource('queueForUsageDump', function (Publisher $publisher) { return new UsageDump($publisher); }, ['publisher']); +Server::setResource('queueForStatsUsage', function (Publisher $publisher) { + return new StatsUsage($publisher); +}, ['publisher']); + +Server::setResource('queueForStatsUsageDump', function (Publisher $publisher) { + return new StatsUsageDump($publisher); +}, ['publisher']); + Server::setResource('queueForDatabase', function (Publisher $publisher) { return new EventDatabase($publisher); }, ['publisher']); diff --git a/bin/screenshot b/bin/screenshot new file mode 100755 index 0000000000..4d8ceb998f --- /dev/null +++ b/bin/screenshot @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php screenshot $@ \ No newline at end of file diff --git a/bin/stats-resources b/bin/stats-resources new file mode 100644 index 0000000000..3104bab896 --- /dev/null +++ b/bin/stats-resources @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php stats-resources $@ \ No newline at end of file diff --git a/bin/worker-stats-resources b/bin/worker-stats-resources new file mode 100644 index 0000000000..9c5d2bebff --- /dev/null +++ b/bin/worker-stats-resources @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/worker.php stats-resources $@ \ No newline at end of file diff --git a/bin/worker-stats-usage b/bin/worker-stats-usage new file mode 100644 index 0000000000..2c267d805e --- /dev/null +++ b/bin/worker-stats-usage @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/worker.php stats-usage $@ \ No newline at end of file diff --git a/bin/worker-stats-usage-dump b/bin/worker-stats-usage-dump new file mode 100644 index 0000000000..98e3c2cac7 --- /dev/null +++ b/bin/worker-stats-usage-dump @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/worker.php stats-usage-dump $@ \ No newline at end of file diff --git a/composer.json b/composer.json index 199229c105..da94b9462d 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "test": "vendor/bin/phpunit", "lint": "vendor/bin/pint --test", "format": "vendor/bin/pint", - "bench": "vendor/bin/phpbench run --report=benchmark" + "bench": "vendor/bin/phpbench run --report=benchmark", + "check": "./vendor/bin/phpstan analyse -c phpstan.neon" }, "autoload": { "psr-4": { @@ -43,18 +44,18 @@ "ext-openssl": "*", "ext-zlib": "*", "ext-sockets": "*", - "appwrite/php-runtimes": "0.17.*", + "appwrite/php-runtimes": "0.18.*", "appwrite/php-clamav": "2.0.*", - "utopia-php/abuse": "0.49.*", + "utopia-php/abuse": "0.50.*", "utopia-php/analytics": "0.10.*", - "utopia-php/audit": "0.49.*", + "utopia-php/audit": "0.51.*", "utopia-php/cache": "0.11.*", "utopia-php/cli": "0.15.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.58.4", + "utopia-php/database": "0.59.0", "utopia-php/domains": "0.5.*", "utopia-php/dsn": "0.2.1", - "utopia-php/framework": "dev-fix-prevent-duplicate-compression as 0.33.99", + "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.3.*", "utopia-php/image": "0.7.*", "utopia-php/locale": "0.4.*", @@ -84,12 +85,13 @@ }, "require-dev": { "ext-fileinfo": "*", - "appwrite/sdk-generator": "0.39.32", + "appwrite/sdk-generator": "0.40.*", "phpunit/phpunit": "9.5.20", "swoole/ide-helper": "5.1.2", "textalk/websocket": "1.5.7", "laravel/pint": "^1.14", - "phpbench/phpbench": "^1.2" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "1.8.*" }, "provide": { "ext-phpiredis": "*" diff --git a/composer.lock b/composer.lock index 4f6d1a0bbe..a7f0992f99 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4da9bee4423753c2e592c081b19b8711", + "content-hash": "3b6171de8c624cfbcd723f1cc76a9560", "packages": [ { "name": "adhocore/jwt", @@ -157,16 +157,16 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.17.0", + "version": "0.18.0", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "9a9e20d1f5c28caf539ad4cb52164dc283f99797" + "reference": "f1ddcc567325659ad79506bc9684a4fc2009dc42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/runtimes/zipball/9a9e20d1f5c28caf539ad4cb52164dc283f99797", - "reference": "9a9e20d1f5c28caf539ad4cb52164dc283f99797", + "url": "https://api.github.com/repos/appwrite/runtimes/zipball/f1ddcc567325659ad79506bc9684a4fc2009dc42", + "reference": "f1ddcc567325659ad79506bc9684a4fc2009dc42", "shasum": "" }, "require": { @@ -206,9 +206,9 @@ ], "support": { "issues": "https://github.com/appwrite/runtimes/issues", - "source": "https://github.com/appwrite/runtimes/tree/0.17.0" + "source": "https://github.com/appwrite/runtimes/tree/0.18.0" }, - "time": "2025-01-10T13:36:30+00:00" + "time": "2025-03-07T14:30:31+00:00" }, { "name": "beberlei/assert", @@ -279,16 +279,16 @@ }, { "name": "brick/math", - "version": "0.12.1", + "version": "0.12.3", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", "shasum": "" }, "require": { @@ -297,7 +297,7 @@ "require-dev": { "php-coveralls/php-coveralls": "^2.2", "phpunit/phpunit": "^10.1", - "vimeo/psalm": "5.16.0" + "vimeo/psalm": "6.8.8" }, "type": "library", "autoload": { @@ -327,7 +327,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.1" + "source": "https://github.com/brick/math/tree/0.12.3" }, "funding": [ { @@ -335,7 +335,7 @@ "type": "github" } ], - "time": "2023-11-29T23:19:16+00:00" + "time": "2025-02-28T13:11:00+00:00" }, { "name": "chillerlan/php-qrcode", @@ -709,16 +709,16 @@ }, { "name": "google/protobuf", - "version": "v4.29.3", + "version": "v4.30.0", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7" + "reference": "e1d66682f6836aa87820400f0aa07d9eb566feb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7", - "reference": "ab5077c2cfdd1f415f42d11fdbdf903ba8e3d9b7", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/e1d66682f6836aa87820400f0aa07d9eb566feb6", + "reference": "e1d66682f6836aa87820400f0aa07d9eb566feb6", "shasum": "" }, "require": { @@ -747,9 +747,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.29.3" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.30.0" }, - "time": "2025-01-08T21:00:13+00:00" + "time": "2025-03-04T22:54:49+00:00" }, { "name": "jean85/pretty-package-versions", @@ -1237,16 +1237,16 @@ }, { "name": "open-telemetry/api", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "8b925df3047628968bc5be722468db1b98b82d51" + "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/8b925df3047628968bc5be722468db1b98b82d51", - "reference": "8b925df3047628968bc5be722468db1b98b82d51", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/199d7ddda88f5f5619fa73463f1a5a7149ccd1f1", + "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1", "shasum": "" }, "require": { @@ -1303,7 +1303,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-02-03T21:49:11+00:00" + "time": "2025-03-05T21:42:54+00:00" }, { "name": "open-telemetry/context", @@ -1366,16 +1366,16 @@ }, { "name": "open-telemetry/exporter-otlp", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "243d9657c44a06f740cf384f486afe954c2b725f" + "reference": "b7580440b7481a98da97aceabeb46e1b276c8747" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/243d9657c44a06f740cf384f486afe954c2b725f", - "reference": "243d9657c44a06f740cf384f486afe954c2b725f", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/b7580440b7481a98da97aceabeb46e1b276c8747", + "reference": "b7580440b7481a98da97aceabeb46e1b276c8747", "shasum": "" }, "require": { @@ -1426,7 +1426,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-08T23:50:03+00:00" + "time": "2025-03-06T23:21:56+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -1757,16 +1757,16 @@ }, { "name": "php-amqplib/php-amqplib", - "version": "v3.7.2", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "738a73eb0019b6c99d9bc25d7a0c0dd8f56a5199" + "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/738a73eb0019b6c99d9bc25d7a0c0dd8f56a5199", - "reference": "738a73eb0019b6c99d9bc25d7a0c0dd8f56a5199", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/9f50fe69a9f1a19e2cb25596a354d705de36fe59", + "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59", "shasum": "" }, "require": { @@ -1832,9 +1832,9 @@ ], "support": { "issues": "https://github.com/php-amqplib/php-amqplib/issues", - "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.2" + "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.3" }, - "time": "2024-11-21T09:21:41+00:00" + "time": "2025-02-18T20:11:13+00:00" }, { "name": "php-http/discovery", @@ -2371,16 +2371,16 @@ }, { "name": "ramsey/collection", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", "shasum": "" }, "require": { @@ -2388,25 +2388,22 @@ }, "require-dev": { "captainhook/plugin-composer": "^5.3", - "ergebnis/composer-normalize": "^2.28.3", - "fakerphp/faker": "^1.21", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", "hamcrest/hamcrest-php": "^2.0", - "jangregor/phpstan-prophecy": "^1.0", - "mockery/mockery": "^1.5", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcsstandards/phpcsutils": "^1.0.0-rc1", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18.4", - "ramsey/coding-standard": "^2.0.3", - "ramsey/conventional-commits": "^1.3", - "vimeo/psalm": "^5.4" + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" }, "type": "library", "extra": { @@ -2444,19 +2441,9 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.0.0" + "source": "https://github.com/ramsey/collection/tree/2.1.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", - "type": "tidelift" - } - ], - "time": "2022-12-31T21:50:55+00:00" + "time": "2025-03-02T04:48:29+00:00" }, { "name": "ramsey/uuid", @@ -2694,16 +2681,16 @@ }, { "name": "symfony/http-client", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "7ce6078c79a4a7afff931c413d2959d3bffbfb8d" + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/7ce6078c79a4a7afff931c413d2959d3bffbfb8d", - "reference": "7ce6078c79a4a7afff931c413d2959d3bffbfb8d", + "url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", + "reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6", "shasum": "" }, "require": { @@ -2769,7 +2756,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.2.3" + "source": "https://github.com/symfony/http-client/tree/v7.2.4" }, "funding": [ { @@ -2785,7 +2772,7 @@ "type": "tidelift" } ], - "time": "2025-01-28T15:51:35+00:00" + "time": "2025-02-13T10:27:23+00:00" }, { "name": "symfony/http-client-contracts", @@ -3377,16 +3364,16 @@ }, { "name": "utopia-php/abuse", - "version": "0.49.0", + "version": "0.50.0", "source": { "type": "git", "url": "https://github.com/utopia-php/abuse.git", - "reference": "76612c274b895aa3d4d1fa27557a6402463eea99" + "reference": "3ff67819e9de61506c5ca070a70552f7ebe99f80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/abuse/zipball/76612c274b895aa3d4d1fa27557a6402463eea99", - "reference": "76612c274b895aa3d4d1fa27557a6402463eea99", + "url": "https://api.github.com/repos/utopia-php/abuse/zipball/3ff67819e9de61506c5ca070a70552f7ebe99f80", + "reference": "3ff67819e9de61506c5ca070a70552f7ebe99f80", "shasum": "" }, "require": { @@ -3394,7 +3381,7 @@ "ext-pdo": "*", "ext-redis": "*", "php": ">=8.0", - "utopia-php/database": "0.58.*" + "utopia-php/database": "0.59.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3422,9 +3409,9 @@ ], "support": { "issues": "https://github.com/utopia-php/abuse/issues", - "source": "https://github.com/utopia-php/abuse/tree/0.49.0" + "source": "https://github.com/utopia-php/abuse/tree/0.50.0" }, - "time": "2025-02-04T07:33:59+00:00" + "time": "2025-02-12T09:13:59+00:00" }, { "name": "utopia-php/analytics", @@ -3474,21 +3461,21 @@ }, { "name": "utopia-php/audit", - "version": "0.49.0", + "version": "0.51.0", "source": { "type": "git", "url": "https://github.com/utopia-php/audit.git", - "reference": "9d5c5e0cf0f6d9157b911fc3971da4331d71c96d" + "reference": "a5a4b73a57e27a0fac8025b1d6038e145a1ca04e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/audit/zipball/9d5c5e0cf0f6d9157b911fc3971da4331d71c96d", - "reference": "9d5c5e0cf0f6d9157b911fc3971da4331d71c96d", + "url": "https://api.github.com/repos/utopia-php/audit/zipball/a5a4b73a57e27a0fac8025b1d6038e145a1ca04e", + "reference": "a5a4b73a57e27a0fac8025b1d6038e145a1ca04e", "shasum": "" }, "require": { "php": ">=8.0", - "utopia-php/database": "0.58.*" + "utopia-php/database": "0.59.*" }, "require-dev": { "laravel/pint": "1.*", @@ -3515,9 +3502,9 @@ ], "support": { "issues": "https://github.com/utopia-php/audit/issues", - "source": "https://github.com/utopia-php/audit/tree/0.49.0" + "source": "https://github.com/utopia-php/audit/tree/0.51.0" }, - "time": "2025-02-04T07:27:18+00:00" + "time": "2025-02-12T09:12:44+00:00" }, { "name": "utopia-php/cache", @@ -3717,16 +3704,16 @@ }, { "name": "utopia-php/database", - "version": "0.58.4", + "version": "0.59.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "ff3fd22e4fe757cc2a78f17169f6dcc45c96d0fe" + "reference": "0eed7f1ad3eb66ff4a7d73b68dd9d3e05089eb18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/ff3fd22e4fe757cc2a78f17169f6dcc45c96d0fe", - "reference": "ff3fd22e4fe757cc2a78f17169f6dcc45c96d0fe", + "url": "https://api.github.com/repos/utopia-php/database/zipball/0eed7f1ad3eb66ff4a7d73b68dd9d3e05089eb18", + "reference": "0eed7f1ad3eb66ff4a7d73b68dd9d3e05089eb18", "shasum": "" }, "require": { @@ -3767,9 +3754,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.58.4" + "source": "https://github.com/utopia-php/database/tree/0.59.0" }, - "time": "2025-02-05T02:51:02+00:00" + "time": "2025-02-12T08:08:29+00:00" }, { "name": "utopia-php/domains", @@ -3880,16 +3867,16 @@ }, { "name": "utopia-php/fetch", - "version": "0.3.0", + "version": "0.3.1", "source": { "type": "git", "url": "https://github.com/utopia-php/fetch.git", - "reference": "02b12c05aec13399dcc2da8d51f908e328ab63f4" + "reference": "524dd50afa8c64670c4fb18f1df4db9b5bb4b3d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/fetch/zipball/02b12c05aec13399dcc2da8d51f908e328ab63f4", - "reference": "02b12c05aec13399dcc2da8d51f908e328ab63f4", + "url": "https://api.github.com/repos/utopia-php/fetch/zipball/524dd50afa8c64670c4fb18f1df4db9b5bb4b3d0", + "reference": "524dd50afa8c64670c4fb18f1df4db9b5bb4b3d0", "shasum": "" }, "require": { @@ -3913,22 +3900,22 @@ "description": "A simple library that provides an interface for making HTTP Requests.", "support": { "issues": "https://github.com/utopia-php/fetch/issues", - "source": "https://github.com/utopia-php/fetch/tree/0.3.0" + "source": "https://github.com/utopia-php/fetch/tree/0.3.1" }, - "time": "2025-01-17T06:11:10+00:00" + "time": "2025-03-05T18:08:55+00:00" }, { "name": "utopia-php/framework", - "version": "dev-fix-prevent-duplicate-compression", + "version": "0.33.19", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "a1efe3e10038afe4109af833ce7a25a8ec4b5ed2" + "reference": "64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/a1efe3e10038afe4109af833ce7a25a8ec4b5ed2", - "reference": "a1efe3e10038afe4109af833ce7a25a8ec4b5ed2", + "url": "https://api.github.com/repos/utopia-php/http/zipball/64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0", + "reference": "64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0", "shasum": "" }, "require": { @@ -3960,9 +3947,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/fix-prevent-duplicate-compression" + "source": "https://github.com/utopia-php/http/tree/0.33.19" }, - "time": "2025-02-03T12:02:35+00:00" + "time": "2025-03-06T11:37:49+00:00" }, { "name": "utopia-php/image", @@ -4170,16 +4157,16 @@ }, { "name": "utopia-php/migration", - "version": "0.6.17", + "version": "0.6.19", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "677a5c4688d7f54d1631a91f76a35d51346cf96b" + "reference": "3c9497f7a54ef88b1077c48d8326893133ad78eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/677a5c4688d7f54d1631a91f76a35d51346cf96b", - "reference": "677a5c4688d7f54d1631a91f76a35d51346cf96b", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/3c9497f7a54ef88b1077c48d8326893133ad78eb", + "reference": "3c9497f7a54ef88b1077c48d8326893133ad78eb", "shasum": "" }, "require": { @@ -4187,7 +4174,7 @@ "ext-curl": "*", "ext-openssl": "*", "php": ">=8.1", - "utopia-php/database": "0.58.*", + "utopia-php/database": "0.59.*", "utopia-php/dsn": "0.2.*", "utopia-php/framework": "0.33.*", "utopia-php/storage": "0.18.*" @@ -4220,9 +4207,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/0.6.17" + "source": "https://github.com/utopia-php/migration/tree/0.6.19" }, - "time": "2025-02-05T05:27:29+00:00" + "time": "2025-02-13T07:50:21+00:00" }, { "name": "utopia-php/mongo", @@ -4490,16 +4477,16 @@ }, { "name": "utopia-php/queue", - "version": "0.8.2", + "version": "0.8.6", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "a6ec26a787e8292ca2d7b8f5a0ad179b46b2c4d0" + "reference": "b713b997285c29d120bbcbe3d6e93762d850f87c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/a6ec26a787e8292ca2d7b8f5a0ad179b46b2c4d0", - "reference": "a6ec26a787e8292ca2d7b8f5a0ad179b46b2c4d0", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/b713b997285c29d120bbcbe3d6e93762d850f87c", + "reference": "b713b997285c29d120bbcbe3d6e93762d850f87c", "shasum": "" }, "require": { @@ -4549,9 +4536,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.8.2" + "source": "https://github.com/utopia-php/queue/tree/0.8.6" }, - "time": "2025-02-06T11:01:15+00:00" + "time": "2025-02-10T03:35:00+00:00" }, { "name": "utopia-php/registry", @@ -4607,22 +4594,24 @@ }, { "name": "utopia-php/storage", - "version": "0.18.8", + "version": "0.18.10", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "84737afa634e6a833fc4f8b0c967553234d3f215" + "reference": "76f31158f4251abb207f7a9b16f7cb0bfdb3b39e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/84737afa634e6a833fc4f8b0c967553234d3f215", - "reference": "84737afa634e6a833fc4f8b0c967553234d3f215", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/76f31158f4251abb207f7a9b16f7cb0bfdb3b39e", + "reference": "76f31158f4251abb207f7a9b16f7cb0bfdb3b39e", "shasum": "" }, "require": { "ext-brotli": "*", + "ext-curl": "*", "ext-fileinfo": "*", "ext-lz4": "*", + "ext-simplexml": "*", "ext-snappy": "*", "ext-xz": "*", "ext-zlib": "*", @@ -4656,9 +4645,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.8" + "source": "https://github.com/utopia-php/storage/tree/0.18.10" }, - "time": "2024-12-04T08:30:35+00:00" + "time": "2025-03-03T10:47:54+00:00" }, { "name": "utopia-php/swoole", @@ -5051,16 +5040,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.39.32", + "version": "0.40.2", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "2d02e1305ea5004fb0aec6b2618d6c597659b75c" + "reference": "56f09482d9e2f223911277ab887f197402708049" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/2d02e1305ea5004fb0aec6b2618d6c597659b75c", - "reference": "2d02e1305ea5004fb0aec6b2618d6c597659b75c", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/56f09482d9e2f223911277ab887f197402708049", + "reference": "56f09482d9e2f223911277ab887f197402708049", "shasum": "" }, "require": { @@ -5096,9 +5085,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.39.32" + "source": "https://github.com/appwrite/sdk-generator/tree/0.40.2" }, - "time": "2025-01-29T04:04:19+00:00" + "time": "2025-03-06T16:31:03+00:00" }, { "name": "doctrine/annotations", @@ -5370,16 +5359,16 @@ }, { "name": "laravel/pint", - "version": "v1.20.0", + "version": "v1.21.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b" + "reference": "531fa0871fbde719c51b12afa3a443b8f4e4b425" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/53072e8ea22213a7ed168a8a15b96fbb8b82d44b", - "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b", + "url": "https://api.github.com/repos/laravel/pint/zipball/531fa0871fbde719c51b12afa3a443b8f4e4b425", + "reference": "531fa0871fbde719c51b12afa3a443b8f4e4b425", "shasum": "" }, "require": { @@ -5387,15 +5376,15 @@ "ext-mbstring": "*", "ext-tokenizer": "*", "ext-xml": "*", - "php": "^8.1.0" + "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.66.0", - "illuminate/view": "^10.48.25", - "larastan/larastan": "^2.9.12", - "laravel-zero/framework": "^10.48.25", + "friendsofphp/php-cs-fixer": "^3.68.5", + "illuminate/view": "^11.42.0", + "larastan/larastan": "^3.0.4", + "laravel-zero/framework": "^11.36.1", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^1.17.0", + "nunomaduro/termwind": "^2.3", "pestphp/pest": "^2.36.0" }, "bin": [ @@ -5432,7 +5421,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-01-14T16:20:53+00:00" + "time": "2025-02-18T03:18:57+00:00" }, { "name": "matthiasmullie/minify", @@ -5560,16 +5549,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", "shasum": "" }, "require": { @@ -5608,7 +5597,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" }, "funding": [ { @@ -5616,7 +5605,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-02-12T12:17:51+00:00" }, { "name": "nikic/php-parser", @@ -6190,16 +6179,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", - "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { @@ -6231,9 +6220,68 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2024-10-13T11:29:49+00:00" + "time": "2025-02-19T13:28:12+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.8.11", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "46e223dd68a620da18855c23046ddb00940b4014" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014", + "reference": "46e223dd68a620da18855c23046ddb00940b4014", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/1.8.11" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2022-10-24T15:45:13+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8371,16 +8419,16 @@ }, { "name": "symfony/process", - "version": "v7.2.0", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", "shasum": "" }, "require": { @@ -8412,7 +8460,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.0" + "source": "https://github.com/symfony/process/tree/v7.2.4" }, "funding": [ { @@ -8428,7 +8476,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2025-02-05T08:33:46+00:00" }, { "name": "symfony/string", @@ -8745,18 +8793,9 @@ "time": "2024-03-07T20:33:40+00:00" } ], - "aliases": [ - { - "package": "utopia-php/framework", - "version": "dev-fix-prevent-duplicate-compression", - "alias": "0.33.99", - "alias_normalized": "0.33.99.0" - } - ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/framework": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/docker-compose.yml b/docker-compose.yml index 5deb04316e..1cf0af5078 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,6 +76,7 @@ services: - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw - appwrite-functions:/storage/functions:rw + - appwrite-sites:/storage/sites:rw - appwrite-builds:/storage/builds:rw - ./phpunit.xml:/usr/src/code/phpunit.xml - ./tests:/usr/src/code/tests @@ -141,6 +142,7 @@ 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 @@ -204,7 +206,7 @@ services: appwrite-console: <<: *x-logging container_name: appwrite-console - image: appwrite/console:5.3.0-sites-rc.4 + image: appwrite/console:5.3.0-sites-rc.16 restart: unless-stopped networks: - appwrite @@ -347,6 +349,7 @@ services: - appwrite-uploads:/storage/uploads:rw - appwrite-cache:/storage/cache:rw - appwrite-functions:/storage/functions:rw + - appwrite-sites:/storage/sites:rw - appwrite-builds:/storage/builds:rw - appwrite-certificates:/storage/certificates:rw - ./app:/usr/src/code/app @@ -369,6 +372,7 @@ 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 @@ -432,7 +436,9 @@ services: - appwrite volumes: - appwrite-functions:/storage/functions:rw + - appwrite-sites:/storage/sites:rw - appwrite-builds:/storage/builds:rw + - appwrite-uploads:/storage/uploads:rw - ./app:/usr/src/code/app - ./src:/usr/src/code/src depends_on: @@ -470,6 +476,7 @@ 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 @@ -636,6 +643,7 @@ 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 @@ -727,10 +735,72 @@ services: - _APP_MAINTENANCE_DELAY - _APP_DATABASE_SHARED_TABLES - appwrite-worker-usage: - entrypoint: worker-usage + appwrite-task-stats-resources: + container_name: appwrite-task-stats-resources + entrypoint: stats-resources <<: *x-logging - container_name: appwrite-worker-usage + 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 image: appwrite-dev networks: - appwrite @@ -758,10 +828,10 @@ services: - _APP_USAGE_AGGREGATION_INTERVAL - _APP_DATABASE_SHARED_TABLES - appwrite-worker-usage-dump: - entrypoint: worker-usage-dump + appwrite-worker-stats-usage-dump: + entrypoint: worker-stats-usage-dump <<: *x-logging - container_name: appwrite-worker-usage-dump + container_name: appwrite-worker-stats-usage-dump image: appwrite-dev networks: - appwrite @@ -788,7 +858,8 @@ 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 @@ -880,12 +951,18 @@ services: environment: - _APP_ASSISTANT_OPENAI_API_KEY + appwrite-browser: + container_name: appwrite-browser + image: appwrite/browser:0.2.1 + networks: + - appwrite + openruntimes-executor: container_name: openruntimes-executor hostname: exc1 <<: *x-logging stop_signal: SIGINT - image: openruntimes/executor:0.7.4 + image: openruntimes/executor:0.7.8 restart: unless-stopped networks: - appwrite @@ -894,6 +971,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - appwrite-builds:/storage/builds:rw - appwrite-functions:/storage/functions:rw + - appwrite-sites:/storage/sites:rw # Host mount nessessary to share files between executor and runtimes. # It's not possible to share mount file between 2 containers without host mount (copying is too slow) - /tmp:/tmp:rw @@ -914,6 +992,7 @@ 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 @@ -1064,5 +1143,6 @@ volumes: appwrite-uploads: appwrite-certificates: appwrite-functions: + appwrite-sites: appwrite-builds: appwrite-config: \ No newline at end of file diff --git a/docs/references/databases/create-document.md b/docs/references/databases/create-document.md index a2444d58a4..643518df51 100644 --- a/docs/references/databases/create-document.md +++ b/docs/references/databases/create-document.md @@ -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. \ No newline at end of file +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. diff --git a/docs/references/health/get-queue-stats-resources.md b/docs/references/health/get-queue-stats-resources.md new file mode 100644 index 0000000000..5221327467 --- /dev/null +++ b/docs/references/health/get-queue-stats-resources.md @@ -0,0 +1 @@ +Get the number of metrics that are waiting to be processed in the Appwrite stats resources queue. \ No newline at end of file diff --git a/docs/references/health/get-queue-usage-dump.md b/docs/references/health/get-queue-stats-usage-dump.md similarity index 100% rename from docs/references/health/get-queue-usage-dump.md rename to docs/references/health/get-queue-stats-usage-dump.md diff --git a/docs/references/health/get-queue-usage.md b/docs/references/health/get-queue-stats-usage.md similarity index 100% rename from docs/references/health/get-queue-usage.md rename to docs/references/health/get-queue-stats-usage.md diff --git a/docs/references/proxy/create-rule.md b/docs/references/proxy/create-rule.md deleted file mode 100644 index be567b1cc0..0000000000 --- a/docs/references/proxy/create-rule.md +++ /dev/null @@ -1 +0,0 @@ -Create a new proxy rule. \ No newline at end of file diff --git a/docs/tutorials/add-oauth2-provider.md b/docs/tutorials/add-oauth2-provider.md index ab33f70cb2..da5f514919 100644 --- a/docs/tutorials/add-oauth2-provider.md +++ b/docs/tutorials/add-oauth2-provider.md @@ -175,9 +175,9 @@ Please mention in your documentation what resources or API docs you used to impl ## 3. Test your provider -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. +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. -> To start Appwrite console from the source code, you can simply run `docker compose up -d'. +> To start the 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). diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000000..b18f3d6d58 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + level: 8 + paths: + - src/Appwrite/Transformation + scanDirectories: + - vendor/swoole/ide-helper + excludePaths: + - tests/resources \ No newline at end of file diff --git a/public/images/sites/templates/astro-starter-dark.png b/public/images/sites/templates/astro-starter-dark.png new file mode 100644 index 0000000000..3d064c5cdd Binary files /dev/null and b/public/images/sites/templates/astro-starter-dark.png differ diff --git a/public/images/sites/templates/astro-starter-light.png b/public/images/sites/templates/astro-starter-light.png new file mode 100644 index 0000000000..3d064c5cdd Binary files /dev/null and b/public/images/sites/templates/astro-starter-light.png differ diff --git a/public/images/sites/templates/flutter-starter-dark.png b/public/images/sites/templates/flutter-starter-dark.png new file mode 100644 index 0000000000..9b239ff767 Binary files /dev/null and b/public/images/sites/templates/flutter-starter-dark.png differ diff --git a/public/images/sites/templates/flutter-starter-light.png b/public/images/sites/templates/flutter-starter-light.png new file mode 100644 index 0000000000..9b239ff767 Binary files /dev/null and b/public/images/sites/templates/flutter-starter-light.png differ diff --git a/public/images/sites/templates/nextjs-starter-dark.png b/public/images/sites/templates/nextjs-starter-dark.png new file mode 100644 index 0000000000..022e981d08 Binary files /dev/null and b/public/images/sites/templates/nextjs-starter-dark.png differ diff --git a/public/images/sites/templates/nextjs-starter-light.png b/public/images/sites/templates/nextjs-starter-light.png new file mode 100644 index 0000000000..0e19bf5d37 Binary files /dev/null and b/public/images/sites/templates/nextjs-starter-light.png differ diff --git a/public/images/sites/templates/nuxt-starter-dark.png b/public/images/sites/templates/nuxt-starter-dark.png new file mode 100644 index 0000000000..bea04f1cf9 Binary files /dev/null and b/public/images/sites/templates/nuxt-starter-dark.png differ diff --git a/public/images/sites/templates/nuxt-starter-light.png b/public/images/sites/templates/nuxt-starter-light.png new file mode 100644 index 0000000000..006e366a9f Binary files /dev/null and b/public/images/sites/templates/nuxt-starter-light.png differ diff --git a/public/images/sites/templates/remix-starter-dark.png b/public/images/sites/templates/remix-starter-dark.png new file mode 100644 index 0000000000..47c0cc0394 Binary files /dev/null and b/public/images/sites/templates/remix-starter-dark.png differ diff --git a/public/images/sites/templates/remix-starter-light.png b/public/images/sites/templates/remix-starter-light.png new file mode 100644 index 0000000000..73c2d12625 Binary files /dev/null and b/public/images/sites/templates/remix-starter-light.png differ diff --git a/public/images/sites/templates/starter-for-nextjs-dark.png b/public/images/sites/templates/starter-for-nextjs-dark.png new file mode 100644 index 0000000000..7acc80d4d4 Binary files /dev/null and b/public/images/sites/templates/starter-for-nextjs-dark.png differ diff --git a/public/images/sites/templates/starter-for-nextjs-light.png b/public/images/sites/templates/starter-for-nextjs-light.png new file mode 100644 index 0000000000..7acc80d4d4 Binary files /dev/null and b/public/images/sites/templates/starter-for-nextjs-light.png differ diff --git a/public/images/sites/templates/starter-for-nuxt-dark.png b/public/images/sites/templates/starter-for-nuxt-dark.png new file mode 100644 index 0000000000..ab8884febd Binary files /dev/null and b/public/images/sites/templates/starter-for-nuxt-dark.png differ diff --git a/public/images/sites/templates/starter-for-nuxt-light.png b/public/images/sites/templates/starter-for-nuxt-light.png new file mode 100644 index 0000000000..ab8884febd Binary files /dev/null and b/public/images/sites/templates/starter-for-nuxt-light.png differ diff --git a/public/images/sites/templates/starter-for-react-dark.png b/public/images/sites/templates/starter-for-react-dark.png new file mode 100644 index 0000000000..dcfc0a5521 Binary files /dev/null and b/public/images/sites/templates/starter-for-react-dark.png differ diff --git a/public/images/sites/templates/starter-for-react-light.png b/public/images/sites/templates/starter-for-react-light.png new file mode 100644 index 0000000000..dcfc0a5521 Binary files /dev/null and b/public/images/sites/templates/starter-for-react-light.png differ diff --git a/public/images/sites/templates/starter-for-svelte-dark.png b/public/images/sites/templates/starter-for-svelte-dark.png new file mode 100644 index 0000000000..f25943ae48 Binary files /dev/null and b/public/images/sites/templates/starter-for-svelte-dark.png differ diff --git a/public/images/sites/templates/starter-for-svelte-light.png b/public/images/sites/templates/starter-for-svelte-light.png new file mode 100644 index 0000000000..f25943ae48 Binary files /dev/null and b/public/images/sites/templates/starter-for-svelte-light.png differ diff --git a/public/images/sites/templates/starter-for-vue-dark.png b/public/images/sites/templates/starter-for-vue-dark.png new file mode 100644 index 0000000000..9d9af29f87 Binary files /dev/null and b/public/images/sites/templates/starter-for-vue-dark.png differ diff --git a/public/images/sites/templates/starter-for-vue-light.png b/public/images/sites/templates/starter-for-vue-light.png new file mode 100644 index 0000000000..9d9af29f87 Binary files /dev/null and b/public/images/sites/templates/starter-for-vue-light.png differ diff --git a/public/images/sites/templates/sveltekit-starter-dark.png b/public/images/sites/templates/sveltekit-starter-dark.png new file mode 100644 index 0000000000..90ac7f96aa Binary files /dev/null and b/public/images/sites/templates/sveltekit-starter-dark.png differ diff --git a/public/images/sites/templates/sveltekit-starter-light.png b/public/images/sites/templates/sveltekit-starter-light.png new file mode 100644 index 0000000000..90ac7f96aa Binary files /dev/null and b/public/images/sites/templates/sveltekit-starter-light.png differ diff --git a/public/images/sites/templates/template-for-blog-dark.png b/public/images/sites/templates/template-for-blog-dark.png new file mode 100644 index 0000000000..b076b918a8 Binary files /dev/null and b/public/images/sites/templates/template-for-blog-dark.png differ diff --git a/public/images/sites/templates/template-for-blog-light.png b/public/images/sites/templates/template-for-blog-light.png new file mode 100644 index 0000000000..b076b918a8 Binary files /dev/null and b/public/images/sites/templates/template-for-blog-light.png differ diff --git a/public/images/sites/templates/template-for-event-dark.png b/public/images/sites/templates/template-for-event-dark.png new file mode 100644 index 0000000000..c3376d3ff6 Binary files /dev/null and b/public/images/sites/templates/template-for-event-dark.png differ diff --git a/public/images/sites/templates/template-for-event-light.png b/public/images/sites/templates/template-for-event-light.png new file mode 100644 index 0000000000..c97901326a Binary files /dev/null and b/public/images/sites/templates/template-for-event-light.png differ diff --git a/public/images/sites/templates/template-for-onelink-dark.png b/public/images/sites/templates/template-for-onelink-dark.png new file mode 100644 index 0000000000..4680399672 Binary files /dev/null and b/public/images/sites/templates/template-for-onelink-dark.png differ diff --git a/public/images/sites/templates/template-for-onelink-light.png b/public/images/sites/templates/template-for-onelink-light.png new file mode 100644 index 0000000000..7a040b5d8d Binary files /dev/null and b/public/images/sites/templates/template-for-onelink-light.png differ diff --git a/public/images/sites/templates/template-for-portfolio-dark.png b/public/images/sites/templates/template-for-portfolio-dark.png new file mode 100644 index 0000000000..4b4382bf80 Binary files /dev/null and b/public/images/sites/templates/template-for-portfolio-dark.png differ diff --git a/public/images/sites/templates/template-for-portfolio-light.png b/public/images/sites/templates/template-for-portfolio-light.png new file mode 100644 index 0000000000..4b4382bf80 Binary files /dev/null and b/public/images/sites/templates/template-for-portfolio-light.png differ diff --git a/public/images/sites/templates/template-for-store-dark.png b/public/images/sites/templates/template-for-store-dark.png new file mode 100644 index 0000000000..645671ab55 Binary files /dev/null and b/public/images/sites/templates/template-for-store-dark.png differ diff --git a/public/images/sites/templates/template-for-store-light.png b/public/images/sites/templates/template-for-store-light.png new file mode 100644 index 0000000000..645671ab55 Binary files /dev/null and b/public/images/sites/templates/template-for-store-light.png differ diff --git a/src/Appwrite/Auth/Key.php b/src/Appwrite/Auth/Key.php new file mode 100644 index 0000000000..83f8dd408d --- /dev/null +++ b/src/Appwrite/Auth/Key.php @@ -0,0 +1,184 @@ +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; + } + + + public function getHostnameOverride(): bool + { + return $this->hostnameOverride; + } + + + public function isBannerDisabled(): bool + { + return $this->bannerDisabled; + } + + public function isProjectCheckDisabled(): bool + { + return $this->projectCheckDisabled; + } + + /** + * 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'] ?? []; + $hostnameOverride = $payload['hostnameOverride'] ?? false; + $bannerDisabled = $payload['bannerDisabled'] ?? false; + $projectCheckDisabled = $payload['projectCheckDisabled'] ?? false; + $scopes = \array_merge($payload['scopes'] ?? [], $scopes); + + if (!$projectCheckDisabled && $projectId !== $project->getId()) { + return $guestKey; + } + + return new Key( + $projectId, + $type, + $role, + $scopes, + $name, + $expired, + $disabledMetrics, + $hostnameOverride, + $bannerDisabled, + $projectCheckDisabled + ); + 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; + } + } +} diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index ff55ad877a..1e19072138 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -27,11 +27,22 @@ class Event public const SITES_QUEUE_NAME = 'v1-sites'; public const SITES_CLASS_NAME = 'SitesV1'; + /** 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'; diff --git a/src/Appwrite/Event/StatsResources.php b/src/Appwrite/Event/StatsResources.php new file mode 100644 index 0000000000..e7a3df97e0 --- /dev/null +++ b/src/Appwrite/Event/StatsResources.php @@ -0,0 +1,29 @@ +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 + ]; + } +} diff --git a/src/Appwrite/Event/StatsUsage.php b/src/Appwrite/Event/StatsUsage.php new file mode 100644 index 0000000000..e259ba5e04 --- /dev/null +++ b/src/Appwrite/Event/StatsUsage.php @@ -0,0 +1,86 @@ +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; + }), + ]; + } +} diff --git a/src/Appwrite/Event/StatsUsageDump.php b/src/Appwrite/Event/StatsUsageDump.php new file mode 100644 index 0000000000..0573a88040 --- /dev/null +++ b/src/Appwrite/Event/StatsUsageDump.php @@ -0,0 +1,44 @@ +setQueue(Event::STATS_USAGE_DUMP_QUEUE_NAME) + ->setClass(Event::STATS_USAGE_DUMP_CLASS_NAME); + } + + /** + * Add Stats. + * + * @param array $stats + * @return self + */ + public function setStats(array $stats): self + { + $this->stats = $stats; + + return $this; + } + + /** + * Prepare the payload for the usage dump event. + * + * @return array + */ + protected function preparePayload(): array + { + return [ + 'stats' => $this->stats, + ]; + } +} diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 2992c75987..f1494b5d67 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -118,6 +118,9 @@ class Exception extends \Exception public const TEAM_INVITE_MISMATCH = 'team_invite_mismatch'; public const TEAM_ALREADY_EXISTS = 'team_already_exists'; + /** Console */ + public const RESOURCE_ALREADY_EXISTS = 'resource_already_exists'; + /** Membership */ public const MEMBERSHIP_NOT_FOUND = 'membership_not_found'; public const MEMBERSHIP_ALREADY_CONFIRMED = 'membership_already_confirmed'; @@ -255,6 +258,7 @@ class Exception extends \Exception /** Variables */ public const VARIABLE_NOT_FOUND = 'variable_not_found'; public const VARIABLE_ALREADY_EXISTS = 'variable_already_exists'; + public const VARIABLE_CANNOT_UNSET_SECRET = 'variable_cannot_unset_secret'; /** Platform */ public const PLATFORM_NOT_FOUND = 'platform_not_found'; diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 19f69b1a4f..56016f1057 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -92,6 +92,7 @@ abstract class Migration '1.5.11' => 'V20', '1.6.0' => 'V21', '1.6.1' => 'V21', + '1.6.2' => 'V22', ]; /** diff --git a/src/Appwrite/Migration/Version/V22.php b/src/Appwrite/Migration/Version/V22.php new file mode 100644 index 0000000000..4d15662112 --- /dev/null +++ b/src/Appwrite/Migration/Version/V22.php @@ -0,0 +1,83 @@ + 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); + } + } +} diff --git a/src/Appwrite/Platform/Action.php b/src/Appwrite/Platform/Action.php new file mode 100644 index 0000000000..72c41582ea --- /dev/null +++ b/src/Appwrite/Platform/Action.php @@ -0,0 +1,90 @@ +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)]; + } + } +} diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php index b77ccce979..be6ddbae8a 100644 --- a/src/Appwrite/Platform/Appwrite.php +++ b/src/Appwrite/Platform/Appwrite.php @@ -2,8 +2,10 @@ namespace Appwrite\Platform; +use Appwrite\Platform\Modules\Console; use Appwrite\Platform\Modules\Core; use Appwrite\Platform\Modules\Functions; +use Appwrite\Platform\Modules\Proxy; use Appwrite\Platform\Modules\Sites; use Utopia\Platform\Platform; @@ -14,5 +16,7 @@ class Appwrite extends Platform parent::__construct(new Core()); $this->addModule(new Functions\Module()); $this->addModule(new Sites\Module()); + $this->addModule(new Console\Module()); + $this->addModule(new Proxy\Module()); } } diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 9ea0ef86c5..7880df189e 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -4,6 +4,7 @@ namespace Appwrite\Platform\Modules\Compute; use Appwrite\Event\Build; use Appwrite\Extend\Exception; +use Appwrite\Query; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Document; @@ -19,7 +20,7 @@ use Utopia\VCS\Exception\RepositoryNotFound; class Base extends Action { - public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, GitHub $github) + public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, string $referenceType = 'branch', string $reference = ''): Document { $deploymentId = ID::unique(); $entrypoint = $function->getAttribute('entrypoint', ''); @@ -37,7 +38,12 @@ class Base extends Action } catch (RepositoryNotFound $e) { throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); } - $providerBranch = $function->getAttribute('providerBranch', 'main'); + + // TODO: Support tag and commit in future + if ($referenceType === 'branch') { + $providerBranch = empty($reference) ? $function->getAttribute('providerBranch', 'main') : $reference; + } + $authorUrl = "https://github.com/$owner"; $repositoryUrl = "https://github.com/$owner/$repositoryName"; $branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch"; @@ -83,7 +89,7 @@ class Base extends Action 'providerBranch' => $providerBranch, 'providerRootDirectory' => $function->getAttribute('providerRootDirectory', ''), 'search' => implode(' ', [$deploymentId, $entrypoint]), - 'activate' => true, + 'activate' => $activate, ])); $queueForBuilds @@ -91,9 +97,11 @@ class Base extends Action ->setResource($function) ->setDeployment($deployment) ->setTemplate($template); + + return $deployment; } - public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github) + public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, string $referenceType = 'branch', string $reference = ''): Document { $deploymentId = ID::unique(); $providerInstallationId = $installation->getAttribute('providerInstallationId', ''); @@ -111,7 +119,11 @@ class Base extends Action throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); } - $providerBranch = $site->getAttribute('providerBranch', 'main'); + // TODO: Support tag and commit in future + if ($referenceType === 'branch') { + $providerBranch = empty($reference) ? $site->getAttribute('providerBranch', 'main') : $reference; + } + $authorUrl = "https://github.com/$owner"; $repositoryUrl = "https://github.com/$owner/$repositoryName"; $branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch"; @@ -158,27 +170,23 @@ class Base extends Action 'providerBranch' => $providerBranch, 'providerRootDirectory' => $site->getAttribute('providerRootDirectory', ''), 'search' => implode(' ', [$deploymentId]), - 'activate' => true, + 'activate' => $activate, ])); - // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); @@ -187,5 +195,41 @@ class Base extends Action ->setResource($site) ->setDeployment($deployment) ->setTemplate($template); + + return $deployment; + } + + protected function listRules(Document $project, array $queries, Database $database, callable $callback): void + { + $limit = 100; + $cursor = null; + + do { + $queries = \array_merge([ + Query::limit($limit), + Query::equal("projectInternalId", [$project->getInternalId()]) + ], $queries); + + if ($cursor !== null) { + $queries[] = Query::cursorAfter($cursor); + } + + $results = $database->find('rules', $queries); + + $total = \count($results); + if ($total > 0) { + $cursor = $results[$total - 1]; + } + + if ($total < $limit) { + $cursor = null; + } + + foreach ($results as $document) { + if (is_callable($callback)) { + $callback($document); + } + } + } while (!\is_null($cursor)); } } diff --git a/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php new file mode 100644 index 0000000000..28e7194b2c --- /dev/null +++ b/src/Appwrite/Platform/Modules/Console/Http/Resources/Get.php @@ -0,0 +1,85 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/console/resources') + ->desc('Check resource ID availability') + ->groups(['api', 'projects']) + ->label('scope', 'rules.read') + ->label('sdk', new Method( + namespace: 'console', + name: 'getResource', + description: <<label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}, url:{url}') + ->label('abuse-time', 60) + ->param('value', '', new Text(256), 'Resource value.') + ->param('type', '', new WhiteList(['rules']), 'Resource type.') + ->inject('response') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $value, string $type, Response $response, Database $dbForPlatform) + { + if ($type === 'rules') { + $validator = new Domain($value); + + if (!$validator->isValid($value)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $validator->getDescription()); + } + + $document = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ + Query::equal('domain', [$value]), + ])); + + if (!$document->isEmpty()) { + throw new Exception(Exception::RESOURCE_ALREADY_EXISTS); + } + + $response->noContent(); + } + + // Only occurs if type is added into whitelist, but not supported in action + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid type'); + } +} diff --git a/src/Appwrite/Platform/Modules/Console/Module.php b/src/Appwrite/Platform/Modules/Console/Module.php new file mode 100644 index 0000000000..7bf2805479 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Console/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Console/Services/Http.php b/src/Appwrite/Platform/Modules/Console/Services/Http.php new file mode 100644 index 0000000000..6221db6a96 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Console/Services/Http.php @@ -0,0 +1,16 @@ +type = Service::TYPE_HTTP; + // Resources + $this->addAction(GetResourceAvailability::getName(), new GetResourceAvailability()); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php new file mode 100644 index 0000000000..5bde7903d9 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php @@ -0,0 +1,109 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId') + ->desc('Delete deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].deployments.[deploymentId].delete') + ->label('audits.event', 'deployment.delete') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'deleteDeployment', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDeletes') + ->inject('queueForEvents') + ->inject('deviceForFunctions') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Device $deviceForFunctions) + { + $function = $dbForProject->getDocument('functions', $functionId); + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($deployment->getAttribute('resourceId') !== $function->getId()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if (!$dbForProject->deleteDocument('deployments', $deployment->getId())) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB'); + } + + if (!empty($deployment->getAttribute('path', ''))) { + if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage'); + } + } + + if ($function->getAttribute('deployment') === $deployment->getId()) { // Reset function deployment + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ + 'deployment' => '', + 'deploymentInternalId' => '', + ]))); + } + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($deployment); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Download/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php similarity index 54% rename from src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Download/Get.php rename to src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php index f86fa591c1..a63ac18354 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php @@ -1,6 +1,6 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build/download') - ->desc('Download build') - ->groups(['api', 'sites']) - ->label('scope', 'sites.read') + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/download') + ->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download', [ 'type' => 'output' ]) + ->groups(['api', 'functions']) + ->desc('Download deployment') + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( - namespace: 'sites', - name: 'getDeploymentBuildDownload', + namespace: 'functions', + name: 'getDeploymentDownload', description: <<param('siteId', '', new UID(), 'Site ID.') + ->param('functionId', '', new UID(), 'Function ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true) ->inject('response') ->inject('request') ->inject('dbForProject') + ->inject('deviceForFunctions') ->inject('deviceForBuilds') ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForBuilds) + public function action(string $functionId, string $deploymentId, string $type, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions, Device $deviceForBuilds) { - $site = $dbForProject->getDocument('sites', $siteId); - if ($site->isEmpty()) { - throw new Exception(Exception::SITE_NOT_FOUND); + $function = $dbForProject->getDocument('functions', $functionId); + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); } $deployment = $dbForProject->getDocument('deployments', $deploymentId); @@ -70,27 +75,37 @@ class Get extends Action throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } - if ($deployment->getAttribute('resourceId') !== $site->getId()) { + if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } - $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId')); - if ($build->isEmpty()) { - throw new Exception(Exception::BUILD_NOT_FOUND); + switch ($type) { + case 'output': + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId')); + if ($build->isEmpty()) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + $path = $build->getAttribute('path', ''); + $device = $deviceForBuilds; + break; + case 'source': + $path = $deployment->getAttribute('path', ''); + $device = $deviceForFunctions; + break; } - $path = $build->getAttribute('path', ''); - if (!$deviceForBuilds->exists($path)) { - throw new Exception(Exception::BUILD_NOT_FOUND); + if (!$device->exists($path)) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } $response ->setContentType('application/gzip') - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache + ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days ->addHeader('X-Peak', \memory_get_peak_usage()) ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); - $size = $deviceForBuilds->getFileSize($path); + $size = $device->getFileSize($path); $rangeHeader = $request->getHeader('range'); if (!empty($rangeHeader)) { @@ -112,13 +127,13 @@ class Get extends Action ->addHeader('Content-Length', $end - $start + 1) ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); - $response->send($deviceForBuilds->read($path, $start, ($end - $start + 1))); + $response->send($device->read($path, $start, ($end - $start + 1))); } if ($size > APP_STORAGE_READ_BUFFER) { for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { $response->chunk( - $deviceForBuilds->read( + $device->read( $path, ($i * MAX_OUTPUT_CHUNK_SIZE), min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) @@ -127,7 +142,7 @@ class Get extends Action ); } } else { - $response->send($deviceForBuilds->read($path)); + $response->send($device->read($path)); } } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php new file mode 100644 index 0000000000..624a410dcd --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php @@ -0,0 +1,115 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions/:functionId/deployments/duplicate') + ->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build') + ->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') + ->desc('Create duplicate deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].deployments.[deploymentId].update') + ->label('audits.event', 'deployment.update') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'createDuplicateDeployment', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('deviceForFunctions') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, string $buildId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $path = $deployment->getAttribute('path'); + if (empty($path) || !$deviceForFunctions->exists($path)) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $deploymentId = ID::unique(); + + $destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); + $deviceForFunctions->transfer($path, $destination, $deviceForFunctions); + + $deployment->removeAttribute('$internalId'); + $deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([ + '$internalId' => '', + '$id' => $deploymentId, + 'buildId' => '', + 'buildInternalId' => '', + 'path' => $destination, + 'entrypoint' => $function->getAttribute('entrypoint'), + 'commands' => $function->getAttribute('commands', ''), + 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]), + ])); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($function) + ->setDeployment($deployment); + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php new file mode 100644 index 0000000000..1d074e9e2a --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Get.php @@ -0,0 +1,81 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId') + ->desc('Get deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'getDeployment', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->getAttribute('resourceId') !== $function->getId()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); + $deployment->setAttribute('status', $build->getAttribute('status', 'waiting')); + $deployment->setAttribute('buildLogs', $build->getAttribute('logs', '')); + $deployment->setAttribute('buildTime', $build->getAttribute('duration', 0)); + $deployment->setAttribute('buildSize', $build->getAttribute('size', 0)); + $deployment->setAttribute('size', $deployment->getAttribute('size', 0)); + + $response->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php new file mode 100644 index 0000000000..e349292e4b --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php @@ -0,0 +1,137 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/status') + ->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build') + ->desc('Update deployment status') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('audits.event', 'deployment.update') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'updateDeploymentStatus', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('project') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + + if ($build->isEmpty()) { + $buildId = ID::unique(); + $build = $dbForProject->createDocument('builds', new Document([ + '$id' => $buildId, + '$permissions' => [], + 'startTime' => DateTime::now(), + 'deploymentInternalId' => $deployment->getInternalId(), + 'deploymentId' => $deployment->getId(), + 'status' => 'canceled', + 'path' => '', + 'runtime' => $function->getAttribute('runtime'), + 'source' => $deployment->getAttribute('path', ''), + 'sourceType' => '', + 'logs' => '', + 'duration' => 0, + 'size' => 0 + ])); + + $deployment->setAttribute('buildId', $build->getId()); + $deployment->setAttribute('buildInternalId', $build->getInternalId()); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + } else { + if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) { + throw new Exception(Exception::BUILD_ALREADY_COMPLETED); + } + + $startTime = new \DateTime($build->getAttribute('startTime')); + $endTime = new \DateTime('now'); + $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); + + $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([ + 'endTime' => DateTime::now(), + 'duration' => $duration, + 'status' => 'canceled' + ])); + } + + $dbForProject->purgeCachedDocument('deployments', $deployment->getId()); + + try { + $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); + } catch (\Throwable $th) { + // Don't throw if the deployment doesn't exist + if ($th->getCode() !== 404) { + throw $th; + } + } + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response->dynamic($build, Response::MODEL_BUILD); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php new file mode 100644 index 0000000000..9a61693e15 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -0,0 +1,153 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions/:functionId/deployments/template') + ->desc('Create template deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('event', 'functions.[functionId].deployments.[deploymentId].create') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('audits.event', 'deployment.create') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'createTemplateDeployment', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('repository', '', new Text(128, 0), 'Repository name of the template.') + ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') + ->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.') + ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.') + ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('queueForEvents') + ->inject('project') + ->inject('queueForBuilds') + ->inject('gitHub') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $repository, string $owner, string $rootDirectory, string $version, bool $activate, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Document $project, Build $queueForBuilds, GitHub $github) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $template = new Document([ + 'repositoryName' => $repository, + 'ownerName' => $owner, + 'rootDirectory' => $rootDirectory, + 'version' => $version + ]); + + if (!empty($function->getAttribute('providerRepositoryId'))) { + $installation = $dbForPlatform->getDocument('installations', $function->getAttribute('installationId')); + + $deployment = $this->redeployVcsFunction( + request: $request, + function: $function, + project: $project, + installation: $installation, + dbForProject: $dbForProject, + queueForBuilds: $queueForBuilds, + template: $template, + github: $github, + activate: $activate + ); + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + + return; + } + + $deploymentId = ID::unique(); + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceId' => $function->getId(), + 'resourceInternalId' => $function->getInternalId(), + 'resourceType' => 'functions', + 'entrypoint' => $function->getAttribute('entrypoint', ''), + 'commands' => $function->getAttribute('commands', ''), + 'type' => 'manual', + 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint', '')]), + 'activate' => $activate, + ])); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($function) + ->setDeployment($deployment) + ->setTemplate($template); + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php new file mode 100644 index 0000000000..53c4dcb8c9 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php @@ -0,0 +1,111 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions/:functionId/deployments/vcs') + ->desc('Create VCS deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].deployments.[deploymentId].create') + ->label('audits.event', 'deployment.create') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'createVcsDeployment', + description: <<param('functionId', '', new UID(), 'Function ID.') + // TODO: Support tag and commit in future + ->param('type', '', new WhiteList(['branch']), 'Type of reference passed. Allowed values are: branch') + ->param('reference', '', new Text(255), 'VCS reference to create deployment from. Depending on type this can be: branch name') + ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('project') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('gitHub') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $type, string $reference, bool $activate, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project, Event $queueForEvents, Build $queueForBuilds, GitHub $github) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $template = new Document(); + + $installation = $dbForPlatform->getDocument('installations', $function->getAttribute('installationId')); + + $deployment = $this->redeployVcsFunction( + request: $request, + function: $function, + project: $project, + installation: $installation, + dbForProject: $dbForProject, + queueForBuilds: $queueForBuilds, + template: $template, + github: $github, + activate: $activate, + reference: $reference, + referenceType: $type + ); + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php new file mode 100644 index 0000000000..9e03a86bc7 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/XList.php @@ -0,0 +1,127 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/deployments') + ->desc('List deployments') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listDeployments', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('queries', [], new Deployments(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, array $queries, string $search, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + // Set resource queries + $queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]); + $queries[] = Query::equal('resourceType', ['functions']); + + /** + * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries + */ + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); + }); + $cursor = reset($cursor); + if ($cursor) { + /** @var Query $cursor */ + + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $deploymentId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('deployments', $deploymentId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + $results = $dbForProject->find('deployments', $queries); + $total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT); + + foreach ($results as $result) { + $build = $dbForProject->getDocument('builds', $result->getAttribute('buildId', '')); + $result->setAttribute('status', $build->getAttribute('status', 'processing')); + $result->setAttribute('buildLogs', $build->getAttribute('logs', '')); + $result->setAttribute('buildTime', $build->getAttribute('duration', 0)); + $result->setAttribute('buildSize', $build->getAttribute('size', 0)); + $result->setAttribute('size', $result->getAttribute('size', 0)); + } + + $response->dynamic(new Document([ + 'deployments' => $results, + 'total' => $total, + ]), Response::MODEL_DEPLOYMENT_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php new file mode 100644 index 0000000000..f522e2dc62 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -0,0 +1,474 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions/:functionId/executions') + ->desc('Create execution') + ->groups(['api', 'functions']) + ->label('scope', 'execution.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].executions.[executionId].create') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'createExecution', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true) + ->param('async', false, new Boolean(true), 'Execute code in the background. Default value is false.', true) + ->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true) + ->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'HTTP method of execution. Default value is GET.', true) + ->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_MINUTES, offset: 60), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true) + ->inject('response') + ->inject('request') + ->inject('project') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('user') + ->inject('queueForEvents') + ->inject('queueForStatsUsage') + ->inject('queueForFunctions') + ->inject('geodb') + ->callback([$this, 'action']); + } + + public function action(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) + { + $async = \strval($async) === 'true' || \strval($async) === '1'; + + if (!$async && !is_null($scheduledAt)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Scheduled executions must run asynchronously. Set scheduledAt to a future date, or set async to true.'); + } + + /** + * @var array $headers + */ + $assocParams = ['headers']; + foreach ($assocParams as $assocParam) { + if (!empty('headers') && !is_array($$assocParam)) { + $$assocParam = \json_decode($$assocParam, true); + } + } + + $booleanParams = ['async']; + foreach ($booleanParams as $booleamParam) { + if (!empty($$booleamParam) && !is_bool($$booleamParam)) { + $$booleamParam = $$booleamParam === "true" ? true : false; + } + } + + // 'headers' validator + $validator = new Headers(); + if (!$validator->isValid($headers)) { + throw new Exception($validator->getDescription(), 400); + } + + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $version = $function->getAttribute('version', 'v2'); + $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); + $spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; + + $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; + + if (\is_null($runtime)) { + throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); + } + + $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + + if ($deployment->getAttribute('resourceId') !== $function->getId()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); + } + + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); + } + + /** Check if build has completed */ + $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + if ($build->isEmpty()) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + if ($build->getAttribute('status') !== 'ready') { + throw new Exception(Exception::BUILD_NOT_READY); + } + + $validator = new Authorization('execute'); + + if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function + throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); + } + + $jwt = ''; // initialize + if (!$user->isEmpty()) { // If userId exists, generate a JWT for function + $sessions = $user->getAttribute('sessions', []); + $current = new Document(); + + foreach ($sessions as $session) { + /** @var Utopia\Database\Document $session */ + if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too + $current = $session; + } + } + + if (!$current->isEmpty()) { + $jwtExpiry = $function->getAttribute('timeout', 900); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); + $jwt = $jwtObj->encode([ + 'userId' => $user->getId(), + 'sessionId' => $current->getId(), + ]); + } + } + + $jwtExpiry = $function->getAttribute('timeout', 900); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); + $apiKey = $jwtObj->encode([ + 'projectId' => $project->getId(), + 'scopes' => $function->getAttribute('scopes', []) + ]); + + $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; + $headers['x-appwrite-trigger'] = 'http'; + $headers['x-appwrite-user-id'] = $user->getId() ?? ''; + $headers['x-appwrite-user-jwt'] = $jwt ?? ''; + $headers['x-appwrite-country-code'] = ''; + $headers['x-appwrite-continent-code'] = ''; + $headers['x-appwrite-continent-eu'] = 'false'; + + $ip = $headers['x-real-ip'] ?? ''; + if (!empty($ip)) { + $record = $geodb->get($ip); + + if ($record) { + $eu = Config::getParam('locale-eu'); + + $headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? ''; + $headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? ''; + $headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false'; + } + } + + $headersFiltered = []; + foreach ($headers as $key => $value) { + if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { + $headersFiltered[] = ['name' => $key, 'value' => $value]; + } + } + + $executionId = ID::unique(); + + $status = $async ? 'waiting' : 'processing'; + + if (!is_null($scheduledAt)) { + $status = 'scheduled'; + } + + $execution = new Document([ + '$id' => $executionId, + '$permissions' => !$user->isEmpty() ? [Permission::read(Role::user($user->getId()))] : [], + 'resourceInternalId' => $function->getInternalId(), + 'resourceId' => $function->getId(), + 'resourceType' => 'functions', + 'deploymentInternalId' => $deployment->getInternalId(), + 'deploymentId' => $deployment->getId(), + 'trigger' => (!is_null($scheduledAt)) ? 'schedule' : 'http', + 'status' => $status, // waiting / processing / completed / failed / scheduled + 'responseStatusCode' => 0, + 'responseHeaders' => [], + 'requestPath' => $path, + 'requestMethod' => $method, + 'requestHeaders' => $headersFiltered, + 'errors' => '', + 'logs' => '', + 'duration' => 0.0, + 'search' => implode(' ', [$functionId, $executionId]), + ]); + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('executionId', $execution->getId()) + ->setContext('function', $function); + + if ($async) { + if (is_null($scheduledAt)) { + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + $queueForFunctions + ->setType('http') + ->setExecution($execution) + ->setFunction($function) + ->setBody($body) + ->setHeaders($headers) + ->setPath($path) + ->setMethod($method) + ->setJWT($jwt) + ->setProject($project) + ->setUser($user) + ->setParam('functionId', $function->getId()) + ->setParam('executionId', $execution->getId()) + ->trigger(); + } else { + $data = [ + 'headers' => $headers, + 'path' => $path, + 'method' => $method, + 'body' => $body, + 'userId' => $user->getId() + ]; + + $schedule = $dbForPlatform->createDocument('schedules', new Document([ + 'region' => System::getEnv('_APP_REGION', 'default'), + 'resourceType' => ScheduleExecutions::getSupportedResource(), + 'resourceId' => $execution->getId(), + 'resourceInternalId' => $execution->getInternalId(), + 'resourceUpdatedAt' => DateTime::now(), + 'projectId' => $project->getId(), + 'schedule' => $scheduledAt, + 'data' => $data, + 'active' => true, + ])); + + $execution = $execution + ->setAttribute('scheduleId', $schedule->getId()) + ->setAttribute('scheduleInternalId', $schedule->getInternalId()) + ->setAttribute('scheduledAt', $scheduledAt); + + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + } + + return $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($execution, Response::MODEL_EXECUTION); + } + + $durationStart = \microtime(true); + + $vars = []; + + // V2 vars + if ($version === 'v2') { + $vars = \array_merge($vars, [ + 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', + 'APPWRITE_FUNCTION_DATA' => $body ?? '', + 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', + 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' + ]); + } + + // Shared vars + foreach ($function->getAttribute('varsProject', []) as $var) { + $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); + } + + // Function vars + foreach ($function->getAttribute('vars', []) as $var) { + $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); + } + + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; + $hostname = System::getEnv('_APP_DOMAIN'); + $endpoint = $protocol . '://' . $hostname . "/v1"; + + // Appwrite vars + $vars = \array_merge($vars, [ + 'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint, + 'APPWRITE_FUNCTION_ID' => $functionId, + 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'), + 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), + 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), + 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', + 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', + 'APPWRITE_FUNCTION_CPUS' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + 'APPWRITE_FUNCTION_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + 'APPWRITE_VERSION' => APP_VERSION_STABLE, + 'APPWRITE_REGION' => $project->getAttribute('region'), + 'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''), + 'APPWRITE_VCS_REPOSITORY_ID' => $deployment->getAttribute('providerRepositoryId', ''), + 'APPWRITE_VCS_REPOSITORY_NAME' => $deployment->getAttribute('providerRepositoryName', ''), + 'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment->getAttribute('providerRepositoryOwner', ''), + 'APPWRITE_VCS_REPOSITORY_URL' => $deployment->getAttribute('providerRepositoryUrl', ''), + 'APPWRITE_VCS_REPOSITORY_BRANCH' => $deployment->getAttribute('providerBranch', ''), + 'APPWRITE_VCS_REPOSITORY_BRANCH_URL' => $deployment->getAttribute('providerBranchUrl', ''), + 'APPWRITE_VCS_COMMIT_HASH' => $deployment->getAttribute('providerCommitHash', ''), + 'APPWRITE_VCS_COMMIT_MESSAGE' => $deployment->getAttribute('providerCommitMessage', ''), + 'APPWRITE_VCS_COMMIT_URL' => $deployment->getAttribute('providerCommitUrl', ''), + 'APPWRITE_VCS_COMMIT_AUTHOR_NAME' => $deployment->getAttribute('providerCommitAuthor', ''), + 'APPWRITE_VCS_COMMIT_AUTHOR_URL' => $deployment->getAttribute('providerCommitAuthorUrl', ''), + 'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''), + ]); + + /** Execute function */ + $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); + try { + $version = $function->getAttribute('version', 'v2'); + $command = $runtime['startCommand']; + $command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'; + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deployment->getId(), + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $function->getAttribute('timeout', 0), + image: $runtime['image'], + source: $build->getAttribute('path', ''), + entrypoint: $deployment->getAttribute('entrypoint', ''), + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $command, + cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, + memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, + logging: $function->getAttribute('logging', true), + requestTimeout: 30 + ); + + $headersFiltered = []; + foreach ($executionResponse['headers'] as $key => $value) { + if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { + $headersFiltered[] = ['name' => $key, 'value' => $value]; + } + } + + /** Update execution status */ + $status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed'; + $execution->setAttribute('status', $status); + $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); + $execution->setAttribute('responseHeaders', $headersFiltered); + $execution->setAttribute('logs', $executionResponse['logs']); + $execution->setAttribute('errors', $executionResponse['errors']); + $execution->setAttribute('duration', $executionResponse['duration']); + } catch (\Throwable $th) { + $durationEnd = \microtime(true); + + $execution + ->setAttribute('duration', $durationEnd - $durationStart) + ->setAttribute('status', 'failed') + ->setAttribute('responseStatusCode', 500) + ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); + Console::error($th->getMessage()); + + if ($th instanceof AppwriteException) { + throw $th; + } + } finally { + $queueForStatsUsage + ->addMetric(METRIC_EXECUTIONS, 1) + ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1) + ->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project + ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function + ->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) + ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) + ; + + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + } + + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + + if (!$isPrivilegedUser && !$isAppUser) { + $execution->setAttribute('logs', ''); + $execution->setAttribute('errors', ''); + } + + $headers = []; + foreach (($executionResponse['headers'] ?? []) as $key => $value) { + $headers[] = ['name' => $key, 'value' => $value]; + } + + $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); + $execution->setAttribute('responseHeaders', $headers); + + $acceptTypes = \explode(', ', $request->getHeader('accept')); + foreach ($acceptTypes as $acceptType) { + if (\str_starts_with($acceptType, 'application/json') || \str_starts_with($acceptType, 'application/*')) { + $response->setContentType(Response::CONTENT_TYPE_JSON); + break; + } elseif (\str_starts_with($acceptType, 'multipart/form-data') || \str_starts_with($acceptType, 'multipart/*')) { + $response->setContentType(Response::CONTENT_TYPE_MULTIPART); + break; + } + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($execution, Response::MODEL_EXECUTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php new file mode 100644 index 0000000000..7b9fd77a25 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php @@ -0,0 +1,116 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/functions/:functionId/executions/:executionId') + ->desc('Delete execution') + ->groups(['api', 'functions']) + ->label('scope', 'execution.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].executions.[executionId].delete') + ->label('audits.event', 'executions.delete') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'deleteExecution', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('executionId', '', new UID(), 'Execution ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('queueForEvents') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $execution = $dbForProject->getDocument('executions', $executionId); + if ($execution->isEmpty()) { + throw new Exception(Exception::EXECUTION_NOT_FOUND); + } + + if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { + throw new Exception(Exception::EXECUTION_NOT_FOUND); + } + $status = $execution->getAttribute('status'); + + if (!in_array($status, ['completed', 'failed', 'scheduled'])) { + throw new Exception(Exception::EXECUTION_IN_PROGRESS); + } + + if (!$dbForProject->deleteDocument('executions', $execution->getId())) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove execution from DB'); + } + + if ($status === 'scheduled') { + $schedule = $dbForPlatform->findOne('schedules', [ + Query::equal('resourceId', [$execution->getId()]), + Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]), + Query::equal('active', [true]), + ]); + + if (!$schedule->isEmpty()) { + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('active', false); + + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + } + } + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('executionId', $execution->getId()) + ->setPayload($response->output($execution, Response::MODEL_EXECUTION)); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php new file mode 100644 index 0000000000..da58d86414 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Get.php @@ -0,0 +1,88 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/executions/:executionId') + ->desc('Get execution') + ->groups(['api', 'functions']) + ->label('scope', 'execution.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'getExecution', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('executionId', '', new UID(), 'Execution ID.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $executionId, Response $response, Database $dbForProject) + { + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $execution = $dbForProject->getDocument('executions', $executionId); + + if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { + throw new Exception(Exception::EXECUTION_NOT_FOUND); + } + + if ($execution->isEmpty()) { + throw new Exception(Exception::EXECUTION_NOT_FOUND); + } + + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + if (!$isPrivilegedUser && !$isAppUser) { + $execution->setAttribute('logs', ''); + $execution->setAttribute('errors', ''); + } + + $response->dynamic($execution, Response::MODEL_EXECUTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php new file mode 100644 index 0000000000..597e830dd3 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php @@ -0,0 +1,135 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/executions') + ->desc('List executions') + ->groups(['api', 'functions']) + ->label('scope', 'execution.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listExecutions', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('queries', [], new Executions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Executions::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, array $queries, string $search, Response $response, Database $dbForProject) + { + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + + $isAPIKey = Auth::isAppUser(Authorization::getRoles()); + $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + + if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + // Set internal queries + $queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]); + $queries[] = Query::equal('resourceType', ['functions']); + + /** + * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries + */ + $cursor = \array_filter($queries, function ($query) { + return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); + }); + $cursor = reset($cursor); + if ($cursor) { + /** @var Query $cursor */ + + $validator = new Cursor(); + if (!$validator->isValid($cursor)) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription()); + } + + $executionId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('executions', $executionId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + + $results = $dbForProject->find('executions', $queries); + $total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT); + + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + if (!$isPrivilegedUser && !$isAppUser) { + $results = array_map(function ($execution) { + $execution->setAttribute('logs', ''); + $execution->setAttribute('errors', ''); + return $execution; + }, $results); + } + + $response->dynamic(new Document([ + 'executions' => $results, + 'total' => $total, + ]), Response::MODEL_EXECUTION_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 9d89b7edf0..0507c68068 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -2,12 +2,10 @@ namespace Appwrite\Platform\Modules\Functions\Http\Functions; -use Appwrite\Event\Build; use Appwrite\Event\Event; use Appwrite\Event\Validator\FunctionEvent; use Appwrite\Extend\Exception; use Appwrite\Functions\Validator\RuntimeSpecification; -use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -15,7 +13,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Task\Validator\Cron; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; -use Appwrite\Utopia\Response\Model\Rule; use Utopia\Abuse\Abuse; use Utopia\App; use Utopia\Config\Config; @@ -29,14 +26,12 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Roles; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -use Utopia\Swoole\Request; use Utopia\System\System; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; use Utopia\Validator\Range; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; -use Utopia\VCS\Adapter\Git\GitHub; class Create extends Base { @@ -90,30 +85,22 @@ class Create extends Base ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the function.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the function? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to function code in the linked repo.', true) - ->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true) - ->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true) - ->param('templateRootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.', true) - ->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) ->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new RuntimeSpecification( $plan, Config::getParam('runtime-specifications', []), App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT), App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT) ), 'Runtime specification for the function and builds.', true, ['plan']) - ->inject('request') ->inject('response') ->inject('dbForProject') ->inject('timelimit') ->inject('project') - ->inject('user') ->inject('queueForEvents') - ->inject('queueForBuilds') ->inject('dbForPlatform') - ->inject('gitHub') ->callback([$this, 'action']); } - public function action(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) + public function action(string $functionId, string $name, string $runtime, array $execute, array $events, string $schedule, int $timeout, bool $enabled, bool $logging, string $entrypoint, string $commands, array $scopes, string $installationId, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Response $response, Database $dbForProject, callable $timelimit, Document $project, Event $queueForEvents, Database $dbForPlatform) { // Temporary abuse check @@ -153,20 +140,6 @@ class Create extends Base throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $runtime . '" is not supported'); } - // build from template - $template = new Document([]); - if ( - !empty($templateRepository) - && !empty($templateOwner) - && !empty($templateRootDirectory) - && !empty($templateVersion) - ) { - $template->setAttribute('repositoryName', $templateRepository) - ->setAttribute('ownerName', $templateOwner) - ->setAttribute('rootDirectory', $templateRootDirectory) - ->setAttribute('version', $templateVersion); - } - $installation = $dbForPlatform->getDocument('installations', $installationId); if (!empty($installationId) && $installation->isEmpty()) { @@ -254,102 +227,6 @@ class Create extends Base $function = $dbForProject->updateDocument('functions', $function->getId(), $function); - if (!empty($providerRepositoryId)) { - // Deploy VCS - $this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, $template, $github); - } elseif (!$template->isEmpty()) { - // Deploy non-VCS from template - $deploymentId = ID::unique(); - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getInternalId(), - 'resourceType' => 'functions', - 'entrypoint' => $function->getAttribute('entrypoint', ''), - 'commands' => $function->getAttribute('commands', ''), - 'type' => 'manual', - 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint', '')]), - 'activate' => true, - ])); - - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($function) - ->setDeployment($deployment) - ->setTemplate($template); - } - - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - if (!empty($functionsDomain)) { - $routeSubdomain = ID::unique(); - $domain = "{$routeSubdomain}.{$functionsDomain}"; - $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); - - $rule = Authorization::skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain, - 'resourceType' => 'function', - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getInternalId(), - 'status' => 'verified', - 'certificateId' => '', - ])) - ); - - /** Trigger Webhook */ - $ruleModel = new Rule(); - $ruleCreate = - $queueForEvents - ->setClass(Event::WEBHOOK_CLASS_NAME) - ->setQueue(Event::WEBHOOK_QUEUE_NAME); - - $ruleCreate - ->setProject($project) - ->setEvent('rules.[ruleId].create') - ->setParam('ruleId', $rule->getId()) - ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) - ->trigger(); - - /** Trigger Functions */ - $ruleCreate - ->setClass(Event::FUNCTIONS_CLASS_NAME) - ->setQueue(Event::FUNCTIONS_QUEUE_NAME) - ->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()); $response diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php new file mode 100644 index 0000000000..92c99133b7 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php @@ -0,0 +1,93 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/functions/:functionId') + ->desc('Delete function') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].delete') + ->label('audits.event', 'function.delete') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'delete', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForDeletes') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + if (!$dbForProject->deleteDocument('functions', $function->getId())) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove function from DB'); + } + + // Inform scheduler to no longer run function + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('active', false); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $queueForDeletes + ->setType(DELETE_TYPE_DOCUMENT) + ->setDocument($function); + + $queueForEvents->setParam('functionId', $function->getId()); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php new file mode 100644 index 0000000000..f45aa0a108 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php @@ -0,0 +1,115 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/functions/:functionId/deployment') + ->httpAlias('/v1/functions/:functionId/deployments/:deploymentId') + ->desc('Update function\'s deployment') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('event', 'functions.[functionId].deployments.[deploymentId].update') + ->label('audits.event', 'deployment.update') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'updateFunctionDeployment', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('project') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $deploymentId, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($build->isEmpty()) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + if ($build->getAttribute('status') !== 'ready') { + throw new Exception(Exception::BUILD_NOT_READY); + } + + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ + 'deploymentInternalId' => $deployment->getInternalId(), + 'deployment' => $deployment->getId(), + ]))); + + // Inform scheduler if function is still active + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $this->listRules($project, [ + Query::equal("automation", ["function=" . $function->getId()]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); + + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response->dynamic($function, Response::MODEL_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Get.php new file mode 100644 index 0000000000..77c673b614 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Get.php @@ -0,0 +1,64 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId') + ->desc('Get function') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'get', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $response->dynamic($function, Response::MODEL_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php index 1c8731655d..9244b10c8c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php @@ -248,7 +248,7 @@ class Update extends Base // Redeploy logic if (!$isConnected && !empty($providerRepositoryId)) { - $this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github); + $this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github, true); } // Inform scheduler if function is still active diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php new file mode 100644 index 0000000000..dbaf3daa04 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Specifications/XList.php @@ -0,0 +1,76 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/specifications') + ->groups(['api', 'functions']) + ->desc('List available function runtime specifications') + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listSpecifications', + description: <<inject('response') + ->inject('plan') + ->callback([$this, 'action']); + } + + public function action(Response $response, array $plan) + { + $allRuntimeSpecs = Config::getParam('runtime-specifications', []); + + $runtimeSpecs = []; + foreach ($allRuntimeSpecs as $spec) { + $spec['enabled'] = true; + + if (array_key_exists('runtimeSpecifications', $plan)) { + $spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']); + } + + // Only add specs that are within the limits set by environment variables + if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) { + $runtimeSpecs[] = $spec; + } + } + + $response->dynamic(new Document([ + 'specifications' => $runtimeSpecs, + 'total' => count($runtimeSpecs) + ]), Response::MODEL_SPECIFICATION_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Templates/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Templates/Get.php new file mode 100644 index 0000000000..e624b26de6 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Templates/Get.php @@ -0,0 +1,69 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/templates/:templateId') + ->desc('Get function template') + ->label('scope', 'public') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'getTemplate', + description: <<param('templateId', '', new Text(128), 'Template ID.') + ->inject('response') + ->callback([$this, 'action']); + } + + public function action(string $templateId, Response $response) + { + $templates = Config::getParam('function-templates', []); + + $filtered = \array_filter($templates, function ($template) use ($templateId) { + return $template['id'] === $templateId; + }); + + $template = array_shift($filtered); + + if (empty($template)) { + throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND); + } + + $response->dynamic(new Document($template), Response::MODEL_TEMPLATE_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php new file mode 100644 index 0000000000..619ad6a315 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Templates/XList.php @@ -0,0 +1,79 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/templates') + ->desc('List templates') + ->label('scope', 'public') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listTemplates', + description: <<param('runtimes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('runtimes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of runtimes allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' runtimes are allowed.', true) + ->param('useCases', [], new ArrayList(new WhiteList(['dev-tools','starter','databases','ai','messaging','utilities']), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true) + ->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true) + ->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true) + ->inject('response') + ->callback([$this, 'action']); + } + + public function action(array $runtimes, array $usecases, int $limit, int $offset, Response $response) + { + $templates = Config::getParam('function-templates', []); + + if (!empty($runtimes)) { + $templates = \array_filter($templates, function ($template) use ($runtimes) { + return \count(\array_intersect($runtimes, \array_column($template['runtimes'], 'name'))) > 0; + }); + } + + if (!empty($usecases)) { + $templates = \array_filter($templates, function ($template) use ($usecases) { + return \count(\array_intersect($usecases, $template['useCases'])) > 0; + }); + } + + $responseTemplates = \array_slice($templates, $offset, $limit); + $response->dynamic(new Document([ + 'templates' => $responseTemplates, + 'total' => \count($responseTemplates), + ]), Response::MODEL_TEMPLATE_FUNCTION_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php new file mode 100644 index 0000000000..88221ccac0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Usage/Get.php @@ -0,0 +1,149 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/usage') + ->desc('Get function usage') + ->groups(['api', 'functions', 'usage']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'getUsage', + description: <<param('functionId', '', new UID(), 'Function ID.') + ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $range, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $periods = Config::getParam('usage', []); + $stats = $usage = []; + $days = $periods[$range]; + $metrics = [ + str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS), + str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), + str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS) + ]; + + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + foreach ($metrics as $metric) { + $result = $dbForProject->findOne('stats', [ + Query::equal('metric', [$metric]), + Query::equal('period', ['inf']) + ]); + + $stats[$metric]['total'] = $result['value'] ?? 0; + $limit = $days['limit']; + $period = $days['period']; + $results = $dbForProject->find('stats', [ + Query::equal('metric', [$metric]), + Query::equal('period', [$period]), + Query::limit($limit), + Query::orderDesc('time'), + ]); + $stats[$metric]['data'] = []; + foreach ($results as $result) { + $stats[$metric]['data'][$result->getAttribute('time')] = [ + 'value' => $result->getAttribute('value'), + ]; + } + } + }); + + $format = match ($days['period']) { + '1h' => 'Y-m-d\TH:00:00.000P', + '1d' => 'Y-m-d\T00:00:00.000P', + }; + + foreach ($metrics as $metric) { + $usage[$metric]['total'] = $stats[$metric]['total']; + $usage[$metric]['data'] = []; + $leap = time() - ($days['limit'] * $days['factor']); + while ($leap < time()) { + $leap += $days['factor']; + $formatDate = date($format, $leap); + $usage[$metric]['data'][] = [ + 'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0, + 'date' => $formatDate, + ]; + } + } + + $response->dynamic(new Document([ + 'range' => $range, + 'deploymentsTotal' => $usage[$metrics[0]]['total'], + 'deploymentsStorageTotal' => $usage[$metrics[1]]['total'], + 'buildsTotal' => $usage[$metrics[2]]['total'], + 'buildsStorageTotal' => $usage[$metrics[3]]['total'], + 'buildsTimeTotal' => $usage[$metrics[4]]['total'], + 'executionsTotal' => $usage[$metrics[5]]['total'], + 'executionsTimeTotal' => $usage[$metrics[6]]['total'], + 'deployments' => $usage[$metrics[0]]['data'], + 'deploymentsStorage' => $usage[$metrics[1]]['data'], + 'builds' => $usage[$metrics[2]]['data'], + 'buildsStorage' => $usage[$metrics[3]]['data'], + 'buildsTime' => $usage[$metrics[4]]['data'], + 'executions' => $usage[$metrics[5]]['data'], + 'executionsTime' => $usage[$metrics[6]]['data'], + 'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'], + 'buildsMbSeconds' => $usage[$metrics[7]]['data'], + 'executionsMbSeconds' => $usage[$metrics[8]]['data'], + 'executionsMbSecondsTotal' => $usage[$metrics[8]]['total'] + ]), Response::MODEL_USAGE_FUNCTION); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php new file mode 100644 index 0000000000..ca94b59a96 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Usage/XList.php @@ -0,0 +1,142 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/usage') + ->desc('Get functions usage') + ->groups(['api', 'functions', 'usage']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('sdk', new Method( + namespace: 'functions', + name: 'listUsage', + description: <<param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $range, Response $response, Database $dbForProject) + { + $periods = Config::getParam('usage', []); + $stats = $usage = []; + $days = $periods[$range]; + $metrics = [ + METRIC_FUNCTIONS, + METRIC_DEPLOYMENTS, + METRIC_DEPLOYMENTS_STORAGE, + METRIC_BUILDS, + METRIC_BUILDS_STORAGE, + METRIC_BUILDS_COMPUTE, + METRIC_EXECUTIONS, + METRIC_EXECUTIONS_COMPUTE, + METRIC_BUILDS_MB_SECONDS, + METRIC_EXECUTIONS_MB_SECONDS, + ]; + + Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { + foreach ($metrics as $metric) { + $result = $dbForProject->findOne('stats', [ + Query::equal('metric', [$metric]), + Query::equal('period', ['inf']) + ]); + + $stats[$metric]['total'] = $result['value'] ?? 0; + $limit = $days['limit']; + $period = $days['period']; + $results = $dbForProject->find('stats', [ + Query::equal('metric', [$metric]), + Query::equal('period', [$period]), + Query::limit($limit), + Query::orderDesc('time'), + ]); + $stats[$metric]['data'] = []; + foreach ($results as $result) { + $stats[$metric]['data'][$result->getAttribute('time')] = [ + 'value' => $result->getAttribute('value'), + ]; + } + } + }); + + $format = match ($days['period']) { + '1h' => 'Y-m-d\TH:00:00.000P', + '1d' => 'Y-m-d\T00:00:00.000P', + }; + + foreach ($metrics as $metric) { + $usage[$metric]['total'] = $stats[$metric]['total']; + $usage[$metric]['data'] = []; + $leap = time() - ($days['limit'] * $days['factor']); + while ($leap < time()) { + $leap += $days['factor']; + $formatDate = date($format, $leap); + $usage[$metric]['data'][] = [ + 'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0, + 'date' => $formatDate, + ]; + } + } + $response->dynamic(new Document([ + 'range' => $range, + 'functionsTotal' => $usage[$metrics[0]]['total'], + 'deploymentsTotal' => $usage[$metrics[1]]['total'], + 'deploymentsStorageTotal' => $usage[$metrics[2]]['total'], + 'buildsTotal' => $usage[$metrics[3]]['total'], + 'buildsStorageTotal' => $usage[$metrics[4]]['total'], + 'buildsTimeTotal' => $usage[$metrics[5]]['total'], + 'executionsTotal' => $usage[$metrics[6]]['total'], + 'executionsTimeTotal' => $usage[$metrics[7]]['total'], + 'functions' => $usage[$metrics[0]]['data'], + 'deployments' => $usage[$metrics[1]]['data'], + 'deploymentsStorage' => $usage[$metrics[2]]['data'], + 'builds' => $usage[$metrics[3]]['data'], + 'buildsStorage' => $usage[$metrics[4]]['data'], + 'buildsTime' => $usage[$metrics[5]]['data'], + 'executions' => $usage[$metrics[6]]['data'], + 'executionsTime' => $usage[$metrics[7]]['data'], + 'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'], + 'buildsMbSeconds' => $usage[$metrics[8]]['data'], + 'executionsMbSeconds' => $usage[$metrics[9]]['data'], + 'executionsMbSecondsTotal' => $usage[$metrics[9]]['total'], + ]), Response::MODEL_USAGE_FUNCTIONS); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php new file mode 100644 index 0000000000..ba48a5c4bc --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php @@ -0,0 +1,115 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/functions/:functionId/variables') + ->desc('Create variable') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('audits.event', 'variable.create') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'createVariable', + description: <<param('functionId', '', new UID(), 'Function unique ID.', false) + ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) + ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) + ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $variableId = ID::unique(); + + $variable = new Document([ + '$id' => $variableId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceInternalId' => $function->getInternalId(), + 'resourceId' => $function->getId(), + 'resourceType' => 'function', + 'key' => $key, + 'value' => $value, + 'secret' => $secret, + 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']), + ]); + + try { + $variable = $dbForProject->createDocument('variables', $variable); + } catch (DuplicateException $th) { + throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); + } + + $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + + // Inform scheduler to pull the latest changes + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($variable, Response::MODEL_VARIABLE); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php new file mode 100644 index 0000000000..650eaf4010 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php @@ -0,0 +1,93 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) + ->setHttpPath('/v1/functions/:functionId/variables/:variableId') + ->desc('Delete variable') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('audits.event', 'variable.delete') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'deleteVariable', + description: <<param('functionId', '', new UID(), 'Function unique ID.', false) + ->param('variableId', '', new UID(), 'Variable unique ID.', false) + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $variable = $dbForProject->getDocument('variables', $variableId); + if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { + throw new Exception(Exception::VARIABLE_NOT_FOUND); + } + + if ($variable === false || $variable->isEmpty()) { + throw new Exception(Exception::VARIABLE_NOT_FOUND); + } + + $dbForProject->deleteDocument('variables', $variable->getId()); + + $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + + // Inform scheduler to pull the latest changes + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $response->noContent(); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Get.php new file mode 100644 index 0000000000..eeb5c51f33 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Get.php @@ -0,0 +1,82 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/variables/:variableId') + ->desc('Get variable') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label( + 'sdk', + new Method( + namespace: 'functions', + name: 'getVariable', + description: <<param('functionId', '', new UID(), 'Function unique ID.', false) + ->param('variableId', '', new UID(), 'Variable unique ID.', false) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $variableId, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $variable = $dbForProject->getDocument('variables', $variableId); + if ( + $variable === false || + $variable->isEmpty() || + $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || + $variable->getAttribute('resourceType') !== 'function' + ) { + throw new Exception(Exception::VARIABLE_NOT_FOUND); + } + + if ($variable === false || $variable->isEmpty()) { + throw new Exception(Exception::VARIABLE_NOT_FOUND); + } + + $response->dynamic($variable, Response::MODEL_VARIABLE); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php new file mode 100644 index 0000000000..8911dc8abb --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php @@ -0,0 +1,111 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT) + ->setHttpPath('/v1/functions/:functionId/variables/:variableId') + ->desc('Update variable') + ->groups(['api', 'functions']) + ->label('scope', 'functions.write') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label('audits.event', 'variable.update') + ->label('audits.resource', 'function/{request.functionId}') + ->label('sdk', new Method( + namespace: 'functions', + name: 'updateVariable', + description: <<param('functionId', '', new UID(), 'Function unique ID.', false) + ->param('variableId', '', new UID(), 'Variable unique ID.', false) + ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false) + ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true) + ->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true) + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $functionId, string $variableId, string $key, ?string $value, ?bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $variable = $dbForProject->getDocument('variables', $variableId); + if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { + throw new Exception(Exception::VARIABLE_NOT_FOUND); + } + + if ($variable === false || $variable->isEmpty()) { + throw new Exception(Exception::VARIABLE_NOT_FOUND); + } + + if ($variable->getAttribute('secret') === true && $secret === false) { + throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET); + } + + $variable + ->setAttribute('key', $key) + ->setAttribute('value', $value ?? $variable->getAttribute('value')) + ->setAttribute('secret', $secret ?? $variable->getAttribute('secret')) + ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); + + try { + $dbForProject->updateDocument('variables', $variable->getId(), $variable); + } catch (DuplicateException $th) { + throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); + } + + $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + + // Inform scheduler to pull the latest changes + $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('schedule', $function->getAttribute('schedule')) + ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); + Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + + $response->dynamic($variable, Response::MODEL_VARIABLE); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php new file mode 100644 index 0000000000..000e83a0c9 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php @@ -0,0 +1,71 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/functions/:functionId/variables') + ->desc('List variables') + ->groups(['api', 'functions']) + ->label('scope', 'functions.read') + ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) + ->label( + 'sdk', + new Method( + namespace: 'functions', + name: 'listVariables', + description: <<param('functionId', '', new UID(), 'Function unique ID.', false) + ->inject('response') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $functionId, Response $response, Database $dbForProject) + { + $function = $dbForProject->getDocument('functions', $functionId); + + if ($function->isEmpty()) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } + + $response->dynamic(new Document([ + 'variables' => $function->getAttribute('vars', []), + 'total' => \count($function->getAttribute('vars', [])), + ]), Response::MODEL_VARIABLE_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Services/Http.php b/src/Appwrite/Platform/Modules/Functions/Services/Http.php index 6c74182776..cf47574472 100644 --- a/src/Appwrite/Platform/Modules/Functions/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Functions/Services/Http.php @@ -3,10 +3,35 @@ namespace Appwrite\Platform\Modules\Functions\Services; use Appwrite\Platform\Modules\Functions\Http\Deployments\Create as CreateDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Delete as DeleteDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Download\Get as DownloadDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Duplicate\Create as CreateDuplicateDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Get as GetDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Status\Update as UpdateDeploymentStatus; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Template\Create as CreateTemplateDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\Vcs\Create as CreateVcsDeployment; +use Appwrite\Platform\Modules\Functions\Http\Deployments\XList as ListDeployments; +use Appwrite\Platform\Modules\Functions\Http\Executions\Create as CreateExecution; +use Appwrite\Platform\Modules\Functions\Http\Executions\Delete as DeleteExecution; +use Appwrite\Platform\Modules\Functions\Http\Executions\Get as GetExecution; +use Appwrite\Platform\Modules\Functions\Http\Executions\XList as ListExecutions; use Appwrite\Platform\Modules\Functions\Http\Functions\Create as CreateFunction; +use Appwrite\Platform\Modules\Functions\Http\Functions\Delete as DeleteFunction; +use Appwrite\Platform\Modules\Functions\Http\Functions\Deployment\Update as UpdateFunctionDeployment; +use Appwrite\Platform\Modules\Functions\Http\Functions\Get as GetFunction; use Appwrite\Platform\Modules\Functions\Http\Functions\Update as UpdateFunction; use Appwrite\Platform\Modules\Functions\Http\Functions\XList as ListFunctions; use Appwrite\Platform\Modules\Functions\Http\Runtimes\XList as ListRuntimes; +use Appwrite\Platform\Modules\Functions\Http\Specifications\XList as ListSpecifications; +use Appwrite\Platform\Modules\Functions\Http\Templates\Get as GetTemplate; +use Appwrite\Platform\Modules\Functions\Http\Templates\XList as ListTemplates; +use Appwrite\Platform\Modules\Functions\Http\Usage\Get as GetUsage; +use Appwrite\Platform\Modules\Functions\Http\Usage\XList as ListUsage; +use Appwrite\Platform\Modules\Functions\Http\Variables\Create as CreateVariable; +use Appwrite\Platform\Modules\Functions\Http\Variables\Delete as DeleteVariable; +use Appwrite\Platform\Modules\Functions\Http\Variables\Get as GetVariable; +use Appwrite\Platform\Modules\Functions\Http\Variables\Update as UpdateVariable; +use Appwrite\Platform\Modules\Functions\Http\Variables\XList as ListVariables; use Utopia\Platform\Service; class Http extends Service @@ -14,10 +39,51 @@ class Http extends Service public function __construct() { $this->type = Service::TYPE_HTTP; + + // Functions $this->addAction(CreateFunction::getName(), new CreateFunction()); + $this->addAction(GetFunction::getName(), new GetFunction()); $this->addAction(UpdateFunction::getName(), new UpdateFunction()); $this->addAction(ListFunctions::getName(), new ListFunctions()); + $this->addAction(DeleteFunction::getName(), new DeleteFunction()); + + // Runtimes $this->addAction(ListRuntimes::getName(), new ListRuntimes()); + + // Specifications + $this->addAction(ListSpecifications::getName(), new ListSpecifications()); + + // Deployments $this->addAction(CreateDeployment::getName(), new CreateDeployment()); + $this->addAction(GetDeployment::getName(), new GetDeployment()); + $this->addAction(UpdateFunctionDeployment::getName(), new UpdateFunctionDeployment()); + $this->addAction(ListDeployments::getName(), new ListDeployments()); + $this->addAction(DeleteDeployment::getName(), new DeleteDeployment()); + $this->addAction(CreateTemplateDeployment::getName(), new CreateTemplateDeployment()); + $this->addAction(CreateVcsDeployment::getName(), new CreateVcsDeployment()); + $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); + $this->addAction(CreateDuplicateDeployment::getName(), new CreateDuplicateDeployment()); + $this->addAction(UpdateDeploymentStatus::getName(), new UpdateDeploymentStatus()); + + // Executions + $this->addAction(CreateExecution::getName(), new CreateExecution()); + $this->addAction(GetExecution::getName(), new GetExecution()); + $this->addAction(ListExecutions::getName(), new ListExecutions()); + $this->addAction(DeleteExecution::getName(), new DeleteExecution()); + + // Usage + $this->addAction(GetUsage::getName(), new GetUsage()); + $this->addAction(ListUsage::getName(), new ListUsage()); + + // Variables + $this->addAction(CreateVariable::getName(), new CreateVariable()); + $this->addAction(GetVariable::getName(), new GetVariable()); + $this->addAction(ListVariables::getName(), new ListVariables()); + $this->addAction(UpdateVariable::getName(), new UpdateVariable()); + $this->addAction(DeleteVariable::getName(), new DeleteVariable()); + + // Templates + $this->addAction(GetTemplate::getName(), new GetTemplate()); + $this->addAction(ListTemplates::getName(), new ListTemplates()); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 41ab2fe02a..53556f60ba 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -5,8 +5,10 @@ namespace Appwrite\Platform\Modules\Functions\Workers; use Ahc\Jwt\JWT; use Appwrite\Event\Event; use Appwrite\Event\Func; -use Appwrite\Event\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Messaging\Adapter\Realtime; +use Appwrite\Permission; +use Appwrite\Role; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; use Exception; @@ -24,9 +26,11 @@ use Utopia\Database\Exception\Structure; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Fetch\Client as FetchClient; use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; +use Utopia\Storage\Compression\Compression; use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\System\System; @@ -51,12 +55,16 @@ class Builds extends Action ->inject('dbForPlatform') ->inject('queueForEvents') ->inject('queueForFunctions') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('cache') ->inject('dbForProject') ->inject('deviceForFunctions') + ->inject('deviceForSites') + ->inject('isResourceBlocked') + ->inject('deviceForFiles') ->inject('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)); + ->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Device $deviceForSites, callable $isResourceBlocked, Device $deviceForFiles, Log $log) => + $this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $deviceForSites, $isResourceBlocked, $deviceForFiles, $log)); } /** @@ -65,15 +73,17 @@ class Builds extends Action * @param Database $dbForPlatform * @param Event $queueForEvents * @param Func $queueForFunctions - * @param Usage $queueForUsage + * @param StatsUsage $queueForStatsUsage * @param Cache $cache * @param Database $dbForProject * @param Device $deviceForFunctions + * @param Device $deviceForSites + * @param Device $deviceForFiles * @param Log $log * @return void * @throws \Utopia\Database\Exception */ - public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void + public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Device $deviceForSites, callable $isResourceBlocked, Device $deviceForFiles, Log $log): void { $payload = $message->getPayload() ?? []; @@ -94,7 +104,7 @@ class Builds extends Action case BUILD_TYPE_RETRY: Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); - $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $log); + $this->buildDeployment($deviceForFunctions, $deviceForSites, $deviceForFiles, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log); break; default: @@ -104,9 +114,11 @@ class Builds extends Action /** * @param Device $deviceForFunctions + * @param Device $deviceForSites + * @param Device $deviceForFiles * @param Func $queueForFunctions * @param Event $queueForEvents - * @param Usage $queueForUsage + * @param StatsUsage $queueForStatsUsage * @param Database $dbForPlatform * @param Database $dbForProject * @param GitHub $github @@ -120,7 +132,7 @@ class Builds extends Action * * @throws Exception */ - protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, Log $log): void + protected function buildDeployment(Device $deviceForFunctions, Device $deviceForSites, Device $deviceForFiles, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void { $resourceKey = match($resource->getCollection()) { 'functions' => 'functionId', @@ -128,30 +140,43 @@ class Builds extends Action default => throw new \Exception('Invalid resource type') }; + $device = match ($resource->getCollection()) { + 'sites' => $deviceForSites, + 'functions' => $deviceForFunctions, + }; + $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); $log->addTag($resourceKey, $resource->getId()); $resource = $dbForProject->getDocument($resource->getCollection(), $resource->getId()); if ($resource->isEmpty()) { - throw new \Exception('Function not found', 404); + throw new \Exception('Resource not found'); + } + + if ($isResourceBlocked($project, $resourceKey === 'functions' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES, $resource->getId())) { + throw new \Exception('Resource is blocked'); } $log->addTag('deploymentId', $deployment->getId()); $deployment = $dbForProject->getDocument('deployments', $deployment->getId()); if ($deployment->isEmpty()) { - throw new \Exception('Deployment not found', 404); + throw new \Exception('Deployment not found'); } - // todo: figure out a better way, entrypoint is not required for sites if ($resource->getCollection() === 'functions' && 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".', 500); + 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".'); } $version = $this->getVersion($resource); $runtime = $this->getRuntime($resource, $version); - $spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specifications', APP_COMPUTE_SPECIFICATION_DEFAULT)]; + + $spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; + + if ($resource->getCollection() === 'functions' && \is_null($runtime)) { + throw new \Exception('Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported'); + } // Realtime preparation $allEvents = Event::generateEvents("{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update", [ @@ -251,8 +276,8 @@ class Builds extends Action $tarParamDirectory = \escapeshellarg($tmpTemplateDirectory . (empty($templateRootDirectory) ? '' : '/' . $templateRootDirectory)); Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax - $source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - $result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions); + $source = $device->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); + $result = $localDevice->transfer($tmpPathFile, $source, $device); if (!$result) { throw new \Exception("Unable to move file"); @@ -260,7 +285,7 @@ class Builds extends Action Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $stdout, $stderr); - $directorySize = $deviceForFunctions->getFileSize($source); + $directorySize = $device->getFileSize($source); $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source)); $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize)); } @@ -408,8 +433,8 @@ class Builds extends Action $tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory); Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax - $source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - $result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions); + $source = $device->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); + $result = $localDevice->transfer($tmpPathFile, $source, $device); if (!$result) { throw new \Exception("Unable to move file"); @@ -419,7 +444,7 @@ class Builds extends Action $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source)); - $directorySize = $deviceForFunctions->getFileSize($source); + $directorySize = $device->getFileSize($source); $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize)); $this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform); @@ -502,8 +527,6 @@ class Builds extends Action 'APPWRITE_VERSION' => APP_VERSION_STABLE, 'APPWRITE_REGION' => $project->getAttribute('region'), 'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''), - 'APPWRITE_COMPUTE_CPUS' => $cpus, - 'APPWRITE_COMPUTE_MEMORY' => $memory, 'APPWRITE_VCS_REPOSITORY_ID' => $deployment->getAttribute('providerRepositoryId', ''), 'APPWRITE_VCS_REPOSITORY_NAME' => $deployment->getAttribute('providerRepositoryName', ''), 'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment->getAttribute('providerRepositoryOwner', ''), @@ -530,6 +553,8 @@ class Builds extends Action 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', + 'APPWRITE_FUNCTION_CPUS' => $cpus, + 'APPWRITE_FUNCTION_MEMORY' => $memory, ]; break; case 'sites': @@ -543,6 +568,8 @@ class Builds extends Action 'APPWRITE_SITE_PROJECT_ID' => $project->getId(), 'APPWRITE_SITE_RUNTIME_NAME' => $runtime['name'] ?? '', 'APPWRITE_SITE_RUNTIME_VERSION' => $runtime['version'] ?? '', + 'APPWRITE_SITE_CPUS' => $cpus, + 'APPWRITE_SITE_MEMORY' => $memory, ]; break; } @@ -578,7 +605,7 @@ class Builds extends Action memory: $memory, timeout: $timeout, remove: true, - entrypoint: $deployment->getAttribute('entrypoint', 'package.json'), // TODO: change this later so that sites don't need to have an entrypoint + entrypoint: $deployment->getAttribute('entrypoint', ''), destination: APP_STORAGE_BUILDS . "/app-{$project->getId()}", variables: $vars, command: $command, @@ -604,7 +631,7 @@ class Builds extends Action $build = $dbForProject->getDocument('builds', $build->getId()); if ($build->isEmpty()) { - throw new \Exception('Build not found', 404); + throw new \Exception('Build not found'); } if ($build->getAttribute('status') === 'canceled') { @@ -700,6 +727,110 @@ class Builds extends Action Console::success("Build id: $buildId created"); + if ($resource->getCollection() === 'sites') { + try { + $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ + Query::equal("projectInternalId", [$project->getInternalId()]), + Query::equal("type", ["deployment"]), + Query::equal("value", [$deployment->getId()]) + ])); + + if ($rule->isEmpty()) { + throw new \Exception("Rule for build not found"); + } + + $client = new FetchClient(); + $client->setTimeout(\intval($resource->getAttribute('timeout', '15'))); + $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); + + $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + + $configs = [ + 'screenshotLight' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=light', + 'theme' => 'light' + ], + 'screenshotDark' => [ + 'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ], + 'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=dark', + 'theme' => 'dark' + ], + ]; + + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); + $apiKey = $jwtObj->encode([ + 'hostnameOverride' => true, + 'bannerDisabled' => true, + 'projectCheckDisabled' => true + ]); + + // TODO: @Meldiron if becomes too slow, do concurrently + foreach ($configs as $key => $config) { + $config['headers'] = \array_merge($config['headers'] ?? [], [ + 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey + ]); + + $response = $client->fetch( + url: 'http://appwrite-browser:3000/v1/screenshots', + method: 'POST', + body: $config + ); + + if ($response->getStatusCode() >= 400) { + throw new \Exception($response->getBody()); + } + + $screenshot = $response->getBody(); + + $fileId = ID::unique(); + $fileName = $fileId . '.png'; + $path = $deviceForFiles->getPath($fileName); + $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root + $success = $deviceForFiles->write($path, $screenshot, "image/png"); + + if (!$success) { + throw new \Exception("Screenshot failed to save"); + } + + $teamId = $project->getAttribute('teamId', ''); + $file = new Document([ + '$id' => $fileId, + '$permissions' => [ + Permission::read(Role::team(ID::custom($teamId))), + ], + 'bucketId' => $bucket->getId(), + 'bucketInternalId' => $bucket->getInternalId(), + 'name' => $fileName, + 'path' => $path, + 'signature' => $deviceForFiles->getFileHash($path), + 'mimeType' => $deviceForFiles->getFileMimeType($path), + 'sizeOriginal' => \strlen($screenshot), + 'sizeActual' => $deviceForFiles->getFileSize($path), + 'algorithm' => Compression::GZIP, + 'comment' => '', + 'chunksTotal' => 1, + 'chunksUploaded' => 1, + 'openSSLVersion' => null, + 'openSSLCipher' => null, + 'openSSLTag' => null, + 'openSSLIV' => null, + 'search' => implode(' ', [$fileId, $fileName]), + 'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($path)], + ]); + $file = Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getInternalId(), $file)); + + $deployment->setAttribute($key, $fileId); + } + + $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + } catch (\Throwable $th) { + Console::warning("Screenshot failed to generate:"); + Console::warning($th->getMessage()); + Console::warning($th->getTraceAsString()); + } + } + /** Set auto deploy */ if ($deployment->getAttribute('activate') === true) { $resource->setAttribute('deploymentInternalId', $deployment->getInternalId()); @@ -708,14 +839,51 @@ class Builds extends Action case 'functions': $resource->setAttribute('deployment', $deployment->getId()); $resource = $dbForProject->updateDocument('functions', $resource->getId(), $resource); + + $this->listRules($project, [ + Query::equal("automation", ["function=" . $resource->getId()]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); break; case 'sites': $resource->setAttribute('deploymentId', $deployment->getId()); $resource = $dbForProject->updateDocument('sites', $resource->getId(), $resource); + + $this->listRules($project, [ + Query::equal("automation", ["site=" . $resource->getId()]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); + + // VCS branch + $branchName = $deployment->getAttribute('providerBranch'); + if (!empty($branchName)) { + $this->listRules($project, [ + Query::equal("automation", ["branch=" . $branchName]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); + } + + // VCS commit + $commitHash = $deployment->getAttribute('providerCommitHash', ''); + if (!empty($commitHash)) { + $this->listRules($project, [ + Query::equal("automation", ["commit=" . $commitHash]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); + } break; } } + if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') { Console::info('Build has been canceled'); return; @@ -772,12 +940,12 @@ class Builds extends Action resource:$resource, build: $build, project: $project, - queue: $queueForUsage + queue: $queueForStatsUsage ); } } - protected function sendUsage(Document $resource, Document $build, Document $project, Usage $queue): void + protected function sendUsage(Document $resource, Document $build, Document $project, StatsUsage $queue): void { $key = match($resource->getCollection()) { 'functions' => 'functionInternalId', @@ -813,8 +981,8 @@ class Builds extends Action $queue ->addMetric(METRIC_BUILDS_SUCCESS, 1) // per project ->addMetric(METRIC_BUILDS_COMPUTE_SUCCESS, (int)$build->getAttribute('duration', 0) * 1000) - ->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsSuccess']), 1) // per function - ->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsComputeSuccess']), (int)$build->getAttribute('duration', 0) * 1000); + ->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_SUCCESS), 1) // per function + ->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE_SUCCESS), (int)$build->getAttribute('duration', 0) * 1000); break; case 'failed': $queue @@ -830,10 +998,10 @@ class Builds extends Action ->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0)) ->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000) ->addMetric(METRIC_BUILDS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) - ->addMetric(str_replace($key, $resource->getInternalId(), $metrics['builds']), 1) // per function - ->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsStorage']), $build->getAttribute('size', 0)) - ->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsCompute']), (int)$build->getAttribute('duration', 0) * 1000) - ->addMetric(str_replace($key, $resource->getInternalId(), $metrics['buildsMbSeconds']), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) + ->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS), 1) // per function + ->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), $build->getAttribute('size', 0)) + ->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000) + ->addMetric(str_replace($key, $resource->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $build->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) ->setProject($project) ->trigger(); } @@ -869,22 +1037,27 @@ class Builds extends Action } elseif ($resource->getCollection() === 'sites') { $commands = []; - $commands[] = $deployment->getAttribute('installCommand', ''); - $commands[] = $deployment->getAttribute('buildCommand', ''); - $frameworks = Config::getParam('frameworks', []); $framework = $frameworks[$resource->getAttribute('framework', '')] ?? null; + $envCommand = ''; + $bundleCommand = ''; + if (!is_null($framework)) { $adapter = ($framework['adapters'] ?? [])[$resource->getAttribute('adapter', '')] ?? null; - if (!is_null($adapter) && isset($adapter['bundleCommand'])) { - $commands[] = $adapter['bundleCommand']; - } if (!is_null($adapter) && isset($adapter['envCommand'])) { - $commands[] = $adapter['envCommand']; + $envCommand = $adapter['envCommand']; + } + if (!is_null($adapter) && isset($adapter['bundleCommand'])) { + $bundleCommand = $adapter['bundleCommand']; } } + $commands[] = $envCommand; + $commands[] = $deployment->getAttribute('installCommand', ''); + $commands[] = $deployment->getAttribute('buildCommand', ''); + $commands[] = $bundleCommand; + $commands = array_filter($commands, fn ($command) => !empty($command)); return implode(' && ', $commands); @@ -977,8 +1150,8 @@ class Builds extends Action $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ Query::equal("projectInternalId", [$project->getInternalId()]), - Query::equal("resourceType", ["deployment"]), - Query::equal("resourceInternalId", [$deployment->getInternalId()]) + Query::equal("type", ["deployment"]), + Query::equal("value", [$deployment->getId()]) ])); $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; @@ -1003,4 +1176,38 @@ class Builds extends Action } } } + + protected function listRules(Document $project, array $queries, Database $database, callable $callback): void + { + $limit = 100; + $cursor = null; + + do { + $queries = \array_merge([ + Query::limit($limit), + Query::equal("projectInternalId", [$project->getInternalId()]) + ], $queries); + + if ($cursor !== null) { + $queries[] = Query::cursorAfter($cursor); + } + + $results = $database->find('rules', $queries); + + $total = \count($results); + if ($total > 0) { + $cursor = $results[$total - 1]; + } + + if ($total < $limit) { + $cursor = null; + } + + foreach ($results as $document) { + if (is_callable($callback)) { + $callback($document); + } + } + } while (!\is_null($cursor)); + } } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php new file mode 100644 index 0000000000..36e2bcdf12 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -0,0 +1,152 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/proxy/rules/api') + ->groups(['api', 'proxy']) + ->desc('Create API rule') + ->label('scope', 'rules.write') + ->label('event', 'rules.[ruleId].create') + ->label('audits.event', 'rule.create') + ->label('audits.resource', 'rule/{response.$id}') + ->label('sdk', new Method( + namespace: 'proxy', + name: 'createAPIRule', + description: <<label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}, url:{url}') + ->label('abuse-time', 60) + ->param('domain', null, new ValidatorDomain(), 'Domain name.') + ->inject('response') + ->inject('project') + ->inject('queueForCertificates') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform) + { + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + if (\in_array($domain, $deniedDomains)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + try { + $domain = new Domain($domain); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); + } + + // Apex domain prevention due to CNAME limitations + if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { + if ($domain->get() === $domain->getRegisterable()) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); + } + } + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); + + $status = 'created'; + if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { + $status = 'verified'; + } + if ($status === 'created') { + $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validator = new CNAME($target->get()); + if ($validator->isValid($domain->get())) { + $status = 'verifying'; + } + } + + $rule = new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain->get(), + 'status' => $status, + 'type' => 'api', + 'value' => '', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain->get()]), + ]); + + try { + $rule = $dbForPlatform->createDocument('rules', $rule); + } catch (Duplicate $e) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + + if ($rule->getAttribute('status', '') === 'verifying') { + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $rule->getAttribute('domain') + ])) + ->trigger(); + } + + $queueForEvents->setParam('ruleId', $rule->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($rule, Response::MODEL_PROXY_RULE); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php new file mode 100644 index 0000000000..bcc3b91e8f --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -0,0 +1,160 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/proxy/rules/function') + ->groups(['api', 'proxy']) + ->desc('Create function rule') + ->label('scope', 'rules.write') + ->label('event', 'rules.[ruleId].create') + ->label('audits.event', 'rule.create') + ->label('audits.resource', 'rule/{response.$id}') + ->label('sdk', new Method( + namespace: 'proxy', + name: 'createFunctionRule', + description: <<label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}, url:{url}') + ->label('abuse-time', 60) + ->param('domain', null, new ValidatorDomain(), 'Domain name.') + ->param('functionId', '', new UID(), 'ID of function to be executed.') + ->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true) + ->inject('response') + ->inject('project') + ->inject('queueForCertificates') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) + { + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + if (\in_array($domain, $deniedDomains)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + try { + $domain = new Domain($domain); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); + } + + // Apex domain prevention due to CNAME limitations + if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { + if ($domain->get() === $domain->getRegisterable()) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); + } + } + + $function = $dbForProject->getDocument('functions', $functionId); + if ($function->isEmpty()) { + throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); + } + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); + + $status = 'created'; + if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { + $status = 'verified'; + } + if ($status === 'created') { + $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validator = new CNAME($target->get()); + if ($validator->isValid($domain->get())) { + $status = 'verifying'; + } + } + + $rule = new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain->get(), + 'status' => $status, + 'type' => 'deployment', + 'value' => $function->getAttribute('deployment', ''), + 'certificateId' => '', + 'automation' => 'function=' . $function->getId(), + 'automation' => !empty($branch) ? ('branch=' . $branch) : ('function=' . $function->getId()), + 'search' => implode(' ', [$ruleId, $domain->get(), $branch]), + ]); + + try { + $rule = $dbForPlatform->createDocument('rules', $rule); + } catch (Duplicate $e) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + + if ($rule->getAttribute('status', '') === 'verifying') { + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $rule->getAttribute('domain') + ])) + ->trigger(); + } + + $queueForEvents->setParam('ruleId', $rule->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($rule, Response::MODEL_PROXY_RULE); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php new file mode 100644 index 0000000000..ac23cca168 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -0,0 +1,155 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/proxy/rules/redirect') + ->groups(['api', 'proxy']) + ->desc('Create Redirect rule') + ->label('scope', 'rules.write') + ->label('event', 'rules.[ruleId].create') + ->label('audits.event', 'rule.create') + ->label('audits.resource', 'rule/{response.$id}') + ->label('sdk', new Method( + namespace: 'proxy', + name: 'createRedirectRule', + description: <<label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}, url:{url}') + ->label('abuse-time', 60) + ->param('domain', null, new ValidatorDomain(), 'Domain name.') + ->param('target', null, new ValidatorDomain(), 'Target domain (hostname) of redirection') + ->inject('response') + ->inject('project') + ->inject('queueForCertificates') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->callback([$this, 'action']); + } + + public function action(string $domain, string $target, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform) + { + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + if (\in_array($domain, $deniedDomains)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + try { + $domain = new Domain($domain); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); + } + + try { + $target = new Domain($target); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Target may not start with http:// or https://.'); + } + + // Apex domain prevention due to CNAME limitations + if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { + if ($domain->get() === $domain->getRegisterable()) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); + } + } + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); + + $status = 'created'; + if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { + $status = 'verified'; + } + if ($status === 'created') { + $dnsTarget = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validator = new CNAME($dnsTarget->get()); + if ($validator->isValid($domain->get())) { + $status = 'verifying'; + } + } + + $rule = new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain->get(), + 'status' => $status, + 'type' => 'redirect', + 'value' => $target->get(), + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain->get()]), + ]); + + try { + $rule = $dbForPlatform->createDocument('rules', $rule); + } catch (Duplicate $e) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + + if ($rule->getAttribute('status', '') === 'verifying') { + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $rule->getAttribute('domain') + ])) + ->trigger(); + } + + $queueForEvents->setParam('ruleId', $rule->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($rule, Response::MODEL_PROXY_RULE); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php new file mode 100644 index 0000000000..b6e88be00c --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -0,0 +1,159 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/proxy/rules/site') + ->groups(['api', 'proxy']) + ->desc('Create site rule') + ->label('scope', 'rules.write') + ->label('event', 'rules.[ruleId].create') + ->label('audits.event', 'rule.create') + ->label('audits.resource', 'rule/{response.$id}') + ->label('sdk', new Method( + namespace: 'proxy', + name: 'createSiteRule', + description: <<label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}, url:{url}') + ->label('abuse-time', 60) + ->param('domain', null, new ValidatorDomain(), 'Domain name.') + ->param('siteId', '', new UID(), 'ID of site to be executed.') + ->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true) + ->inject('response') + ->inject('project') + ->inject('queueForCertificates') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) + { + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + if (\in_array($domain, $deniedDomains)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + try { + $domain = new Domain($domain); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); + } + + // Apex domain prevention due to CNAME limitations + if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) { + if ($domain->get() === $domain->getRegisterable()) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.'); + } + } + + $site = $dbForProject->getDocument('sites', $siteId); + if ($site->isEmpty()) { + throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); + } + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); + + $status = 'created'; + if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { + $status = 'verified'; + } + if ($status === 'created') { + $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validator = new CNAME($target->get()); + if ($validator->isValid($domain->get())) { + $status = 'verifying'; + } + } + + $rule = new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain->get(), + 'status' => $status, + 'type' => 'deployment', + 'value' => $site->getAttribute('deploymentId', ''), + 'certificateId' => '', + 'automation' => !empty($branch) ? ('branch=' . $branch) : ('site=' . $site->getId()), + 'search' => implode(' ', [$ruleId, $domain->get(), $branch]), + ]); + + try { + $rule = $dbForPlatform->createDocument('rules', $rule); + } catch (Duplicate $e) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + + if ($rule->getAttribute('status', '') === 'verifying') { + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $rule->getAttribute('domain') + ])) + ->trigger(); + } + + $queueForEvents->setParam('ruleId', $rule->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($rule, Response::MODEL_PROXY_RULE); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Module.php b/src/Appwrite/Platform/Modules/Proxy/Module.php new file mode 100644 index 0000000000..cd8f6f86dc --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php new file mode 100644 index 0000000000..c5f11ad5be --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php @@ -0,0 +1,23 @@ +type = Service::TYPE_HTTP; + + // Rules + $this->addAction(CreateAPIRule::getName(), new CreateAPIRule()); + $this->addAction(CreateSiteRule::getName(), new CreateSiteRule()); + $this->addAction(CreateFunctionRule::getName(), new CreateFunctionRule()); + $this->addAction(CreateRedirectRule::getName(), new CreateRedirectRule()); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index 38b9e2aeba..fc70f2fe25 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -47,6 +47,7 @@ class Create extends Action ->desc('Create deployment') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('event', 'sites.[siteId].deployments.[deploymentId].create') ->label('audits.event', 'deployment.create') ->label('audits.resource', 'site/{request.siteId}') @@ -80,13 +81,12 @@ class Create extends Action ->inject('project') ->inject('queueForEvents') ->inject('deviceForSites') - ->inject('deviceForFunctions') // TODO: Remove this later once volume is added to executor ->inject('deviceForLocal') ->inject('queueForBuilds') ->callback([$this, 'action']); } - public function action(string $siteId, ?string $installCommand, ?string $buildCommand, ?string $outputDirectory, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project, Event $queueForEvents, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds) + public function action(string $siteId, ?string $installCommand, ?string $buildCommand, ?string $outputDirectory, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project, Event $queueForEvents, Device $deviceForSites, Device $deviceForLocal, Build $queueForBuilds) { $activate = \strval($activate) === 'true' || \strval($activate) === '1'; @@ -168,7 +168,7 @@ class Create extends Action // Save to storage $fileSize ??= $deviceForLocal->getFileSize($fileTmpName); - $path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); + $path = $deviceForSites->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); $deployment = $dbForProject->getDocument('deployments', $deploymentId); $metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)]; @@ -180,7 +180,7 @@ class Create extends Action } } - $chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata); + $chunksUploaded = $deviceForSites->upload($fileTmpName, $path, $chunk, $chunks, $metadata); if (empty($chunksUploaded)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file'); @@ -203,7 +203,7 @@ class Create extends Action } } - $fileSize = $deviceForFunctions->getFileSize($path); + $fileSize = $deviceForSites->getFileSize($path); if ($deployment->isEmpty()) { $deployment = $dbForProject->createDocument('deployments', new Document([ @@ -228,24 +228,20 @@ class Create extends Action 'type' => $type ])); - // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); } else { @@ -283,24 +279,20 @@ class Create extends Action 'type' => $type ])); - // Preview deployments for sites - $projectId = $project->getId(); - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; + $domain = ID::unique() . "." . $sitesDomain; $ruleId = md5($domain); - - $rule = Authorization::skip( + Authorization::skip( fn () => $dbForPlatform->createDocument('rules', new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getInternalId(), 'domain' => $domain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), + 'type' => 'deployment', + 'value' => $deployment->getId(), 'status' => 'verified', 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), ])) ); } else { diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php index 133e9a1906..fe16dafae1 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php @@ -34,6 +34,7 @@ class Delete extends Action ->desc('Delete deployment') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('event', 'sites.[siteId].deployments.[deploymentId].delete') ->label('audits.event', 'deployment.delete') ->label('audits.resource', 'site/{request.siteId}') @@ -59,11 +60,10 @@ class Delete extends Action ->inject('queueForDeletes') ->inject('queueForEvents') ->inject('deviceForSites') - ->inject('deviceForFunctions') //TODO: remove it later ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Device $deviceForSites, Device $deviceForFunctions) + public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Device $deviceForSites) { $site = $dbForProject->getDocument('sites', $siteId); if ($site->isEmpty()) { @@ -84,14 +84,14 @@ class Delete extends Action } if (!empty($deployment->getAttribute('path', ''))) { - if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) { + if (!($deviceForSites->delete($deployment->getAttribute('path', '')))) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage'); } } if ($site->getAttribute('deployment') === $deployment->getId()) { // Reset site deployment $site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [ - 'deployment' => '', + 'deploymentId' => '', 'deploymentInternalId' => '', ]))); } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php index 8e1235c5ee..f4b6b73784 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php @@ -15,6 +15,7 @@ use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; use Utopia\Swoole\Request; +use Utopia\Validator\WhiteList; class Get extends Action { @@ -33,6 +34,7 @@ class Get extends Action ->desc('Download deployment') ->groups(['api', 'sites']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'getDeploymentDownload', @@ -51,15 +53,16 @@ class Get extends Action )) ->param('siteId', '', new UID(), 'Site ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true) ->inject('response') ->inject('request') ->inject('dbForProject') ->inject('deviceForSites') - ->inject('deviceForFunctions') //TODO: Remove this later + ->inject('deviceForBuilds') ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForSites, Device $deviceForFunctions) + public function action(string $siteId, string $deploymentId, string $type, Response $response, Request $request, Database $dbForProject, Device $deviceForSites, Device $deviceForBuilds) { $site = $dbForProject->getDocument('sites', $siteId); if ($site->isEmpty()) { @@ -75,18 +78,33 @@ class Get extends Action throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } - $path = $deployment->getAttribute('path', ''); - if (!$deviceForFunctions->exists($path)) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + switch ($type) { + case 'output': + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId')); + if ($build->isEmpty()) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + $path = $build->getAttribute('path', ''); + $device = $deviceForBuilds; + break; + case 'source': + $path = $deployment->getAttribute('path', ''); + $device = $deviceForSites; + break; + } + + if (!$device->exists($path)) { + throw new Exception(Exception::BUILD_NOT_FOUND); } $response ->setContentType('application/gzip') ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->addHeader('X-Peak', \memory_get_peak_usage()) - ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); + ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"'); - $size = $deviceForFunctions->getFileSize($path); + $size = $device->getFileSize($path); $rangeHeader = $request->getHeader('range'); if (!empty($rangeHeader)) { @@ -108,13 +126,13 @@ class Get extends Action ->addHeader('Content-Length', $end - $start + 1) ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); - $response->send($deviceForFunctions->read($path, $start, ($end - $start + 1))); + $response->send($device->read($path, $start, ($end - $start + 1))); } if ($size > APP_STORAGE_READ_BUFFER) { for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { $response->chunk( - $deviceForFunctions->read( + $device->read( $path, ($i * MAX_OUTPUT_CHUNK_SIZE), min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) @@ -123,7 +141,7 @@ class Get extends Action ); } } else { - $response->send($deviceForFunctions->read($path)); + $response->send($device->read($path)); } } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php similarity index 63% rename from src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php rename to src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php index 0c5cfc34e4..b30c07838f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php @@ -1,6 +1,6 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build') - ->desc('Rebuild deployment') + ->setHttpPath('/v1/sites/:siteId/deployments/duplicate') + ->desc('Create duplicate deployment') ->groups(['api', 'sites']) ->label('scope', 'sites.write') ->label('event', 'sites.[siteId].deployments.[deploymentId].update') @@ -38,30 +41,31 @@ class Create extends Action ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( namespace: 'sites', - name: 'createDeploymentBuild', + name: 'createDuplicateDeployment', description: <<param('siteId', '', new UID(), 'Site ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') + ->inject('project') ->inject('dbForProject') + ->inject('dbForPlatform') ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('deviceForSites') - ->inject('deviceForFunctions') //TODO: remove it later ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions) + public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites) { $site = $dbForProject->getDocument('sites', $siteId); @@ -75,14 +79,14 @@ class Create extends Action } $path = $deployment->getAttribute('path'); - if (empty($path) || !$deviceForFunctions->exists($path)) { + if (empty($path) || !$deviceForSites->exists($path)) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } $deploymentId = ID::unique(); - $destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - $deviceForFunctions->transfer($path, $destination, $deviceForFunctions); + $destination = $deviceForSites->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); + $deviceForSites->transfer($path, $destination, $deviceForSites); $deployment->removeAttribute('$internalId'); $deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([ @@ -95,8 +99,28 @@ class Create extends Action 'installCommand' => $site->getAttribute('installCommand', ''), 'outputDirectory' => $site->getAttribute('outputDirectory', ''), 'search' => implode(' ', [$deploymentId]), + 'screenshotLight' => '', + 'screenshotDark' => '' ])); + // Preview deployments for sites + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $domain = ID::unique() . "." . $sitesDomain; + $ruleId = md5($domain); + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'type' => 'deployment', + 'value' => $deployment->getId(), + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + ])) + ); + $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) ->setResource($site) @@ -106,6 +130,8 @@ class Create extends Action ->setParam('siteId', $site->getId()) ->setParam('deploymentId', $deployment->getId()); - $response->noContent(); + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php index 3864fdcac6..c7929b2e7a 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Get.php @@ -8,9 +8,6 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; -use Utopia\Database\Document; -use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -32,6 +29,7 @@ class Get extends Action ->desc('Get deployment') ->groups(['api', 'sites']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'getDeployment', @@ -49,13 +47,11 @@ class Get extends Action ->param('siteId', '', new UID(), 'Site ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') - ->inject('project') ->inject('dbForProject') - ->inject('dbForPlatform') ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) + public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject) { $site = $dbForProject->getDocument('sites', $siteId); @@ -80,16 +76,6 @@ class Get extends Action $deployment->setAttribute('buildSize', $build->getAttribute('size', 0)); $deployment->setAttribute('size', $deployment->getAttribute('size', 0)); - $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ - Query::equal("projectInternalId", [$project->getInternalId()]), - Query::equal("resourceType", ["deployment"]), - Query::equal("resourceInternalId", [$deployment->getInternalId()]) - ])); - - if (!empty($rule)) { - $deployment->setAttribute('domain', $rule->getAttribute('domain', '')); - } - $response->dynamic($deployment, Response::MODEL_DEPLOYMENT); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php similarity index 95% rename from src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Update.php rename to src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php index 87831670a3..0a15053df1 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php @@ -1,6 +1,6 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) - ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build') - ->desc('Cancel deployment') + ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/status') + ->desc('Update deployment status') ->groups(['api', 'sites']) ->label('scope', 'sites.write') ->label('audits.event', 'deployment.update') ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( namespace: 'sites', - name: 'updateDeploymentBuild', + name: 'updateDeploymentStatus', description: <<setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/sites/:siteId/deployments/template') + ->desc('Create deployment') + ->groups(['api', 'sites']) + ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) + ->label('event', 'sites.[siteId].deployments.[deploymentId].create') + ->label('audits.event', 'deployment.create') + ->label('audits.resource', 'site/{request.siteId}') + ->label('sdk', new Method( + namespace: 'sites', + name: 'createTemplateDeployment', + description: <<param('siteId', '', new UID(), 'Site ID.') + ->param('repository', '', new Text(128, 0), 'Repository name of the template.') + ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') + ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') + ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the site template.') + ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('project') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('gitHub') + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $repository, string $owner, string $rootDirectory, string $version, bool $activate, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project, Event $queueForEvents, Build $queueForBuilds, GitHub $github) + { + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $template = new Document([ + 'repositoryName' => $repository, + 'ownerName' => $owner, + 'rootDirectory' => $rootDirectory, + 'version' => $version + ]); + + if (!empty($site->getAttribute('providerRepositoryId'))) { + $installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId')); + + $deployment = $this->redeployVcsSite( + request: $request, + site: $site, + project: $project, + installation: $installation, + dbForProject: $dbForProject, + dbForPlatform: $dbForPlatform, + queueForBuilds: $queueForBuilds, + template: $template, + github: $github, + activate: $activate, + ); + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + + return; + } + + $deploymentId = ID::unique(); + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceId' => $site->getId(), + 'resourceInternalId' => $site->getInternalId(), + 'resourceType' => 'sites', + 'installCommand' => $site->getAttribute('installCommand', ''), + 'buildCommand' => $site->getAttribute('buildCommand', ''), + 'outputDirectory' => $site->getAttribute('outputDirectory', ''), + 'type' => 'manual', + 'search' => implode(' ', [$deploymentId]), + 'activate' => $activate, + ])); + + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $domain = ID::unique() . "." . $sitesDomain; + $ruleId = md5($domain); + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain, + 'type' => 'deployment', + 'value' => $deployment->getId(), + 'status' => 'verified', + 'certificateId' => '', + 'search' => implode(' ', [$ruleId, $domain]), + ])) + ); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($site) + ->setDeployment($deployment) + ->setTemplate($template); + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php new file mode 100644 index 0000000000..d4e256c570 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php @@ -0,0 +1,112 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/sites/:siteId/deployments/vcs') + ->desc('Create VCS deployment') + ->groups(['api', 'sites']) + ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) + ->label('event', 'sites.[siteId].deployments.[deploymentId].create') + ->label('audits.event', 'deployment.create') + ->label('audits.resource', 'site/{request.siteId}') + ->label('sdk', new Method( + namespace: 'sites', + name: 'createVcsDeployment', + description: <<param('siteId', '', new UID(), 'Site ID.') + // TODO: Support tag and commit in future + ->param('type', '', new WhiteList(['branch']), 'Type of reference passed. Allowed values are: branch') + ->param('reference', '', new Text(255), 'VCS reference to create deployment from. Depending on type this can be: branch name') + ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('project') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('gitHub') + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $type, string $reference, bool $activate, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project, Event $queueForEvents, Build $queueForBuilds, GitHub $github) + { + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $template = new Document(); + + $installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId')); + + $deployment = $this->redeployVcsSite( + request: $request, + site: $site, + project: $project, + installation: $installation, + dbForProject: $dbForProject, + dbForPlatform: $dbForPlatform, + queueForBuilds: $queueForBuilds, + template: $template, + github: $github, + activate: $activate, + reference: $reference, + referenceType: $type + ); + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php index 8a79561e2b..c29b14b840 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/XList.php @@ -12,7 +12,6 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Query; -use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -36,6 +35,7 @@ class XList extends Action ->desc('List deployments') ->groups(['api', 'sites']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'listDeployments', @@ -54,13 +54,11 @@ class XList extends Action ->param('queries', [], new Deployments(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') - ->inject('project') ->inject('dbForProject') - ->inject('dbForPlatform') ->callback([$this, 'action']); } - public function action(string $siteId, array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) + public function action(string $siteId, array $queries, string $search, Response $response, Database $dbForProject) { $site = $dbForProject->getDocument('sites', $siteId); @@ -119,16 +117,6 @@ class XList extends Action $result->setAttribute('buildTime', $build->getAttribute('duration', 0)); $result->setAttribute('buildSize', $build->getAttribute('size', 0)); $result->setAttribute('size', $result->getAttribute('size', 0)); - - $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ - Query::equal("projectInternalId", [$project->getInternalId()]), - Query::equal("resourceType", ["deployment"]), - Query::equal("resourceInternalId", [$result->getInternalId()]) - ])); - - if (!empty($rule)) { - $result->setAttribute('domain', $rule->getAttribute('domain', '')); - } } $response->dynamic(new Document([ diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Frameworks/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Frameworks/XList.php index 09075b53cb..0546ee6b4d 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Frameworks/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Frameworks/XList.php @@ -30,6 +30,7 @@ class XList extends Base ->desc('List frameworks') ->groups(['api', 'sites']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'listFrameworks', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Logs/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Logs/Get.php index 0dddeef7c0..f640c164cc 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Logs/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Logs/Get.php @@ -30,6 +30,7 @@ class Get extends Base ->desc('Get log') ->groups(['api', 'sites']) ->label('scope', 'log.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'getLog', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index 079f2fec49..c39f618691 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -2,10 +2,8 @@ namespace Appwrite\Platform\Modules\Sites\Http\Sites; -use Appwrite\Event\Build; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -13,7 +11,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Sites\Validator\FrameworkSpecification; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; -use Appwrite\Utopia\Response\Model\Rule; use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; @@ -21,16 +18,13 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Database\Validator\Authorization; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -use Utopia\Swoole\Request; use Utopia\System\System; use Utopia\Validator\Boolean; use Utopia\Validator\Range; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; -use Utopia\VCS\Adapter\Git\GitHub; class Create extends Base { @@ -49,6 +43,7 @@ class Create extends Base ->desc('Create site') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('event', 'sites.[siteId].create') ->label('audits.event', 'site.create') ->label('audits.resource', 'site/{response.$id}') @@ -74,38 +69,29 @@ class Create extends Base ->param('installCommand', '', new Text(8192, 0), 'Install Command.', true) ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) - ->param('subdomain', '', new CustomId(), 'Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true) ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.') - ->param('adapter', '', new Text(8192, 0), 'Framework adapter. Allows: static, ssr', true) + ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true) ->param('providerBranch', '', new Text(128, 0), 'Production branch for the repo linked to the site.', true) ->param('providerSilentMode', false, new Boolean(), 'Is the VCS (Version Control System) connection in silent mode for the repo linked to the site? In silent mode, comments will not be made on commits and pull requests.', true) ->param('providerRootDirectory', '', new Text(128, 0), 'Path to site code in the linked repo.', true) - ->param('templateRepository', '', new Text(128, 0), 'Repository name of the template.', true) - ->param('templateOwner', '', new Text(128, 0), 'The name of the owner of the template.', true) - ->param('templateRootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.', true) - ->param('templateVersion', '', new Text(128, 0), 'Version (tag) for the repo linked to the site template.', true) ->param('specification', APP_COMPUTE_SPECIFICATION_DEFAULT, fn (array $plan) => new FrameworkSpecification( $plan, Config::getParam('framework-specifications', []), App::getEnv('_APP_COMPUTE_CPUS', APP_COMPUTE_CPUS_DEFAULT), App::getEnv('_APP_COMPUTE_MEMORY', APP_COMPUTE_MEMORY_DEFAULT) ), 'Framework specification for the site and builds.', true, ['plan']) - ->inject('request') ->inject('response') ->inject('dbForProject') ->inject('project') - ->inject('user') ->inject('queueForEvents') - ->inject('queueForBuilds') ->inject('dbForPlatform') - ->inject('gitHub') ->callback([$this, 'action']); } - public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $subdomain, string $buildRuntime, string $adapter, string $installationId, ?string $fallbackFile, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $templateRepository, string $templateOwner, string $templateRootDirectory, string $templateVersion, string $specification, Request $request, Response $response, Database $dbForProject, Document $project, Document $user, Event $queueForEvents, Build $queueForBuilds, Database $dbForPlatform, GitHub $github) + public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $buildRuntime, string $adapter, string $installationId, ?string $fallbackFile, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Database $dbForPlatform) { if (!empty($adapter)) { $configFramework = Config::getParam('frameworks')[$framework] ?? []; @@ -116,21 +102,6 @@ class Create extends Base } } - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $routeSubdomain = ''; - $domain = ''; - - if (!empty($sitesDomain)) { - $routeSubdomain = $subdomain ?: ID::unique(); - $domain = "{$routeSubdomain}.{$sitesDomain}"; - - $subdomain = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', \md5($domain))); - - if ($subdomain && !$subdomain->isEmpty()) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Subdomain already exists. Please choose a different subdomain.'); - } - } - $siteId = ($siteId == 'unique()') ? ID::unique() : $siteId; $allowList = \array_filter(\explode(',', System::getEnv('_APP_SITES_FRAMEWORKS', ''))); @@ -139,20 +110,6 @@ class Create extends Base throw new Exception(Exception::SITE_FRAMEWORK_UNSUPPORTED, 'Framework "' . $framework . '" is not supported'); } - // build from template - $template = new Document([]); - if ( - !empty($templateRepository) - && !empty($templateOwner) - && !empty($templateRootDirectory) - && !empty($templateVersion) - ) { - $template->setAttribute('repositoryName', $templateRepository) - ->setAttribute('ownerName', $templateOwner) - ->setAttribute('rootDirectory', $templateRootDirectory) - ->setAttribute('version', $templateVersion); - } - $installation = $dbForPlatform->getDocument('installations', $installationId); if (!empty($installationId) && $installation->isEmpty()) { @@ -220,118 +177,6 @@ class Create extends Base $site = $dbForProject->updateDocument('sites', $site->getId(), $site); - if (!empty($providerRepositoryId)) { - // Deploy VCS - $this->redeployVcsSite($request, $site, $project, $installation, $dbForProject, $dbForPlatform, $queueForBuilds, $template, $github); - } elseif (!$template->isEmpty()) { - // Deploy non-VCS from template - $deploymentId = ID::unique(); - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceId' => $site->getId(), - 'resourceInternalId' => $site->getInternalId(), - 'resourceType' => 'sites', - 'installCommand' => $site->getAttribute('installCommand', ''), - 'buildCommand' => $site->getAttribute('buildCommand', ''), - 'outputDirectory' => $site->getAttribute('outputDirectory', ''), - 'type' => 'manual', - 'search' => implode(' ', [$deploymentId]), - 'activate' => true, - ])); - - // Preview deployments url - $projectId = $project->getId(); - - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $previewDomain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; - - $rule = Authorization::skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => \md5($previewDomain), - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $previewDomain, - 'resourceType' => 'deployment', - 'resourceId' => $deploymentId, - 'resourceInternalId' => $deployment->getInternalId(), - 'status' => 'verified', - 'certificateId' => '', - ])) - ); - - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($site) - ->setDeployment($deployment) - ->setTemplate($template); - } - - if (!empty($sitesDomain)) { - $rule = Authorization::skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => \md5($domain), - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain, - 'resourceType' => 'site', - 'resourceId' => $site->getId(), - 'resourceInternalId' => $site->getInternalId(), - 'status' => 'verified', - 'certificateId' => '', - ])) - ); - - /** Trigger Webhook */ - $ruleModel = new Rule(); - $ruleCreate = - $queueForEvents - ->setClass(Event::WEBHOOK_CLASS_NAME) - ->setQueue(Event::WEBHOOK_QUEUE_NAME); - - $ruleCreate - ->setProject($project) - ->setEvent('rules.[ruleId].create') - ->setParam('ruleId', $rule->getId()) - ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) - ->trigger(); - - /** Trigger Sites */ - $ruleCreate - ->setClass(Event::SITES_CLASS_NAME) - ->setQueue(Event::SITES_QUEUE_NAME) - ->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('siteId', $site->getId()); $response diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php index 52c39c0e0f..b68b9ec58a 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php @@ -33,6 +33,7 @@ class Delete extends Base ->desc('Delete site') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('event', 'sites.[siteId].delete') ->label('audits.event', 'site.delete') ->label('audits.resource', 'site/{request.siteId}') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php similarity index 74% rename from src/Appwrite/Platform/Modules/Sites/Http/Deployments/Update.php rename to src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php index 9e1c516956..fcb0532929 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php @@ -1,9 +1,11 @@ setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) - ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId') - ->desc('Update deployment') + ->setHttpPath('/v1/sites/:siteId/deployment') + ->desc('Update site\'s deployment') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('event', 'sites.[siteId].deployments.[deploymentId].update') ->label('audits.event', 'deployment.update') ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( namespace: 'sites', - name: 'updateDeployment', + name: 'updateSiteDeployment', description: <<param('siteId', '', new UID(), 'Site ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('project') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -57,7 +61,7 @@ class Update extends Action ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) + public function action(string $siteId, string $deploymentId, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) { $site = $dbForProject->getDocument('sites', $siteId); $deployment = $dbForProject->getDocument('deployments', $deploymentId); @@ -84,6 +88,13 @@ class Update extends Action 'deploymentId' => $deployment->getId(), ]))); + $this->listRules($project, [ + Query::equal("automation", ["site=" . $site->getId()]), + ], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) { + $rule = $rule->setAttribute('value', $deployment->getId()); + $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + }); + $queueForEvents ->setParam('siteId', $site->getId()) ->setParam('deploymentId', $deployment->getId()); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php index 578e0cf164..f9dbdf604c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php @@ -47,6 +47,7 @@ class Update extends Base ->desc('Update site') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('event', 'sites.[siteId].update') ->label('audits.event', 'sites.update') ->label('audits.resource', 'site/{response.$id}') @@ -73,7 +74,7 @@ class Update extends Base ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true) - ->param('adapter', '', new Text(8192, 0), 'Framework adapter. Usuallly allows: static, ssr', true) + ->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true) ->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) ->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true) @@ -244,7 +245,7 @@ class Update extends Base // Redeploy logic if (!$isConnected && !empty($providerRepositoryId)) { - $this->redeployVcsFunction($request, $site, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github); + $this->redeployVcsFunction($request, $site, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github, true); } $queueForEvents->setParam('siteId', $site->getId()); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/XList.php index 35fef542c0..3237eba0be 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/XList.php @@ -35,6 +35,7 @@ class XList extends Base ->desc('List sites') ->groups(['api', 'sites']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'list', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Templates/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Templates/Get.php index 977bfe7fd1..1c92edcf71 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Templates/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Templates/Get.php @@ -29,8 +29,8 @@ class Get extends Base ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/sites/templates/:templateId') ->desc('Get site template') - ->groups(['api']) ->label('scope', 'public') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'getTemplate', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php index 277cc2c615..ae25cbcbf6 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Templates/XList.php @@ -30,8 +30,8 @@ class XList extends Base ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/sites/templates') ->desc('List templates') - ->groups(['api']) ->label('scope', 'public') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'listTemplates', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php index 83b92e701a..660ba49053 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Usage/Get.php @@ -35,6 +35,7 @@ class Get extends Base ->desc('Get site usage') ->groups(['api', 'sites', 'usage']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'getUsage', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php index 225b6cb7a7..ee522114bf 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Usage/XList.php @@ -33,6 +33,7 @@ class XList extends Base ->desc('Get sites usage') ->groups(['api', 'sites', 'usage']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'listUsage', diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php index 1207f6c1c4..66eb84231c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php @@ -37,6 +37,7 @@ class Create extends Base ->desc('Create variable') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('audits.event', 'variable.create') ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( @@ -56,14 +57,13 @@ class Create extends Base ->param('siteId', '', new UID(), 'Site unique ID.', false) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) - ->param('secret', false, new Boolean(), 'Is secret? Secret variables can only be updated or deleted, they cannot be read.', true) + ->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true) ->inject('response') ->inject('dbForProject') - ->inject('dbForPlatform') ->callback([$this, 'action']); } - public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform) + public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject) { $site = $dbForProject->getDocument('sites', $siteId); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php index 461bcdfdd9..458a34438e 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php @@ -31,6 +31,7 @@ class Delete extends Base ->desc('Delete variable') ->groups(['api', 'sites']) ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('audits.event', 'variable.delete') ->label('audits.resource', 'site/{request.siteId}') ->label('sdk', new Method( diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Get.php index ca5ad09663..1e56cc5b15 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Get.php @@ -30,6 +30,7 @@ class Get extends Base ->desc('Get variable') ->groups(['api', 'sites']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label( 'sdk', new Method( diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php index d9d70e4bdf..d64af650e3 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php @@ -13,6 +13,7 @@ use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; +use Utopia\Validator\Boolean; use Utopia\Validator\Text; class Update extends Base @@ -34,6 +35,7 @@ class Update extends Base ->label('scope', 'sites.write') ->label('audits.event', 'variable.update') ->label('audits.resource', 'site/{request.siteId}') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label('sdk', new Method( namespace: 'sites', name: 'updateVariable', @@ -52,12 +54,13 @@ class Update extends Base ->param('variableId', '', new UID(), 'Variable unique ID.', false) ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true) + ->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true) ->inject('response') ->inject('dbForProject') ->callback([$this, 'action']); } - public function action(string $siteId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject) + public function action(string $siteId, string $variableId, string $key, ?string $value, ?bool $secret, Response $response, Database $dbForProject) { $site = $dbForProject->getDocument('sites', $siteId); @@ -74,9 +77,14 @@ class Update extends Base throw new Exception(Exception::VARIABLE_NOT_FOUND); } + if ($variable->getAttribute('secret') === true && $secret === false) { + throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET); + } + $variable ->setAttribute('key', $key) ->setAttribute('value', $value ?? $variable->getAttribute('value')) + ->setAttribute('secret', $secret ?? $variable->getAttribute('secret')) ->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site'])); try { diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php index 9bb887eb26..9d0a55a81c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php @@ -31,6 +31,7 @@ class XList extends Base ->desc('List variables') ->groups(['api', 'sites']) ->label('scope', 'sites.read') + ->label('resourceType', RESOURCE_TYPE_SITES) ->label( 'sdk', new Method( diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index 619702e761..813a857b7d 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -2,14 +2,14 @@ namespace Appwrite\Platform\Modules\Sites\Services; -use Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Create as CreateBuild; -use Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Download\Get as DownloadBuild; -use Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Update as UpdateBuild; use Appwrite\Platform\Modules\Sites\Http\Deployments\Create as CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Delete as DeleteDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Download\Get as DownloadDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\Duplicate\Create as CreateDuplicateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Get as GetDeployment; -use Appwrite\Platform\Modules\Sites\Http\Deployments\Update as UpdateDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\Status\Update as UpdateDeploymentStatus; +use Appwrite\Platform\Modules\Sites\Http\Deployments\Template\Create as CreateTemplateDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\Vcs\Create as CreateVcsDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\XList as ListDeployments; use Appwrite\Platform\Modules\Sites\Http\Frameworks\XList as ListFrameworks; use Appwrite\Platform\Modules\Sites\Http\Logs\Delete as DeleteLog; @@ -17,6 +17,7 @@ use Appwrite\Platform\Modules\Sites\Http\Logs\Get as GetLog; use Appwrite\Platform\Modules\Sites\Http\Logs\XList as ListLogs; use Appwrite\Platform\Modules\Sites\Http\Sites\Create as CreateSite; use Appwrite\Platform\Modules\Sites\Http\Sites\Delete as DeleteSite; +use Appwrite\Platform\Modules\Sites\Http\Sites\Deployment\Update as UpdateSiteDeployment; use Appwrite\Platform\Modules\Sites\Http\Sites\Get as GetSite; use Appwrite\Platform\Modules\Sites\Http\Sites\Update as UpdateSite; use Appwrite\Platform\Modules\Sites\Http\Sites\XList as ListSites; @@ -49,14 +50,15 @@ class Http extends Service // Deployments $this->addAction(CreateDeployment::getName(), new CreateDeployment()); + $this->addAction(CreateTemplateDeployment::getName(), new CreateTemplateDeployment()); + $this->addAction(CreateVcsDeployment::getName(), new CreateVcsDeployment()); $this->addAction(GetDeployment::getName(), new GetDeployment()); $this->addAction(ListDeployments::getName(), new ListDeployments()); - $this->addAction(UpdateDeployment::getName(), new UpdateDeployment()); + $this->addAction(UpdateSiteDeployment::getName(), new UpdateSiteDeployment()); $this->addAction(DeleteDeployment::getName(), new DeleteDeployment()); $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); - $this->addAction(DownloadBuild::getName(), new DownloadBuild()); - $this->addAction(CreateBuild::getName(), new CreateBuild()); - $this->addAction(UpdateBuild::getName(), new UpdateBuild()); + $this->addAction(CreateDuplicateDeployment::getName(), new CreateDuplicateDeployment()); + $this->addAction(UpdateDeploymentStatus::getName(), new UpdateDeploymentStatus()); // Logs $this->addAction(GetLog::getName(), new GetLog()); diff --git a/src/Appwrite/Platform/Services/Tasks.php b/src/Appwrite/Platform/Services/Tasks.php index c09f961fc0..3ada193cf7 100644 --- a/src/Appwrite/Platform/Services/Tasks.php +++ b/src/Appwrite/Platform/Services/Tasks.php @@ -10,9 +10,11 @@ use Appwrite\Platform\Tasks\QueueRetry; use Appwrite\Platform\Tasks\ScheduleExecutions; use Appwrite\Platform\Tasks\ScheduleFunctions; use Appwrite\Platform\Tasks\ScheduleMessages; +use Appwrite\Platform\Tasks\Screenshot; 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; @@ -31,6 +33,7 @@ class Tasks extends Service ->addAction(QueueRetry::getName(), new QueueRetry()) ->addAction(SDKs::getName(), new SDKs()) ->addAction(SSL::getName(), new SSL()) + ->addAction(Screenshot::getName(), new Screenshot()) ->addAction(ScheduleFunctions::getName(), new ScheduleFunctions()) ->addAction(ScheduleExecutions::getName(), new ScheduleExecutions()) ->addAction(ScheduleMessages::getName(), new ScheduleMessages()) @@ -38,6 +41,7 @@ class Tasks extends Service ->addAction(Upgrade::getName(), new Upgrade()) ->addAction(Vars::getName(), new Vars()) ->addAction(Version::getName(), new Version()) + ->addAction(StatsResources::getName(), new StatsResources()) ; } } diff --git a/src/Appwrite/Platform/Services/Workers.php b/src/Appwrite/Platform/Services/Workers.php index bdbb99be75..c37bc80d6e 100644 --- a/src/Appwrite/Platform/Services/Workers.php +++ b/src/Appwrite/Platform/Services/Workers.php @@ -10,8 +10,13 @@ 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; +/** remove */ use Appwrite\Platform\Workers\Usage; use Appwrite\Platform\Workers\UsageDump; +/** /remove */ use Appwrite\Platform\Workers\Webhooks; use Utopia\Platform\Service; @@ -29,10 +34,14 @@ 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(Migrations::getName(), new Migrations()) + ->addAction(StatsResources::getName(), new StatsResources()) + /** Remove */ ->addAction(UsageDump::getName(), new UsageDump()) ->addAction(Usage::getName(), new Usage()) - ->addAction(Migrations::getName(), new Migrations()) - + /** /remove */ ; } } diff --git a/src/Appwrite/Platform/Tasks/Screenshot.php b/src/Appwrite/Platform/Tasks/Screenshot.php new file mode 100644 index 0000000000..43cf434408 --- /dev/null +++ b/src/Appwrite/Platform/Tasks/Screenshot.php @@ -0,0 +1,300 @@ +desc('Create Site template screenshot') + ->param('templateId', '', new Text(128), 'Template ID.') + ->callback(fn (string $templateId) => $this->action($templateId)); + } + + public function action(string $templateId): void + { + $templates = Config::getParam('site-templates', []); + + $allowedTemplates = \array_filter($templates, function ($item) use ($templateId) { + return $item['key'] === $templateId; + }); + $template = \array_shift($allowedTemplates); + + if (empty($template)) { + throw new \Exception("Template {$templateId} not found. Find correct ID in app/config/site-templates.php"); + } + + Console::info("Found: " . $template['name']); + + $client = new Client(); + $client->setEndpoint('http://localhost/v1'); + $client->addHeader('origin', 'http://localhost'); + + // Register + $email = uniqid() . 'user@localhost.test'; + $password = 'password'; + $user = $client->call(Client::METHOD_POST, '/account', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + ]); + + if ($user['headers']['status-code'] !== 201) { + Console::error(\json_encode($user)); + throw new \Exception("Failed to register user"); + } + + Console::info("User created"); + + // Login + $session = $client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'email' => $email, + 'password' => $password, + ]); + + if ($session['headers']['status-code'] !== 201) { + Console::error(\json_encode($session)); + throw new \Exception("Failed to login user"); + } + + Console::info("Session created"); + + $secret = $session['cookies']['a_session_console']; + $cookieConsole = 'a_session_console=' . $secret; + + // Create organization + $team = $client->call(Client::METHOD_POST, '/teams', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => $cookieConsole + ], [ + 'teamId' => ID::unique(), + 'name' => 'Demo Project Team', + ]); + + if ($team['headers']['status-code'] !== 201) { + Console::error(\json_encode($team)); + throw new \Exception("Failed to create team"); + } + + Console::info("Team created"); + + $projectName = 'Demo Project'; + $projectId = ID::unique(); + + // Create project + $project = $client->call(Client::METHOD_POST, '/projects', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => $cookieConsole + ], [ + 'projectId' => $projectId, + 'region' => 'default', + 'name' => $projectName, + 'teamId' => $team['body']['$id'], + 'description' => 'Demo Project Description', + 'url' => 'https://appwrite.io', + ]); + + if ($project['headers']['status-code'] !== 201) { + Console::error(\json_encode($project)); + throw new \Exception("Failed to create project"); + } + + Console::info("Project created"); + + $projectId = $project['body']['$id']; + + $framework = $template['frameworks'][0]; + + // Create site + $site = $client->call(Client::METHOD_POST, '/sites', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ], [ + 'siteId' => ID::unique(), + 'name' => $template["name"], + 'framework' => $framework['key'], + 'adapter' => $framework['adapter'], + 'buildCommand' => $framework['buildCommand'], + 'buildRuntime' => $framework['buildRuntime'], + 'fallbackFile' => $framework['fallbackFile'], + 'installCommand' => $framework['installCommand'], + 'outputDirectory' => $framework['outputDirectory'], + 'providerRootDirectory' => $framework['providerRootDirectory'], + 'timeout' => 60 + ]); + + if ($site['headers']['status-code'] !== 201) { + Console::error(\json_encode($site)); + throw new \Exception("Failed to create site"); + } + + Console::info("Site created"); + + $siteId = $site['body']['$id']; + + // Create variables + if (!empty($template['variables'] ?? [])) { + foreach ($template['variables'] as $variable) { + if (empty($variable['value'] ?? '')) { + if (($variable['required'] ?? false) === true) { + throw new \Exception("Missing required variable: {$variable['name']}"); + } + + continue; + } + + $value = $variable['value']; + $value = \str_replace('{projectName}', $projectName, $value); + $value = \str_replace('{projectId}', $projectId, $value); + $value = \str_replace('{apiEndpoint}', 'http://localhost/v1', $value); + + $response = $client->call(Client::METHOD_POST, '/sites/' . $siteId . '/variables', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ], [ + 'key' => $variable['name'], + 'value' => $value + ]); + + if ($response['headers']['status-code'] !== 201) { + Console::error(\json_encode($response)); + throw new \Exception("Failed to create variable"); + } + } + + Console::info("Variables created"); + } + + // Create deployment + $deployment = $client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments/template', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ], [ + 'owner' => $template['providerOwner'], + 'repository' => $template['providerRepositoryId'], + 'rootDirectory' => $framework['providerRootDirectory'], + 'version' => $template['providerVersion'], + 'activate' => true, + ]); + + if ($deployment['headers']['status-code'] !== 202) { + Console::error(\json_encode($deployment)); + throw new \Exception("Failed to create deployment"); + } + + Console::info("Deployment created"); + + $deploymentId = $deployment['body']['$id']; + + // Await screenshot + $attempts = 50; + $sleep = 5; + + $idLight = ''; + $idDark = ''; + + Console::log("Awaiting deployment (every $sleep seconds, $attempts attempts)"); + + for ($i = 0; $i < $attempts; $i++) { + $deployment = $client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ]); + + if ($deployment['headers']['status-code'] !== 200) { + Console::error(\json_encode($deployment)); + throw new \Exception("Failed to get deployment"); + } + + if ($deployment['body']['status'] === 'failed') { + Console::error(\json_encode($deployment)); + throw new \Exception("Deployment build failed"); + } + + if ($deployment['body']['status'] !== 'ready') { + Console::log("Deployment not ready yet, status: " . $deployment['body']['status']); + \sleep($sleep); + continue; + } + + + if (empty($deployment['body']['screenshotLight'])) { + Console::log("Light screenshot not available yet"); + \sleep($sleep); + continue; + } + + if (empty($deployment['body']['screenshotDark'])) { + Console::log("Dark screenshot not available yet"); + \sleep($sleep); + continue; + } + + $idLight = $deployment['body']['screenshotLight']; + $idDark = $deployment['body']['screenshotDark']; + break; + } + + if (empty($idLight) || empty($idDark)) { + Console::error(\json_encode($deployment)); + throw new \Exception("Failed to get deployment screenshot"); + } + + Console::info("Screenshots created"); + + $themes = [ + [ 'fileId' => $idLight, 'suffix' => 'light' ], + [ 'fileId' => $idDark, 'suffix' => 'dark' ] + ]; + + foreach ($themes as $theme) { + $file = $client->call(Client::METHOD_GET, '/storage/buckets/screenshots/files/' . $theme['fileId'] . '/download', [ + 'x-appwrite-project' => 'console', + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ]); + + if ($file['headers']['status-code'] !== 200) { + Console::error(\json_encode($file)); + throw new \Exception("Failed to download {$theme['suffix']} screenshot"); + } + + $path = "/usr/src/code/public/images/sites/templates/{$template['key']}-{$theme['suffix']}.png"; + + if (!\file_put_contents($path, $file['body'])) { + throw new \Exception("Failed to save {$theme['suffix']} screenshot"); + } + } + + Console::success("Screenshots saved"); + } +} diff --git a/src/Appwrite/Platform/Tasks/StatsResources.php b/src/Appwrite/Platform/Tasks/StatsResources.php new file mode 100644 index 0000000000..ac3b9ead73 --- /dev/null +++ b/src/Appwrite/Platform/Tasks/StatsResources.php @@ -0,0 +1,81 @@ +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"); + } +} diff --git a/src/Appwrite/Platform/Workers/Audits.php b/src/Appwrite/Platform/Workers/Audits.php index c0bcab1c3a..f1ae46eea7 100644 --- a/src/Appwrite/Platform/Workers/Audits.php +++ b/src/Appwrite/Platform/Workers/Audits.php @@ -6,15 +6,33 @@ use Appwrite\Auth\Auth; use Exception; use Throwable; use Utopia\Audit\Audit; +use Utopia\CLI\Console; use Utopia\Database\Database; +use Utopia\Database\DateTime; 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 { + private const BATCH_SIZE_DEVELOPMENT = 1; // smaller batch size for development + private const BATCH_SIZE_PRODUCTION = 5_000; + private const BATCH_AGGREGATION_INTERVAL = 60; // in seconds + + private int $lastTriggeredTime = 0; + + private array $logs = []; + + private 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'; @@ -30,6 +48,8 @@ class Audits extends Action ->inject('message') ->inject('dbForProject') ->callback(fn ($message, $dbForProject) => $this->action($message, $dbForProject)); + + $this->lastTriggeredTime = time(); } @@ -44,13 +64,14 @@ class Audits extends Action */ 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'] ?? ''; @@ -63,23 +84,48 @@ class Audits extends Action $userEmail = $user->getAttribute('email', ''); $userType = $user->getAttribute('type', Auth::ACTIVITY_TYPE_USER); - $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: [ + // Create event data + $eventData = [ + 'userId' => $user->getInternalId(), + '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()) + ]; + + $this->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) { + Console::log('Processing batch with ' . count($this->logs) . ' events'); + $audit = new Audit($dbForProject); + + try { + $audit->logBatch($this->logs); + Console::success('Audit logs processed successfully'); + } catch (Throwable $e) { + Console::error('Error processing audit logs: ' . $e->getMessage()); + } finally { + // Clear the pending events after successful batch processing + $this->logs = []; + $this->lastTriggeredTime = time(); + } + } } } diff --git a/src/Appwrite/Platform/Workers/Databases.php b/src/Appwrite/Platform/Workers/Databases.php index 441b09b4cc..0f400e0107 100644 --- a/src/Appwrite/Platform/Workers/Databases.php +++ b/src/Appwrite/Platform/Workers/Databases.php @@ -577,39 +577,25 @@ class Databases extends Action */ protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void { - $count = 0; - $chunk = 0; - $limit = 50; - $sum = $limit; + $start = \microtime(true); - $executionStart = \microtime(true); + try { + $documents = $database->deleteDocuments($collection, $queries); + } catch (\Throwable $th) { + Console::error('Failed to delete documents for collection ' . $collection . ': ' . $th->getMessage()); + return; + } - 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++; + if (\is_callable($callback)) { + foreach ($documents as $document) { + $callback($document); } } - $executionEnd = \microtime(true); + $end = \microtime(true); + $count = \count($documents); - Console::info("Deleted {$count} document by group in " . ($executionEnd - $executionStart) . " seconds"); + Console::info("Deleted {$count} documents by group in " . ($end - $start) . " seconds"); } protected function trigger( diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 09c71dc004..09fe98dd7f 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -7,6 +7,7 @@ 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; @@ -21,6 +22,7 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization as ValidatorAuthorization; use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Action; @@ -91,13 +93,13 @@ class Deletes extends Action $this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document); break; case DELETE_TYPE_SITES: - $this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $document, $certificates, $project); + $this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project); break; case DELETE_TYPE_FUNCTIONS: $this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project); break; case DELETE_TYPE_DEPLOYMENTS: - $this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $certificates, $project); + $this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForSites, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project); break; case DELETE_TYPE_USERS: $this->deleteUser($getProjectDB, $document, $project); @@ -510,7 +512,8 @@ class Deletes extends Action $projectCollectionIds = [ ...\array_keys(Config::getParam('collections', [])['projects']), - Audit::COLLECTION + Audit::COLLECTION, + AbuseDatabase::COLLECTION, ]; $limit = \count($projectCollectionIds) + 25; @@ -740,14 +743,13 @@ class Deletes extends Action /** * @param callable $getProjectDB * @param Device $deviceForSites - * @param Device $deviceForFunctions * @param Device $deviceForBuilds * @param Document $document function document * @param Document $project * @return void * @throws Exception */ - private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, CertificatesAdapter $certificates, Document $project): void + private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void { $dbForProject = $getProjectDB($project); $siteId = $document->getId(); @@ -758,8 +760,8 @@ class Deletes extends Action */ Console::info("Deleting rules for site " . $siteId); $this->deleteByGroup('rules', [ - Query::equal('resourceType', ['site']), - Query::equal('resourceInternalId', [$siteInternalId]), + Query::equal('type', ['deployment']), + Query::equal('automation', ['site=' . $siteId]), Query::equal('projectInternalId', [$project->getInternalId()]) ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); @@ -779,22 +781,25 @@ class Deletes extends Action */ Console::info("Deleting deployments for site " . $siteId); $deploymentInternalIds = []; + $deploymentIds = []; $this->deleteByGroup('deployments', [ Query::equal('resourceInternalId', [$siteInternalId]) - ], $dbForProject, function (Document $document) use ($deviceForFunctions, &$deploymentInternalIds) { + ], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) { $deploymentInternalIds[] = $document->getInternalId(); - $this->deleteDeploymentFiles($deviceForFunctions, $document); + $deploymentIds[] = $document->getId(); + $this->deleteDeploymentFiles($deviceForSites, $document); + $this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document); + $this->deleteDeploymentRules($dbForPlatform, $document, $project, $certificates); }); /** * Delete rules for all deployments of the site */ - //TODO: If functions also have previews in the future, change the logic here to use unique identifier for sites and functions - foreach ($deploymentInternalIds as $deploymentInternalId) { - Console::info("Deleting rules for site " . $siteId . "'s deployment " . $deploymentInternalId); + foreach ($deploymentIds as $deploymentId) { + Console::info("Deleting rules for site " . $siteId . "'s deployment " . $deploymentId); $this->deleteByGroup('rules', [ - Query::equal('resourceType', ['deployment']), - Query::equal('resourceInternalId', [$deploymentInternalId]), + Query::equal('type', ['deployment']), + Query::equal('value', [$deploymentId]), Query::equal('projectInternalId', [$project->getInternalId()]) ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); @@ -852,8 +857,8 @@ class Deletes extends Action */ Console::info("Deleting rules for function " . $functionId); $this->deleteByGroup('rules', [ - Query::equal('resourceType', ['function']), - Query::equal('resourceInternalId', [$functionInternalId]), + Query::equal('type', ['deployment']), + Query::equal('automation', ['function=' . $functionId]), Query::equal('projectInternalId', [$project->getInternalId()]) ], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); @@ -876,9 +881,10 @@ class Deletes extends Action $deploymentInternalIds = []; $this->deleteByGroup('deployments', [ Query::equal('resourceInternalId', [$functionInternalId]) - ], $dbForProject, function (Document $document) use ($deviceForFunctions, &$deploymentInternalIds) { + ], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, &$deploymentInternalIds) { $deploymentInternalIds[] = $document->getInternalId(); $this->deleteDeploymentFiles($deviceForFunctions, $document); + $this->deleteDeploymentRules($dbForPlatform, $document, $project, $certificates); }); /** @@ -926,6 +932,65 @@ class Deletes extends Action $this->deleteRuntimes($getProjectDB, $document, $project); } + private function deleteDeploymentRules(Database $dbForPlatform, Document $deployment, Document $project, CertificatesAdapter $certificates): void + { + Console::info("Deleting rules for site " . $deployment->getId()); + $this->deleteByGroup('rules', [ + Query::equal('type', ['deployment']), + Query::equal('value', [$deployment->getId()]), + Query::equal('projectInternalId', [$project->getInternalId()]) + ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { + $this->deleteRule($dbForPlatform, $document, $certificates); + }); + } + + private function deleteDeploymentScreenshots(Device $deviceForFiles, Database $dbForPlatform, Document $deployment): void + { + $screenshotIds = []; + if (!empty($deployment->getAttribute('screenshotLight', ''))) { + $screenshotIds[] = $deployment->getAttribute('screenshotLight', ''); + } + if (!empty($deployment->getAttribute('screenshotDark', ''))) { + $screenshotIds[] = $deployment->getAttribute('screenshotDark', ''); + } + + if (empty($screenshotIds)) { + return; + } + Console::info("Deleting screenshots for deployment " . $deployment->getId()); + + $bucket = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); + if ($bucket->isEmpty()) { + Console::error('Failed to get bucket for deployment screenshots'); + return; + } + + foreach ($screenshotIds as $id) { + $file = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('bucket_' . $bucket->getInternalId(), $id)); + + if ($file->isEmpty()) { + Console::error('Failed to get deployment screenshot: ' . $id); + continue; + } + + $path = $file->getAttribute('path', ''); + + try { + if ($deviceForFiles->delete($path, true)) { + Console::success('Deleted deployment screenshot: ' . $path); + } else { + Console::error('Failed to delete deployment screenshot: ' . $path); + } + } catch (\Throwable $th) { + Console::error('Failed to delete deployment screenshot: ' . $path); + Console::error('[Error] Type: ' . get_class($th)); + Console::error('[Error] Message: ' . $th->getMessage()); + Console::error('[Error] File: ' . $th->getFile()); + Console::error('[Error] Line: ' . $th->getLine()); + } + } + } + /** * @param Device $device * @param Document $deployment @@ -991,13 +1056,14 @@ class Deletes extends Action /** * @param callable $getProjectDB * @param Device $deviceForFunctions + * @param Device $deviceForSites * @param Device $deviceForBuilds * @param Document $document * @param Document $project * @return void * @throws Exception */ - private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, CertificatesAdapter $certificates, Document $project): void + private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForSites, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -1007,7 +1073,16 @@ class Deletes extends Action /** * Delete deployment files */ - $this->deleteDeploymentFiles($deviceForFunctions, $document); //TODO: For sites, this should be deviceForSites + match ($document->getAttribute('resourceType')) { + 'functions' => $this->deleteDeploymentFiles($deviceForFunctions, $document), + 'sites' => $this->deleteDeploymentFiles($deviceForSites, $document), + default => throw new Exception('Invalid resource type') + }; + + /** + * Delete deployment screenshots + */ + $this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document); /** * Delete builds @@ -1025,8 +1100,8 @@ class Deletes extends Action */ Console::info("Deleting rules for deployment " . $deploymentId); $this->deleteByGroup('rules', [ - Query::equal('resourceType', ['deployment']), - Query::equal('resourceInternalId', [$deploymentInternalId]), + Query::equal('type', ['deployment']), + Query::equal('value', [$deploymentId]), Query::equal('projectInternalId', [$project->getInternalId()]) ], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) { $this->deleteRule($dbForPlatform, $document, $certificates); @@ -1039,65 +1114,39 @@ 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|null $callback + * @param ?callable $callback * @return void * @throws Exception */ - protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null): void - { - $count = 0; - $chunk = 0; - $limit = 50; - $sum = $limit; + protected function deleteByGroup( + string $collection, + array $queries, + Database $database, + ?callable $callback = null + ): void { + $start = \microtime(true); - $executionStart = \microtime(true); + try { + $documents = $database->deleteDocuments($collection, $queries); + } catch (\Throwable $th) { + Console::error('Failed to delete documents for collection ' . $collection . ': ' . $th->getMessage()); + return; + } - 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++; + if (\is_callable($callback)) { + foreach ($documents as $document) { + $callback($document); } } - $executionEnd = \microtime(true); + $end = \microtime(true); + $count = \count($documents); - Console::info("Deleted {$count} document by group in " . ($executionEnd - $executionStart) . " seconds"); + Console::info("Deleted {$count} documents by group in " . ($end - $start) . " seconds"); } /** @@ -1111,25 +1160,23 @@ class Deletes extends Action protected function listByGroup(string $collection, array $queries, Database $database, callable $callback = null): void { $count = 0; - $chunk = 0; - $limit = 50; - $results = []; + $limit = 1000; $sum = $limit; $cursor = null; - $executionStart = \microtime(true); + $start = \microtime(true); while ($sum === $limit) { - $chunk++; - $mergedQueries = \array_merge([Query::limit($limit)], $queries); - if ($cursor instanceof Document) { - $mergedQueries[] = Query::cursorAfter($cursor); + $queries = \array_merge([Query::limit($limit)], $queries); + + if ($cursor !== null) { + $queries[] = Query::cursorAfter($cursor); } - $results = $database->find($collection, $mergedQueries); + $results = $database->find($collection, $queries); - $sum = count($results); + $sum = \count($results); if ($sum > 0) { $cursor = $results[$sum - 1]; @@ -1144,9 +1191,9 @@ class Deletes extends Action } } - $executionEnd = \microtime(true); + $end = \microtime(true); - Console::info("Listed {$count} document by group in " . ($executionEnd - $executionStart) . " seconds"); + Console::info("Listed {$count} documents by group in " . ($end - $start) . " seconds"); } /** diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 33f0a1e827..f6d441ef3c 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -5,7 +5,7 @@ namespace Appwrite\Platform\Workers; use Ahc\Jwt\JWT; use Appwrite\Event\Event; use Appwrite\Event\Func; -use Appwrite\Event\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Utopia\Response\Model\Execution; use Exception; @@ -46,13 +46,13 @@ class Functions extends Action ->inject('dbForProject') ->inject('queueForFunctions') ->inject('queueForEvents') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('log') ->inject('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)); + ->callback(fn (Document $project, Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log, callable $isResourceBlocked) => $this->action($project, $message, $dbForProject, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $log, $isResourceBlocked)); } - public function action(Document $project, Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Log $log, callable $isResourceBlocked): void + public function action(Document $project, Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log, callable $isResourceBlocked): void { $payload = $message->getPayload() ?? []; @@ -137,7 +137,7 @@ class Functions extends Action log: $log, dbForProject: $dbForProject, queueForFunctions: $queueForFunctions, - queueForUsage: $queueForUsage, + queueForStatsUsage: $queueForStatsUsage, queueForEvents: $queueForEvents, project: $project, function: $function, @@ -177,7 +177,7 @@ class Functions extends Action log: $log, dbForProject: $dbForProject, queueForFunctions: $queueForFunctions, - queueForUsage: $queueForUsage, + queueForStatsUsage: $queueForStatsUsage, queueForEvents: $queueForEvents, project: $project, function: $function, @@ -199,7 +199,7 @@ class Functions extends Action log: $log, dbForProject: $dbForProject, queueForFunctions: $queueForFunctions, - queueForUsage: $queueForUsage, + queueForStatsUsage: $queueForStatsUsage, queueForEvents: $queueForEvents, project: $project, function: $function, @@ -285,7 +285,7 @@ class Functions extends Action * @param Log $log * @param Database $dbForProject * @param Func $queueForFunctions - * @param Usage $queueForUsage + * @param StatsUsage $queueForStatsUsage * @param Event $queueForEvents * @param Document $project * @param Document $function @@ -309,7 +309,7 @@ class Functions extends Action Log $log, Database $dbForProject, Func $queueForFunctions, - Usage $queueForUsage, + StatsUsage $queueForStatsUsage, Event $queueForEvents, Document $project, Document $function, @@ -481,8 +481,8 @@ class Functions extends Action 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', - 'APPWRITE_COMPUTE_CPUS' => ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT), - 'APPWRITE_COMPUTE_MEMORY' => ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT), + 'APPWRITE_FUNCTION_CPUS' => ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT), + 'APPWRITE_FUNCTION_MEMORY' => ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT), 'APPWRITE_VERSION' => APP_VERSION_STABLE, 'APPWRITE_REGION' => $project->getAttribute('region'), 'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''), @@ -554,7 +554,7 @@ class Functions extends Action $errorCode = $th->getCode(); } finally { /** Trigger usage queue */ - $queueForUsage + $queueForStatsUsage ->setProject($project) ->addMetric(METRIC_EXECUTIONS, 1) ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1) diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index aee60a2bb5..cf2b8bfc84 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -2,7 +2,7 @@ namespace Appwrite\Platform\Workers; -use Appwrite\Event\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Messaging\Status as MessageStatus; use Swoole\Runtime; use Utopia\CLI\Console; @@ -70,8 +70,8 @@ class Messaging extends Action ->inject('log') ->inject('dbForProject') ->inject('deviceForFiles') - ->inject('queueForUsage') - ->callback(fn (Message $message, Document $project, Log $log, Database $dbForProject, Device $deviceForFiles, Usage $queueForUsage) => $this->action($message, $project, $log, $dbForProject, $deviceForFiles, $queueForUsage)); + ->inject('queueForStatsUsage') + ->callback(fn (Message $message, Document $project, Log $log, Database $dbForProject, Device $deviceForFiles, StatsUsage $queueForStatsUsage) => $this->action($message, $project, $log, $dbForProject, $deviceForFiles, $queueForStatsUsage)); } /** @@ -80,7 +80,7 @@ class Messaging extends Action * @param Log $log * @param Database $dbForProject * @param Device $deviceForFiles - * @param Usage $queueForUsage + * @param StatsUsage $queueForStatsUsage * @return void * @throws \Exception */ @@ -90,7 +90,7 @@ class Messaging extends Action Log $log, Database $dbForProject, Device $deviceForFiles, - Usage $queueForUsage + StatsUsage $queueForStatsUsage ): void { Runtime::setHookFlags(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP); $payload = $message->getPayload() ?? []; @@ -111,7 +111,7 @@ class Messaging extends Action case MESSAGE_SEND_TYPE_EXTERNAL: $message = $dbForProject->getDocument('messages', $payload['messageId']); - $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $queueForUsage); + $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $queueForStatsUsage); break; default: throw new \Exception('Unknown message type: ' . $type); @@ -123,7 +123,7 @@ class Messaging extends Action Document $message, Device $deviceForFiles, Document $project, - Usage $queueForUsage + StatsUsage $queueForStatsUsage ): void { $topicIds = $message->getAttribute('topics', []); $targetIds = $message->getAttribute('targets', []); @@ -229,8 +229,8 @@ class Messaging extends Action /** * @var array $results */ - $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) { + $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) { if (\array_key_exists($providerId, $providers)) { $provider = $providers[$providerId]; } else { @@ -257,8 +257,8 @@ class Messaging extends Action $adapter->getMaxMessagesPerRequest() ); - 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) { + 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) { $deliveredTotal = 0; $deliveryErrors = []; $messageData = clone $message; @@ -298,7 +298,7 @@ class Messaging extends Action $deliveryErrors[] = 'Failed sending to targets with error: ' . $e->getMessage(); } finally { $errorTotal = \count($deliveryErrors); - $queueForUsage + $queueForStatsUsage ->setProject($project) ->addMetric(METRIC_MESSAGES, ($deliveredTotal + $errorTotal)) ->addMetric(METRIC_MESSAGES_SENT, $deliveredTotal) diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index cd567f6fa3..3bf8e06356 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -188,28 +188,21 @@ class Migrations extends Action } /** - * @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 { $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400, 0); + $apiKey = $jwt->encode([ 'projectId' => $project->getId(), + 'disabledMetrics' => [ + METRIC_DATABASES_OPERATIONS_READS, + METRIC_DATABASES_OPERATIONS_WRITES, + METRIC_NETWORK_REQUESTS, + METRIC_NETWORK_INBOUND, + METRIC_NETWORK_OUTBOUND, + ], 'scopes' => [ 'users.read', 'users.write', @@ -222,12 +215,9 @@ class Migrations extends Action 'functions.read', 'functions.write', 'databases.read', - 'databases.write', 'collections.read', - 'collections.write', 'documents.read', - 'documents.write' - ] + ], ]); return API_KEY_DYNAMIC . '_' . $apiKey; diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php new file mode 100644 index 0000000000..a6101522fb --- /dev/null +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -0,0 +1,340 @@ + 'Y-m-d H:00', + '1d' => 'Y-m-d 00:00', + 'inf' => '0000-00-00 00:00' + ]; + + /** + * @var array $documents + * + * Array of documents to batch write + * + */ + protected array $documents = []; + + public static function getName(): string + { + return 'stats-resources'; + } + + + /** + * @throws Exception + */ + public function __construct() + { + $this + ->desc('Stats resources worker') + ->inject('message') + ->inject('project') + ->inject('getProjectDB') + ->inject('getLogsDB') + ->inject('dbForPlatform') + ->inject('logError') + ->callback([$this, 'action']); + } + + /** + * @param Message $message + * @param Document $project + * @param callable $getProjectDB + * @return void + * @throws \Utopia\Database\Exception + * @throws Exception + */ + public function action(Message $message, Document $project, callable $getProjectDB, callable $getLogsDB, Database $dbForPlatform, callable $logError): void + { + $this->logError = $logError; + + $payload = $message->getPayload() ?? []; + if (empty($payload)) { + throw new Exception('Missing payload'); + } + + if (empty($project->getAttribute('database'))) { + var_dump($payload); + return; + } + + // Reset documents for each job + $this->documents = []; + + $this->countForProject($dbForPlatform, $getLogsDB, $getProjectDB, $project); + } + + + protected function countForProject(Database $dbForPlatform, callable $getLogsDB, callable $getProjectDB, Document $project): void + { + Console::info('Begining count for: ' . $project->getId()); + + $dbForLogs = null; + $dbForProject = null; + try { + /** @var \Utopia\Database\Database $dbForLogs */ + $dbForLogs = call_user_func($getLogsDB, $project); + /** @var \Utopia\Database\Database $dbForProject */ + $dbForProject = call_user_func($getProjectDB, $project); + } catch (Throwable $th) { + Console::error('Unable to get database'); + Console::error($th->getMessage()); + return; + } + + try { + + $region = $project->getAttribute('region'); + + $platforms = $dbForPlatform->count('platforms', [ + Query::equal('projectInternalId', [$project->getInternalId()]) + ]); + $webhooks = $dbForPlatform->count('webhooks', [ + Query::equal('projectInternalId', [$project->getInternalId()]) + ]); + $keys = $dbForPlatform->count('keys', [ + Query::equal('projectInternalId', [$project->getInternalId()]) + ]); + $databases = $dbForProject->count('databases'); + $buckets = $dbForProject->count('buckets'); + $users = $dbForProject->count('users'); + + $last30Days = (new \DateTime())->sub(\DateInterval::createFromDateString('30 days'))->format('Y-m-d 00:00:00'); + $usersMAU = $dbForProject->count('users', [ + Query::greaterThanEqual('accessedAt', $last30Days) + ]); + $last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours'))->format('Y-m-d h:m:00'); + $usersDAU = $dbForProject->count('users', [ + Query::greaterThanEqual('accessedAt', $last24Hours) + ]); + $last7Days = (new \DateTime())->sub(\DateInterval::createFromDateString('7 days'))->format('Y-m-d 00:00:00'); + $usersWAU = $dbForProject->count('users', [ + Query::greaterThanEqual('accessedAt', $last7Days) + ]); + $teams = $dbForProject->count('teams'); + $functions = $dbForProject->count('functions'); + $messages = $dbForProject->count('messages'); + $providers = $dbForProject->count('providers'); + $topics = $dbForProject->count('topics'); + + $metrics = [ + METRIC_DATABASES => $databases, + METRIC_BUCKETS => $buckets, + METRIC_USERS => $users, + METRIC_FUNCTIONS => $functions, + METRIC_TEAMS => $teams, + METRIC_MESSAGES => $messages, + METRIC_MAU => $usersMAU, + METRIC_DAU => $usersDAU, + METRIC_WAU => $usersWAU, + METRIC_WEBHOOKS => $webhooks, + METRIC_PLATFORMS => $platforms, + METRIC_PROVIDERS => $providers, + METRIC_TOPICS => $topics, + METRIC_KEYS => $keys, + ]; + + foreach ($metrics as $metric => $value) { + $this->createStatsDocuments($region, $metric, $value); + } + + try { + $this->countForBuckets($dbForProject, $dbForLogs, $region); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_buckets_{$project->getId()}"]); + } + + try { + $this->countImageTransformations($dbForProject, $dbForLogs, $region); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_buckets_{$project->getId()}"]); + } + + try { + $this->countForDatabase($dbForProject, $dbForLogs, $region); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_database_{$project->getId()}"]); + } + + try { + $this->countForFunctions($dbForProject, $dbForLogs, $region); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_functions_{$project->getId()}"]); + } + + $this->writeDocuments($dbForLogs, $project); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_project_{$project->getId()}"]); + } + + Console::info('End of count for: ' . $project->getId()); + } + + protected function countForBuckets(Database $dbForProject, Database $dbForLogs, string $region) + { + $totalFiles = 0; + $totalStorage = 0; + $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalFiles, &$totalStorage) { + $files = $dbForProject->count('bucket_' . $bucket->getInternalId()); + + $metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES); + $this->createStatsDocuments($region, $metric, $files); + + $storage = $dbForProject->sum('bucket_' . $bucket->getInternalId(), 'sizeActual'); + $metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE); + $this->createStatsDocuments($region, $metric, $storage); + + $totalStorage += $storage; + $totalFiles += $files; + }); + + $this->createStatsDocuments($region, METRIC_FILES, $totalFiles); + $this->createStatsDocuments($region, METRIC_FILES_STORAGE, $totalStorage); + } + + /** + * Need separate function to count per period data + */ + protected function countImageTransformations(Database $dbForProject, Database $dbForLogs, string $region) + { + $totalImageTransformations = 0; + $last30Days = (new \DateTime())->sub(\DateInterval::createFromDateString('30 days'))->format('Y-m-d 00:00:00'); + $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $last30Days, $region, &$totalImageTransformations) { + $imageTransformations = $dbForProject->count('bucket_' . $bucket->getInternalId(), [ + Query::greaterThanEqual('transformedAt', $last30Days), + ]); + $metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED); + $this->createStatsDocuments($region, $metric, $imageTransformations); + $totalImageTransformations += $imageTransformations; + }); + + $this->createStatsDocuments($region, METRIC_FILES_IMAGES_TRANSFORMED, $totalImageTransformations); + } + + protected function countForDatabase(Database $dbForProject, Database $dbForLogs, string $region) + { + $totalCollections = 0; + $totalDocuments = 0; + + $this->foreachDocument($dbForProject, 'databases', [], function ($database) use ($dbForProject, $dbForLogs, $region, &$totalCollections, &$totalDocuments) { + $collections = $dbForProject->count('database_' . $database->getInternalId()); + + $metric = str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS); + $this->createStatsDocuments($region, $metric, $collections); + + $documents = $this->countForCollections($dbForProject, $dbForLogs, $database, $region); + + $totalDocuments += $documents; + $totalCollections += $collections; + }); + + $this->createStatsDocuments($region, METRIC_COLLECTIONS, $totalCollections); + $this->createStatsDocuments($region, METRIC_DOCUMENTS, $totalDocuments); + } + protected function countForCollections(Database $dbForProject, Database $dbForLogs, Document $database, string $region): int + { + $databaseDocuments = 0; + $this->foreachDocument($dbForProject, 'database_' . $database->getInternalId(), [], function ($collection) use ($dbForProject, $dbForLogs, $database, $region, &$totalCollections, &$databaseDocuments) { + $documents = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); + + $metric = str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getInternalId(), $collection->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS); + $this->createStatsDocuments($region, $metric, $documents); + + $databaseDocuments += $documents; + }); + + $metric = str_replace(['{databaseInternalId}'], [$database->getInternalId()], METRIC_DATABASE_ID_DOCUMENTS); + $this->createStatsDocuments($region, $metric, $databaseDocuments); + + return $databaseDocuments; + } + + protected function countForFunctions(Database $dbForProject, Database $dbForLogs, string $region) + { + $deploymentsStorage = $dbForProject->sum('deployments', 'size'); + $buildsStorage = $dbForProject->sum('builds', 'size'); + $this->createStatsDocuments($region, METRIC_DEPLOYMENTS_STORAGE, $deploymentsStorage); + $this->createStatsDocuments($region, METRIC_BUILDS_STORAGE, $buildsStorage); + + $deployments = $dbForProject->count('deployments'); + $builds = $dbForProject->count('builds'); + $this->createStatsDocuments($region, METRIC_DEPLOYMENTS, $deployments); + $this->createStatsDocuments($region, METRIC_BUILDS, $builds); + + + $this->foreachDocument($dbForProject, 'functions', [], function (Document $function) use ($dbForProject, $dbForLogs, $region) { + $functionDeploymentsStorage = $dbForProject->sum('deployments', 'size', [ + Query::equal('resourceInternalId', [$function->getInternalId()]), + Query::equal('resourceType', [RESOURCE_TYPE_FUNCTIONS]), + ]); + $this->createStatsDocuments($region, str_replace(['{resourceType}','{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS,$function->getInternalId()], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE), $functionDeploymentsStorage); + + $functionDeployments = $dbForProject->count('deployments', [ + Query::equal('resourceInternalId', [$function->getInternalId()]), + Query::equal('resourceType', [RESOURCE_TYPE_FUNCTIONS]), + ]); + $this->createStatsDocuments($region, str_replace(['{resourceType}','{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS,$function->getInternalId()], METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS), $functionDeployments); + + /** + * As deployments and builds have 1-1 relationship, + * the count for one should match the other + */ + $this->createStatsDocuments($region, str_replace(['{resourceType}','{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS,$function->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS), $functionDeployments); + + $functionBuildsStorage = 0; + + $this->foreachDocument($dbForProject, 'deployments', [ + Query::equal('resourceInternalId', [$function->getInternalId()]), + Query::equal('resourceType', [RESOURCE_TYPE_FUNCTIONS]), + ], function (Document $deployment) use ($dbForProject, &$functionBuildsStorage): void { + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); + $functionBuildsStorage += $build->getAttribute('size', 0); + }); + + $this->createStatsDocuments($region, str_replace(['{resourceType}','{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS,$function->getInternalId()], METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE), $functionBuildsStorage); + }); + } + + protected function createStatsDocuments(string $region, string $metric, int $value) + { + foreach ($this->periods as $period => $format) { + $time = 'inf' === $period ? null : \date($format, \time()); + $id = \md5("{$time}_{$period}_{$metric}"); + + $this->documents[] = new Document([ + '$id' => $id, + 'metric' => $metric, + 'period' => $period, + 'region' => $region, + 'value' => $value, + 'time' => $time, + ]); + } + } + + protected function writeDocuments(Database $dbForLogs, Document $project): void + { + $dbForLogs->createOrUpdateDocuments( + 'stats', + $this->documents + ); + $this->documents = []; + Console::success('Stats written to logs db for project: ' . $project->getId() . '(' . $project->getInternalId() . ')'); + } +} diff --git a/src/Appwrite/Platform/Workers/StatsUsage.php b/src/Appwrite/Platform/Workers/StatsUsage.php new file mode 100644 index 0000000000..c4d8b0e8d2 --- /dev/null +++ b/src/Appwrite/Platform/Workers/StatsUsage.php @@ -0,0 +1,246 @@ +desc('Stats usage worker') + ->inject('message') + ->inject('getProjectDB') + ->inject('queueForStatsUsageDump') + ->callback([$this, 'action']); + + $this->lastTriggeredTime = time(); + } + + /** + * @param Message $message + * @param callable $getProjectDB + * @param StatsUsageDump $queueForStatsUsageDump + * @return void + * @throws \Utopia\Database\Exception + * @throws Exception + */ + public function action(Message $message, callable $getProjectDB, StatsUsageDump $queueForStatsUsageDump): void + { + $payload = $message->getPayload() ?? []; + if (empty($payload)) { + throw new Exception('Missing payload'); + } + //Todo Figure out way to preserve keys when the container is being recreated @shimonewman + + $aggregationInterval = (int) System::getEnv('_APP_USAGE_AGGREGATION_INTERVAL', '20'); + $project = new Document($payload['project'] ?? []); + $projectId = $project->getInternalId(); + foreach ($payload['reduce'] ?? [] as $document) { + if (empty($document)) { + continue; + } + + $this->reduce( + project: $project, + document: new Document($document), + metrics: $payload['metrics'], + getProjectDB: $getProjectDB + ); + } + + $this->stats[$projectId]['project'] = $project; + $this->stats[$projectId]['receivedAt'] = DateTime::now(); + foreach ($payload['metrics'] ?? [] as $metric) { + $this->keys++; + if (!isset($this->stats[$projectId]['keys'][$metric['key']])) { + $this->stats[$projectId]['keys'][$metric['key']] = $metric['value']; + continue; + } + + $this->stats[$projectId]['keys'][$metric['key']] += $metric['value']; + } + + // If keys crossed threshold or X time passed since the last send and there are some keys in the array ($this->stats) + if ( + $this->keys >= self::KEYS_THRESHOLD || + (time() - $this->lastTriggeredTime > $aggregationInterval && $this->keys > 0) + ) { + Console::warning('[' . DateTime::now() . '] Aggregated ' . $this->keys . ' keys'); + + $queueForStatsUsageDump + ->setStats($this->stats) + ->trigger(); + + $this->stats = []; + $this->keys = 0; + $this->lastTriggeredTime = time(); + } + } + + /** + * On Documents that tied by relations like functions>deployments>build || documents>collection>database || buckets>files. + * When we remove a parent document we need to deduct his children aggregation from the project scope. + * @param Document $project + * @param Document $document + * @param array $metrics + * @param callable $getProjectDB + * @return void + */ + private function reduce(Document $project, Document $document, array &$metrics, callable $getProjectDB): void + { + $dbForProject = $getProjectDB($project); + + try { + switch (true) { + case $document->getCollection() === 'users': // users + $sessions = count($document->getAttribute(METRIC_SESSIONS, 0)); + if (!empty($sessions)) { + $metrics[] = [ + 'key' => METRIC_SESSIONS, + 'value' => ($sessions * -1), + ]; + } + break; + case $document->getCollection() === 'databases': // databases + $collections = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS))); + $documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS))); + if (!empty($collections['value'])) { + $metrics[] = [ + 'key' => METRIC_COLLECTIONS, + 'value' => ($collections['value'] * -1), + ]; + } + + if (!empty($documents['value'])) { + $metrics[] = [ + 'key' => METRIC_DOCUMENTS, + 'value' => ($documents['value'] * -1), + ]; + } + break; + case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections + $parts = explode('_', $document->getCollection()); + $databaseInternalId = $parts[1] ?? 0; + $documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $document->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS))); + + if (!empty($documents['value'])) { + $metrics[] = [ + 'key' => METRIC_DOCUMENTS, + 'value' => ($documents['value'] * -1), + ]; + $metrics[] = [ + 'key' => str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), + 'value' => ($documents['value'] * -1), + ]; + } + break; + + case $document->getCollection() === 'buckets': + $files = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES))); + $storage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE))); + + if (!empty($files['value'])) { + $metrics[] = [ + 'key' => METRIC_FILES, + 'value' => ($files['value'] * -1), + ]; + } + + if (!empty($storage['value'])) { + $metrics[] = [ + 'key' => METRIC_FILES_STORAGE, + 'value' => ($storage['value'] * -1), + ]; + } + break; + + case $document->getCollection() === 'functions': + $deployments = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS))); + $deploymentsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE))); + $builds = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS))); + $buildsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE))); + $buildsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE))); + $executions = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS))); + $executionsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE))); + + if (!empty($deployments['value'])) { + $metrics[] = [ + 'key' => METRIC_DEPLOYMENTS, + 'value' => ($deployments['value'] * -1), + ]; + } + + if (!empty($deploymentsStorage['value'])) { + $metrics[] = [ + 'key' => METRIC_DEPLOYMENTS_STORAGE, + 'value' => ($deploymentsStorage['value'] * -1), + ]; + } + + if (!empty($builds['value'])) { + $metrics[] = [ + 'key' => METRIC_BUILDS, + 'value' => ($builds['value'] * -1), + ]; + } + + if (!empty($buildsStorage['value'])) { + $metrics[] = [ + 'key' => METRIC_BUILDS_STORAGE, + 'value' => ($buildsStorage['value'] * -1), + ]; + } + + if (!empty($buildsCompute['value'])) { + $metrics[] = [ + 'key' => METRIC_BUILDS_COMPUTE, + 'value' => ($buildsCompute['value'] * -1), + ]; + } + + if (!empty($executions['value'])) { + $metrics[] = [ + 'key' => METRIC_EXECUTIONS, + 'value' => ($executions['value'] * -1), + ]; + } + + if (!empty($executionsCompute['value'])) { + $metrics[] = [ + 'key' => METRIC_EXECUTIONS_COMPUTE, + 'value' => ($executionsCompute['value'] * -1), + ]; + } + break; + default: + break; + } + } catch (\Throwable $e) { + console::error("[reducer] " . " {DateTime::now()} " . " {$project->getInternalId()} " . " {$e->getMessage()}"); + } + } +} diff --git a/src/Appwrite/Platform/Workers/StatsUsageDump.php b/src/Appwrite/Platform/Workers/StatsUsageDump.php new file mode 100644 index 0000000000..5d7240f4b5 --- /dev/null +++ b/src/Appwrite/Platform/Workers/StatsUsageDump.php @@ -0,0 +1,353 @@ + true, + METRIC_BUCKETS => true, + METRIC_USERS => true, + METRIC_FUNCTIONS => true, + METRIC_TEAMS => true, + METRIC_MESSAGES => true, + METRIC_MAU => true, + METRIC_WEBHOOKS => true, + METRIC_PLATFORMS => true, + METRIC_PROVIDERS => true, + METRIC_TOPICS => true, + METRIC_KEYS => true, + METRIC_FILES => true, + METRIC_FILES_STORAGE => true, + METRIC_DEPLOYMENTS_STORAGE => true, + METRIC_BUILDS_STORAGE => true, + METRIC_DEPLOYMENTS => true, + METRIC_BUILDS => true, + METRIC_COLLECTIONS => true, + METRIC_DOCUMENTS => true, + ]; + + /** + * Skip metrics associated with parent IDs + * these need to be checked individually with `str_ends_with` + */ + protected array $skipParentIdMetrics = [ + '.files', + '.files.storage', + '.collections', + '.documents', + '.deployments', + '.deployments.storage', + '.builds', + '.builds.storage', + ]; + + /** + * @var callable + */ + protected mixed $getLogsDB; + + protected array $periods = [ + '1h' => 'Y-m-d H:00', + '1d' => 'Y-m-d 00:00', + 'inf' => '0000-00-00 00:00' + ]; + + public static function getName(): string + { + return 'stats-usage-dump'; + } + + /** + * @throws \Exception + */ + public function __construct() + { + $this + ->inject('message') + ->inject('getProjectDB') + ->inject('getLogsDB') + ->inject('register') + ->callback([$this, 'action']); + } + + /** + * @param Message $message + * @param callable $getProjectDB + * @param callable $getLogsDB + * @param Registry $register + * @return void + * @throws Exception + * @throws \Throwable + * @throws \Utopia\Database\Exception + */ + public function action(Message $message, callable $getProjectDB, callable $getLogsDB, Registry $register): void + { + $this->getLogsDB = $getLogsDB; + $this->register = $register; + $payload = $message->getPayload() ?? []; + if (empty($payload)) { + throw new Exception('Missing payload'); + } + + foreach ($payload['stats'] ?? [] as $stats) { + $project = new Document($stats['project'] ?? []); + + $numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0; + $receivedAt = $stats['receivedAt'] ?? 'NONE'; + if ($numberOfKeys === 0) { + continue; + } + + console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getInternalId(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys); + + try { + /** @var \Utopia\Database\Database $dbForProject */ + $dbForProject = $getProjectDB($project); + foreach ($stats['keys'] ?? [] as $key => $value) { + if ($value == 0) { + continue; + } + + if (str_contains($key, METRIC_DATABASES_STORAGE)) { + try { + $this->handleDatabaseStorage($key, $dbForProject, $project); + } catch (\Exception $e) { + console::error('[' . DateTime::now() . '] failed to calculate database storage for key [' . $key . '] ' . $e->getMessage()); + } + continue; + } + + foreach ($this->periods as $period => $format) { + $time = 'inf' === $period ? null : date($format, time()); + $id = \md5("{$time}_{$period}_{$key}"); + + $document = new Document([ + '$id' => $id, + 'period' => $period, + 'time' => $time, + 'metric' => $key, + 'value' => $value, + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + + $documentClone = new Document($document->getArrayCopy()); + + $dbForProject->createOrUpdateDocumentsWithIncrease( + 'stats', + 'value', + [$document] + ); + + $this->writeToLogsDB($project, $documentClone); + } + } + } catch (\Exception $e) { + console::error('[' . DateTime::now() . '] project [' . $project->getInternalId() . '] database [' . $project['database'] . '] ' . ' ' . $e->getMessage()); + } + } + } + + private function handleDatabaseStorage(string $key, Database $dbForProject, Document $project): void + { + $data = explode('.', $key); + $start = microtime(true); + + $updateMetric = function (Database $dbForProject, Document $project, int $value, string $key, string $period, string|null $time) { + $id = \md5("{$time}_{$period}_{$key}"); + + $document = new Document([ + '$id' => $id, + 'period' => $period, + 'time' => $time, + 'metric' => $key, + 'value' => $value, + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + $documentClone = new Document($document->getArrayCopy()); + $dbForProject->createOrUpdateDocumentsWithIncrease( + 'stats', + 'value', + [$document] + ); + $this->writeToLogsDB($project, $documentClone); + }; + + foreach ($this->periods as $period => $format) { + $time = 'inf' === $period ? null : date($format, time()); + $id = \md5("{$time}_{$period}_{$key}"); + + $value = 0; + $previousValue = 0; + try { + $previousValue = ($dbForProject->getDocument('stats', $id))->getAttribute('value', 0); + } catch (\Exception $e) { + // No previous value + } + + switch (count($data)) { + // Collection Level + case self::METRIC_COLLECTION_LEVEL_STORAGE: + Console::log('[' . DateTime::now() . '] Collection Level Storage Calculation [' . $key . ']'); + $databaseInternalId = $data[0]; + $collectionInternalId = $data[1]; + + try { + $value = $dbForProject->getSizeOfCollection('database_' . $databaseInternalId . '_collection_' . $collectionInternalId); + } catch (\Exception $e) { + // Collection not found + if ($e->getMessage() !== 'Collection not found') { + throw $e; + } + } + + // Compare with previous value + $diff = $value - $previousValue; + + if ($diff === 0) { + break; + } + + // Update Collection + $updateMetric($dbForProject, $project, $diff, $key, $period, $time); + + // Update Database + $databaseKey = str_replace(['{databaseInternalId}'], [$data[0]], METRIC_DATABASE_ID_STORAGE); + $updateMetric($dbForProject, $project, $diff, $databaseKey, $period, $time); + + // Update Project + $projectKey = METRIC_DATABASES_STORAGE; + $updateMetric($dbForProject, $project, $diff, $projectKey, $period, $time); + break; + // Database Level + case self::METRIC_DATABASE_LEVEL_STORAGE: + Console::log('[' . DateTime::now() . '] Database Level Storage Calculation [' . $key . ']'); + $databaseInternalId = $data[0]; + + $collections = []; + try { + $collections = $dbForProject->find('database_' . $databaseInternalId); + } catch (\Exception $e) { + // Database not found + if ($e->getMessage() !== 'Collection not found') { + throw $e; + } + } + + foreach ($collections as $collection) { + try { + $value += $dbForProject->getSizeOfCollection('database_' . $databaseInternalId . '_collection_' . $collection->getInternalId()); + } catch (\Exception $e) { + // Collection not found + if ($e->getMessage() !== 'Collection not found') { + throw $e; + } + } + } + + $diff = $value - $previousValue; + + if ($diff === 0) { + break; + } + + // Update Database + $databaseKey = str_replace(['{databaseInternalId}'], [$data[0]], METRIC_DATABASE_ID_STORAGE); + $updateMetric($dbForProject, $project, $diff, $databaseKey, $period, $time); + + // Update Project + $projectKey = METRIC_DATABASES_STORAGE; + $updateMetric($dbForProject, $project, $diff, $projectKey, $period, $time); + break; + // Project Level + case self::METRIC_PROJECT_LEVEL_STORAGE: + Console::log('[' . DateTime::now() . '] Project Level Storage Calculation [' . $key . ']'); + // Get all project databases + $databases = $dbForProject->find('database'); + + // Recalculate all databases + foreach ($databases as $database) { + $collections = $dbForProject->find('database_' . $database->getInternalId()); + + foreach ($collections as $collection) { + try { + $value += $dbForProject->getSizeOfCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); + } catch (\Exception $e) { + // Collection not found + if ($e->getMessage() !== 'Collection not found') { + throw $e; + } + } + } + } + + $diff = $value - $previousValue; + + // Update Project + $projectKey = METRIC_DATABASES_STORAGE; + $updateMetric($dbForProject, $project, $diff, $projectKey, $period, $time); + break; + } + } + + $end = microtime(true); + + console::log('[' . DateTime::now() . '] DB Storage Calculation [' . $key . '] took ' . (($end - $start) * 1000) . ' milliseconds'); + } + + protected function writeToLogsDB(Document $project, Document $document): void + { + if (!System::getEnv('_APP_STATS_USAGE_DUAL_WRITING', false)) { + Console::log('Dual Writing is disabled. Skipping...'); + return; + } + + if (array_key_exists($document->getAttribute('metric'), $this->skipBaseMetrics)) { + return; + } + foreach ($this->skipParentIdMetrics as $skipMetric) { + if (str_ends_with($document->getAttribute('metric'), $skipMetric)) { + return; + } + } + + /** @var \Utopia\Database\Database $dbForLogs*/ + $dbForLogs = call_user_func($this->getLogsDB, $project); + + try { + $dbForLogs->createOrUpdateDocumentsWithIncrease( + 'stats', + 'value', + [$document] + ); + Console::success('Usage logs pushed to Logs DB'); + } catch (\Throwable $th) { + Console::error($th->getMessage()); + } + + $this->register->get('pools')->get('logs')->reclaim(); + } +} diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php index a76e4f17b0..c903dafdae 100644 --- a/src/Appwrite/Platform/Workers/Webhooks.php +++ b/src/Appwrite/Platform/Workers/Webhooks.php @@ -3,7 +3,7 @@ namespace Appwrite\Platform\Workers; use Appwrite\Event\Mail; -use Appwrite\Event\Usage; +use Appwrite\Event\StatsUsage; use Appwrite\Template\Template; use Exception; use Utopia\Database\Database; @@ -35,9 +35,9 @@ class Webhooks extends Action ->inject('project') ->inject('dbForPlatform') ->inject('queueForMails') - ->inject('queueForUsage') + ->inject('queueForStatsUsage') ->inject('log') - ->callback(fn (Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, Usage $queueForUsage, Log $log) => $this->action($message, $project, $dbForPlatform, $queueForMails, $queueForUsage, $log)); + ->callback(fn (Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, StatsUsage $queueForStatsUsage, Log $log) => $this->action($message, $project, $dbForPlatform, $queueForMails, $queueForStatsUsage, $log)); } /** @@ -49,7 +49,7 @@ class Webhooks extends Action * @return void * @throws Exception */ - public function action(Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, Usage $queueForUsage, Log $log): void + public function action(Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, StatsUsage $queueForStatsUsage, Log $log): void { $this->errors = []; $payload = $message->getPayload() ?? []; @@ -66,7 +66,7 @@ class Webhooks extends Action foreach ($project->getAttribute('webhooks', []) as $webhook) { if (array_intersect($webhook->getAttribute('events', []), $events)) { - $this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $queueForUsage); + $this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $queueForStatsUsage); } } @@ -85,7 +85,7 @@ class Webhooks extends Action * @param Mail $queueForMails * @return void */ - private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Mail $queueForMails, Usage $queueForUsage): void + private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Mail $queueForMails, StatsUsage $queueForStatsUsage): void { if ($webhook->getAttribute('enabled') !== true) { return; @@ -168,7 +168,7 @@ class Webhooks extends Action $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $this->errors[] = $logs; - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_WEBHOOKS_FAILED, 1) ->addMetric(str_replace('{webhookInternalId}', $webhook->getInternalId(), METRIC_WEBHOOK_ID_FAILED), 1) ; @@ -178,13 +178,13 @@ class Webhooks extends Action $webhook->setAttribute('attempts', 0); // Reset attempts on success $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); - $queueForUsage + $queueForStatsUsage ->addMetric(METRIC_WEBHOOKS_SENT, 1) ->addMetric(str_replace('{webhookInternalId}', $webhook->getInternalId(), METRIC_WEBHOOK_ID_SENT), 1) ; } - $queueForUsage + $queueForStatsUsage ->setProject($project) ->trigger(); } diff --git a/src/Appwrite/Specification/Format.php b/src/Appwrite/Specification/Format.php index 038f5369f5..b4d2270eef 100644 --- a/src/Appwrite/Specification/Format.php +++ b/src/Appwrite/Specification/Format.php @@ -113,6 +113,18 @@ abstract class Format protected function getEnumName(string $service, string $method, string $param): ?string { switch ($service) { + case 'console': + switch ($method) { + case 'getResource': + switch ($param) { + case 'type': + return 'ConsoleResourceType'; + case 'value': + return 'ConsoleResourceValue'; + } + break; + } + break; case 'account': switch ($method) { case 'createOAuth2Session': @@ -198,8 +210,24 @@ abstract class Format break; } break; + case 'functions': + switch ($method) { + case 'getDeploymentDownload': + switch ($param) { + case 'type': + return 'DeploymentDownloadType'; + } + break; + } + break; case 'sites': switch ($method) { + case 'getDeploymentDownload': + switch ($param) { + case 'type': + return 'DeploymentDownloadType'; + } + break; case 'getUsage': case 'listUsage': switch ($param) { diff --git a/src/Appwrite/Transformation/Adapter.php b/src/Appwrite/Transformation/Adapter.php new file mode 100644 index 0000000000..b4b5dce8a8 --- /dev/null +++ b/src/Appwrite/Transformation/Adapter.php @@ -0,0 +1,32 @@ +input = $input; + return $this; + } + + public function getOutput(): mixed + { + return $this->output; + } + + /** + * @param array $traits + */ + abstract public function isValid(array $traits): bool; + + abstract public function transform(): void; +} diff --git a/src/Appwrite/Transformation/Adapter/Mock.php b/src/Appwrite/Transformation/Adapter/Mock.php new file mode 100644 index 0000000000..13ec1cc88b --- /dev/null +++ b/src/Appwrite/Transformation/Adapter/Mock.php @@ -0,0 +1,26 @@ + $traits Mock traits + */ + public function isValid(array $traits): bool + { + if ($traits['mock'] === true) { + return true; + } + + return false; + } + + public function transform(): void + { + $this->output = $this->input; + $this->output = "Mock: " . $this->output; + } +} diff --git a/src/Appwrite/Transformation/Adapter/Preview.php b/src/Appwrite/Transformation/Adapter/Preview.php new file mode 100644 index 0000000000..94f1454593 --- /dev/null +++ b/src/Appwrite/Transformation/Adapter/Preview.php @@ -0,0 +1,201 @@ + $traits Proxied response headers + */ + public function isValid(array $traits): bool + { + $contentType = ''; + + foreach ($traits as $key => $value) { + if (\strtolower($key) === 'content-type') { + $contentType = $value; + break; + } + } + + if (\str_contains($contentType, 'text/html')) { + return true; + } + + return false; + } + + public function transform(): void + { + $this->output = $this->input; + + $banner = << + #appwrite-preview { + padding: 0; + margin: 0; + position: fixed; + right: 16px; + bottom: 16px; + z-index: 1; + border-radius: var(--border-radius-S, 8px); + border: var(--border-width-S, 1px) solid var(--color-border-neutral, #EDEDF0); + background: var(--color-bgColor-neutral-primary, #FFF); + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.03), 0px 4px 4px 0px rgba(0, 0, 0, 0.04); + padding: var(--space-3, 6px) var(--space-4, 8px); + display: flex; + justify-content: center; + align-items: center; + gap: var(--gap-XXS, 4px); + cursor: pointer; + transition: opacity 0.3s; + } + + #appwrite-preview-close { + position: absolute; + right: 0px; + bottom: 0px; + border-radius: var(--border-radius-S, 8px); + background: linear-gradient(270deg, #FFF 69.64%, rgba(255, 255, 255, 0.00) 114.29%); + height: 100%; + aspect-ratio: 1 / 1; + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + transition: opacity 0.3s; + } + + #appwrite-preview-logo-dark { + display: none; + } + + #appwrite-preview:hover #appwrite-preview-close { + opacity: 1; + } + + #appwrite-preview-text { + padding: 0; + margin: 0; + color: var(--color-fgColor-neutral-secondary, #56565C); + font-family: var(--font-family-sansSerif, Inter); + font-size: var(--font-size-XS, 12px); + font-style: normal; + font-weight: 500; + line-height: 130%; + letter-spacing: -0.12px; + } + + #appwrite-preview-close-text { + opacity: 0; + transition: opacity 0.3s; + position: absolute; + bottom: calc(15px + 4px); + display: flex; + padding: var(--space-1, 2px) var(--space-2, 4px); + color: var(--color-fgColor-neutral-secondary, #56565C); + text-align: center; + font-family: var(--font-family-sansSerif, Inter); + font-size: var(--font-size-XS, 12px); + font-style: normal; + font-weight: 400; + line-height: 130%; + letter-spacing: -0.12px; + border-radius: var(--border-radius-XS, 6px); + background: #EDEDF0; + } + + #appwrite-preview-close:hover #appwrite-preview-close-text { + opacity: 1; + } + + @media (prefers-color-scheme: dark) { + #appwrite-preview { + border: var(--border-width-S, 1px) solid var(--color-border-neutral, #2D2D31); + background: var(--color-bgColor-neutral-primary, #1D1D21); + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.03), 0px 4px 4px 0px rgba(0, 0, 0, 0.04); + } + + #appwrite-preview-text { + color: var(--color-fgColor-neutral-secondary, #C3C3C6); + font-family: var(--font-family-sansSerif, Inter); + font-size: var(--font-size-XS, 12px); + } + + #appwrite-preview-logo-dark { + display: block; + } + + #appwrite-preview-logo-light { + display: none; + } + + #appwrite-preview-close { + background: linear-gradient(270deg, #1D1D21 69.64%, rgba(29, 29, 33, 0.00) 114.29%); + } + + #appwrite-preview-close-text { + background: #2D2D31; + color: var(--color-fgColor-neutral-secondary, #C3C3C6); + } + } + + + + + + EOT; + + $this->output .= $banner; + } +} diff --git a/src/Appwrite/Transformation/Transformation.php b/src/Appwrite/Transformation/Transformation.php new file mode 100644 index 0000000000..d01ee08179 --- /dev/null +++ b/src/Appwrite/Transformation/Transformation.php @@ -0,0 +1,74 @@ + $adapters + */ + protected array $adapters; + + /** + * @var array $traits + */ + protected array $traits; + + protected mixed $input; + protected mixed $output; + + /** + * @param array $adapters + */ + public function __construct(array $adapters = []) + { + $this->adapters = $adapters; + } + + /** + * @param array $traits + */ + public function setTraits(array $traits): self + { + $this->traits = $traits; + return $this; + } + + public function setInput(mixed $input): self + { + $this->input = $input; + return $this; + } + + public function addAdapter(Adapter $adapter): self + { + $this->adapters[] = $adapter; + return $this; + } + + public function transform(): bool + { + foreach ($this->adapters as $adapter) { + if (!$adapter->isValid($this->traits)) { + return false; + } + } + + $output = $this->input; + + foreach ($this->adapters as $adapter) { + $adapter->setInput($output); + $adapter->transform(); + $output = $adapter->getOutput(); + } + + $this->output = $output; + + return true; + } + + public function getOutput(): mixed + { + return $this->output; + } +} diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Rules.php b/src/Appwrite/Utopia/Database/Validator/Queries/Rules.php index 24cb4475f2..61701f0b2c 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Rules.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Rules.php @@ -6,8 +6,9 @@ class Rules extends Base { public const ALLOWED_ATTRIBUTES = [ 'domain', - 'resourceType', - 'resourceId', + 'type', + 'value', + 'automation', 'url' ]; diff --git a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php index 59d22296d1..2c1688969d 100644 --- a/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php +++ b/src/Appwrite/Utopia/Response/Model/ConsoleVariables.php @@ -66,6 +66,15 @@ class ConsoleVariables extends Model 'default' => '', 'example' => 'enabled', ] + ) + ->addRule( + '_APP_DOMAINS_NAMESERVERS', + [ + 'type' => self::TYPE_STRING, + 'description' => 'Comma-separated list of nameservers.', + 'default' => '', + 'example' => 'ns1.example.com,ns2.example.com', + ] ); } diff --git a/src/Appwrite/Utopia/Response/Model/Deployment.php b/src/Appwrite/Utopia/Response/Model/Deployment.php index 0e49c82f82..f42ce6d334 100644 --- a/src/Appwrite/Utopia/Response/Model/Deployment.php +++ b/src/Appwrite/Utopia/Response/Model/Deployment.php @@ -76,6 +76,18 @@ class Deployment extends Model 'default' => false, 'example' => true, ]) + ->addRule('screenshotLight', [ + 'type' => self::TYPE_STRING, + 'description' => 'Screenshot with light theme preference file ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('screenshotDark', [ + 'type' => self::TYPE_STRING, + 'description' => 'Screenshot with dark theme preference file ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) ->addRule('status', [ 'type' => self::TYPE_STRING, 'description' => 'The deployment status. Possible values are "processing", "building", "waiting", "ready", and "failed".', @@ -94,12 +106,6 @@ class Deployment extends Model 'default' => 0, 'example' => 128, ]) - ->addRule('domain', [ - 'type' => self::TYPE_STRING, - 'description' => 'Preview domain.', - 'default' => '', - 'example' => 'deploy1-project1.appwrite.site', - ]) ->addRule('providerRepositoryName', [ 'type' => self::TYPE_STRING, 'description' => 'The name of the vcs provider repository', diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 6e01baee84..fbbe062531 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -323,9 +323,9 @@ class Project extends Model } /** - * Get Collection + * Filter document structure * - * @return string + * @return Document */ public function filter(Document $document): Document { diff --git a/src/Appwrite/Utopia/Response/Model/Rule.php b/src/Appwrite/Utopia/Response/Model/Rule.php index 932591b90f..c365f241f8 100644 --- a/src/Appwrite/Utopia/Response/Model/Rule.php +++ b/src/Appwrite/Utopia/Response/Model/Rule.php @@ -34,17 +34,24 @@ class Rule extends Model 'default' => '', 'example' => 'appwrite.company.com', ]) - ->addRule('resourceType', [ + ->addRule('type', [ 'type' => self::TYPE_STRING, - 'description' => 'Action definition for the rule. Possible values are "api", "function", or "redirect"', + 'description' => 'Action definition for the rule. Possible values are "api", "deployment", or "redirect"', 'default' => '', - 'example' => 'function', + 'example' => 'deployment', ]) - ->addRule('resourceId', [ + ->addRule('value', [ 'type' => self::TYPE_STRING, - 'description' => 'ID of resource for the action type. If resourceType is "api" or "url", it is empty. If resourceType is "function", it is ID of the function.', + 'description' => 'Detail specification for the type. If type is "api", this is empty. If type is "redirect", this is URL. If type is "deployment", this is deployment ID.', 'default' => '', - 'example' => 'myAwesomeFunction', + 'example' => '67a9cf1a00150ee93abd', + ]) + ->addRule('automation', [ + 'type' => self::TYPE_STRING, + 'description' => 'Action that results in a rule update. If VCS branch, value can be of syntax "branch=[name]"', + 'array' => false, + 'default' => '', + 'example' => 'branch=dev', ]) ->addRule('status', [ 'type' => self::TYPE_STRING, diff --git a/src/Appwrite/Utopia/Response/Model/TemplateSite.php b/src/Appwrite/Utopia/Response/Model/TemplateSite.php index b9fe4704c7..fe123bde53 100644 --- a/src/Appwrite/Utopia/Response/Model/TemplateSite.php +++ b/src/Appwrite/Utopia/Response/Model/TemplateSite.php @@ -28,11 +28,17 @@ class TemplateSite extends Model 'default' => '', 'example' => 'https://nextjs-starter.appwrite.network/', ]) - ->addRule('demoImage', [ + ->addRule('screenshotDark', [ 'type' => self::TYPE_STRING, - 'description' => 'File URL with preview screenshot.', + 'description' => 'File URL with preview screenshot in dark theme preference.', 'default' => '', - 'example' => 'https://cloud.appwrite.io/console/images/sites/templates/nextjs-starter.png', + 'example' => 'https://cloud.appwrite.io/images/sites/templates/template-for-blog-dark.png', + ]) + ->addRule('screenshotLight', [ + 'type' => self::TYPE_STRING, + 'description' => 'File URL with preview screenshot in light theme preference.', + 'default' => '', + 'example' => 'https://cloud.appwrite.io/images/sites/templates/template-for-blog-light.png', ]) ->addRule('useCases', [ 'type' => self::TYPE_STRING, diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 74ae1c00bc..00dc790869 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -12,10 +12,12 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; use Tests\E2E\Services\Functions\FunctionsBase; +use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\System\System; class UsageTest extends Scope { @@ -52,6 +54,13 @@ class UsageTest extends Scope } } + public static function getYesterday(): string + { + $date = new DateTime(); + $date->modify('-1 day'); + return $date->format(self::$formatTz); + } + public static function getToday(): string { $date = new DateTime(); @@ -1090,22 +1099,24 @@ class UsageTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), + $rule = $this->client->call( + Client::METHOD_POST, + '/proxy/rules/function', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), + [ + 'domain' => 'test-' . ID::unique() . System::getEnv('_APP_DOMAIN_FUNCTIONS'), + 'functionId' => $functionId, ], - ]); + ); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertNotEmpty($rule['body']['$id']); + $this->assertNotEmpty($rule['body']['domain']); - $domain = $rules['body']['rules'][0]['domain']; + $domain = $rule['body']['domain']; $response = $this->client->call( Client::METHOD_GET, diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index f2617bd4be..f8f779027b 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -82,6 +82,8 @@ trait ProjectCustom 'sites.write', 'execution.read', 'execution.write', + 'log.read', + 'log.write', 'locale.read', 'avatars.read', 'health.read', diff --git a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php index fb65adc299..f70eafc8e0 100644 --- a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php +++ b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php @@ -24,7 +24,7 @@ class ConsoleConsoleClientTest extends Scope ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(9, $response['body']); + $this->assertCount(10, $response['body']); $this->assertIsString($response['body']['_APP_DOMAIN_TARGET']); $this->assertIsInt($response['body']['_APP_STORAGE_LIMIT']); $this->assertIsInt($response['body']['_APP_COMPUTE_SIZE_LIMIT']); @@ -34,5 +34,7 @@ class ConsoleConsoleClientTest extends Scope $this->assertIsBool($response['body']['_APP_ASSISTANT_ENABLED']); $this->assertIsString($response['body']['_APP_DOMAIN_SITES']); $this->assertIsString($response['body']['_APP_OPTIONS_FORCE_HTTPS']); + $this->assertIsString($response['body']['_APP_DOMAINS_NAMESERVERS']); + // When adding new keys, dont forget to update count a few lines above } } diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index d079cb313c..0f57f94515 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -399,6 +399,73 @@ trait DatabasesBase $this->assertEquals(400, $response['headers']['status-code']); } + /** + * @depends testCreateDatabase + */ + public function testPatchAttribute(array $data): void + { + $databaseId = $data['databaseId']; + + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::unique(), + 'name' => 'patch', + 'documentSecurity' => true, + 'permissions' => [ + Permission::create(Role::user($this->getUser()['$id'])), + ], + ]); + + $this->assertEquals(201, $collection['headers']['status-code']); + $this->assertEquals($collection['body']['name'], 'patch'); + + $attribute = $this->client->call(Client::METHOD_POST, '/databases/'.$databaseId.'/collections/'.$collection['body']['$id'].'/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'required' => true, + 'size' => 100, + ]); + $this->assertEquals(202, $attribute['headers']['status-code']); + $this->assertEquals($attribute['body']['size'], 100); + + sleep(1); + + $index = $this->client->call(Client::METHOD_POST, '/databases/'.$databaseId.'/collections/'.$collection['body']['$id'].'/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'titleIndex', + 'type' => 'key', + 'attributes' => ['title'], + ]); + $this->assertEquals(202, $index['headers']['status-code']); + + sleep(1); + + /** + * Update attribute size to exceed Index maximum length + */ + $attribute = $this->client->call(Client::METHOD_PATCH, '/databases/'.$databaseId.'/collections/'.$collection['body']['$id'].'/attributes/string/'.$attribute['body']['key'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'size' => 1000, + 'required' => true, + 'default' => null, + ]); + + $this->assertEquals(400, $attribute['headers']['status-code']); + $this->assertStringContainsString('Index length is longer than the maximum: 76', $attribute['body']['message']); + } + public function testUpdateAttributeEnum(): void { $database = $this->client->call(Client::METHOD_POST, '/databases', [ @@ -1300,7 +1367,7 @@ trait DatabasesBase ]); $this->assertEquals(400, $unknown['headers']['status-code']); - $this->assertEquals('Unknown attribute: Unknown', $unknown['body']['message']); + $this->assertEquals('Unknown attribute: Unknown. Verify the attribute name or create the attribute.', $unknown['body']['message']); $index1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index a1bb8f2b21..9f0a5903f3 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -6,6 +6,9 @@ use Appwrite\Tests\Async; use CURLFile; use Tests\E2E\Client; use Utopia\CLI\Console; +use Utopia\Database\Helpers\ID; +use Utopia\Database\Query; +use Utopia\System\System; trait FunctionsBase { @@ -48,6 +51,18 @@ trait FunctionsBase $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); }, 50000, 500); + // Not === so multipart/form-data works fine too + if (($params['activate'] ?? false) == true) { + $this->assertEventually(function () use ($functionId, $deploymentId) { + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $function['body']['deployment'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + } + return $deploymentId; } @@ -72,6 +87,16 @@ trait FunctionsBase return $function; } + protected function updateFunction(string $functionId, mixed $params): mixed + { + $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $function; + } + protected function createVariable(string $functionId, mixed $params): mixed { $variable = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([ @@ -82,6 +107,46 @@ trait FunctionsBase return $variable; } + protected function getVariable(string $functionId, string $variableId): mixed + { + $variable = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $variable; + } + + protected function updateVariable(string $functionId, string $variableId, mixed $params): mixed + { + $variable = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $variable; + } + + protected function listVariables(string $functionId, mixed $params = []): mixed + { + $variables = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $variables; + } + + protected function deleteVariable(string $functionId, string $variableId): mixed + { + $variable = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $variable; + } + protected function getFunction(string $functionId): mixed { $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ @@ -166,6 +231,16 @@ trait FunctionsBase return $deployment; } + protected function createTemplateDeployment(string $functionId, mixed $params = []): mixed + { + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments/template', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $deployment; + } + protected function getFunctionUsage(string $functionId, mixed $params): mixed { $usage = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/usage', array_merge([ @@ -205,4 +280,113 @@ trait FunctionsBase return $function; } + + protected function setupFunctionDomain(string $functionId, string $subdomain = ''): string + { + $subdomain = $subdomain ? $subdomain : ID::unique(); + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + 'functionId' => $functionId, + ]); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertNotEmpty($rule['body']['$id']); + $this->assertNotEmpty($rule['body']['domain']); + + $domain = $rule['body']['domain']; + + return $domain; + } + + protected function getFunctionDomain(string $functionId): string + { + $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::equal('automation', ['function=' . $functionId])->toString(), + Query::equal('type', ['deployment'])->toString(), + ], + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertGreaterThanOrEqual(1, $rules['body']['total']); + $this->assertGreaterThanOrEqual(1, \count($rules['body']['rules'])); + $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + + $domain = $rules['body']['rules'][0]['domain']; + + return $domain; + } + + protected function getDeploymentDownload(string $functionId, string $deploymentId, string $type): mixed + { + $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId . '/download', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => $type + ]); + + return $response; + } + + protected function setupDuplicateDeployment(string $functionId, string $deploymentId): string + { + $deployment = $this->createDuplicateDeployment($functionId, $deploymentId); + $this->assertEquals(202, $deployment['headers']['status-code']); + + $deploymentId = $deployment['body']['$id']; + $this->assertNotEmpty($deploymentId); + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $function = $this->getFunction($functionId); + $this->assertEquals($deploymentId, $function['body']['deployment'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + return $deploymentId; + } + + protected function createDuplicateDeployment(string $functionId, string $deploymentId): mixed + { + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments/duplicate', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'deploymentId' => $deploymentId, + ]); + + return $deployment; + } + + protected function updateFunctionDeployment(string $functionId, string $deploymentId): mixed + { + $function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployment', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'deploymentId' => $deploymentId + ]); + + return $function; + } + + protected function cancelDeployment(string $functionId, string $deploymentId): mixed + { + $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId . '/status', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $deployment; + } } diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index 0708d40aab..300090b156 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -110,7 +110,8 @@ class FunctionsConsoleClientTest extends Scope $data['functionId'], [ 'key' => 'APP_TEST', - 'value' => 'TESTINGVALUE' + 'value' => 'TESTINGVALUE', + 'secret' => false ] ); @@ -131,6 +132,7 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals(201, $variable['headers']['status-code']); $this->assertEquals('APP_TEST_1', $variable['body']['key']); $this->assertEmpty($variable['body']['value']); + $this->assertTrue($variable['body']['secret']); $secretVariableId = $variable['body']['$id']; @@ -142,7 +144,8 @@ class FunctionsConsoleClientTest extends Scope $data['functionId'], [ 'key' => 'APP_TEST', - 'value' => 'ANOTHERTESTINGVALUE' + 'value' => 'ANOTHERTESTINGVALUE', + 'secret' => false ] ); @@ -320,6 +323,41 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals("APP_TEST_UPDATE_2", $variable['body']['key']); $this->assertEquals("TESTINGVALUEUPDATED", $variable['body']['value']); + // convert non-secret variable to secret + $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'] . '/variables/' . $data['variableId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'key' => 'APP_TEST_UPDATE_2', + 'secret' => true + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals("APP_TEST_UPDATE_2", $response['body']['key']); + $this->assertEmpty($response['body']['value']); + $this->assertTrue($response['body']['secret']); + + $variable = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables/' . $data['variableId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $variable['headers']['status-code']); + $this->assertEquals("APP_TEST_UPDATE_2", $variable['body']['key']); + $this->assertEmpty($variable['body']['value']); + $this->assertTrue($variable['body']['secret']); + + // convert secret variable to non-secret + $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'] . '/variables/' . $data['variableId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'key' => 'APP_TEST_UPDATE', + 'secret' => false + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + /** * Test for FAILURE */ @@ -410,4 +448,93 @@ class FunctionsConsoleClientTest extends Scope return $data; } + + public function testVariableE2E(): void + { + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'runtime' => 'node-18.0', + 'name' => 'Variable E2E Test', + 'entrypoint' => 'index.js', + 'logging' => false, + 'execute' => ['any'] + ]); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertFalse($function['body']['logging']); + $this->assertNotEmpty($function['body']['$id']); + + $functionId = $function['body']['$id'] ?? ''; + + // create variable + $variable = $this->createVariable($functionId, [ + 'key' => 'CUSTOM_VARIABLE', + 'value' => 'a_secret_value', + 'secret' => true, + ]); + + $this->assertEquals(201, $variable['headers']['status-code']); + $this->assertNotEmpty($variable['body']['$id']); + $this->assertEquals('CUSTOM_VARIABLE', $variable['body']['key']); + $this->assertEquals('', $variable['body']['value']); + $this->assertEquals(true, $variable['body']['secret']); + + $deploymentId = $this->setupDeployment($functionId, [ + 'entrypoint' => 'index.js', + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + + $this->assertNotEmpty($deploymentId); + + $execution = $this->createExecution($functionId); + + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertEmpty($execution['body']['logs']); + $this->assertEmpty($execution['body']['errors']); + $body = json_decode($execution['body']['responseBody']); + $this->assertEquals('a_secret_value', $body->CUSTOM_VARIABLE); + + $this->cleanupFunction($functionId); + } + + public function testFunctionDownload(): void + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'runtime' => 'node-18.0', + 'name' => 'Download Test', + 'entrypoint' => 'index.js', + 'logging' => false, + 'execute' => ['any'] + ]); + + $deploymentId = $this->setupDeployment($functionId, [ + 'entrypoint' => 'index.js', + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + + $this->assertNotEmpty($deploymentId); + + $response = $this->getDeploymentDownload($functionId, $deploymentId, 'source'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('application/gzip', $response['headers']['content-type']); + $this->assertGreaterThan(0, $response['headers']['content-length']); + $this->assertGreaterThan(0, \strlen($response['body'])); + + $deploymentMd5 = \md5($response['body']); + + $response = $this->getDeploymentDownload($functionId, $deploymentId, 'output'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('application/gzip', $response['headers']['content-type']); + $this->assertGreaterThan(0, $response['headers']['content-length']); + $this->assertGreaterThan(0, \strlen($response['body'])); + + $buildMd5 = \md5($response['body']); + + $this->assertNotEquals($deploymentMd5, $buildMd5); + + $this->cleanupFunction($functionId); + } } diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 914a255663..9d8ce5a4ee 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -276,7 +276,7 @@ class FunctionsCustomClientTest extends Scope */ // List all templates $templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([ - 'content-type' => 'application/json', + 'content-type' => 'application/json' ], $this->getHeaders())); $this->assertEquals(200, $templates['headers']['status-code']); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 712d6d3948..2056d1f689 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -3,6 +3,7 @@ namespace Tests\E2E\Services\Functions; use Appwrite\Functions\Specification; +use Appwrite\Tests\Retry; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; @@ -38,7 +39,7 @@ class FunctionsCustomServerTest extends Scope 'timeout' => 10, ]); - $functionId = $functionId = $function['body']['$id'] ?? ''; + $functionId = $function['body']['$id'] ?? ''; $dateValidator = new DatetimeValidator(); $this->assertEquals(201, $function['headers']['status-code']); @@ -356,7 +357,22 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(201, $function['headers']['status-code']); $this->assertNotEmpty($function['body']['$id']); - $functionId = $functionId = $function['body']['$id'] ?? ''; + $functionId = $function['body']['$id'] ?? ''; + + $deployment = $this->createTemplateDeployment( + $functionId, + [ + 'functionId' => ID::unique(), + 'activate' => true, + 'repository' => $starterTemplate['body']['providerRepositoryId'], + 'owner' => $starterTemplate['body']['providerOwner'], + 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'version' => $starterTemplate['body']['providerVersion'], + ] + ); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); $deployments = $this->listDeployments($functionId); @@ -496,6 +512,7 @@ class FunctionsCustomServerTest extends Scope /** * @depends testUpdateFunction */ + #[Retry(count: 3)] public function testCancelDeploymentBuild($data): void { $functionId = $data['functionId']; @@ -519,14 +536,9 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals('building', $deployment['body']['status']); }, 100000, 250); - // Cancel the deployment - $cancel = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId . '/build', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(200, $cancel['headers']['status-code']); - $this->assertEquals('canceled', $cancel['body']['status']); + $deployment = $this->cancelDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('canceled', $deployment['body']['status']); /** * Build worker still runs the build. @@ -1090,8 +1102,8 @@ class FunctionsCustomServerTest extends Scope $output = json_decode($execution['body']['responseBody'], true); - $this->assertEquals(1, $output['APPWRITE_COMPUTE_CPUS']); - $this->assertEquals(1024, $output['APPWRITE_COMPUTE_MEMORY']); + $this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']); + $this->assertEquals(1024, $output['APPWRITE_FUNCTION_MEMORY']); // Change the specs to 1vcpu 512mb $function = $this->client->call(Client::METHOD_PUT, '/functions/' . $data['functionId'], array_merge([ @@ -1118,8 +1130,8 @@ class FunctionsCustomServerTest extends Scope $output = json_decode($execution['body']['responseBody'], true); - $this->assertEquals(1, $output['APPWRITE_COMPUTE_CPUS']); - $this->assertEquals(512, $output['APPWRITE_COMPUTE_MEMORY']); + $this->assertEquals(1, $output['APPWRITE_FUNCTION_CPUS']); + $this->assertEquals(512, $output['APPWRITE_FUNCTION_MEMORY']); /** * Test for FAILURE @@ -1639,22 +1651,7 @@ class FunctionsCustomServerTest extends Scope 'execute' => ['any'] ]); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), - ], - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $domain = $this->setupFunctionDomain($functionId); $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', @@ -1715,22 +1712,7 @@ class FunctionsCustomServerTest extends Scope 'execute' => ['any'] ]); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), - ], - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $domain = $this->setupFunctionDomain($functionId); $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', @@ -1765,22 +1747,7 @@ class FunctionsCustomServerTest extends Scope 'execute' => ['any'] ]); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), - ], - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $domain = $this->setupFunctionDomain($functionId); $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', @@ -1898,7 +1865,9 @@ class FunctionsCustomServerTest extends Scope $this->assertFalse($function['body']['logging']); $this->assertNotEmpty($function['body']['$id']); - $functionId = $functionId = $function['body']['$id'] ?? ''; + $functionId = $function['body']['$id'] ?? ''; + + $domain = $this->setupFunctionDomain($functionId); $this->setupDeployment($functionId, [ 'code' => $this->packageFunction('node'), @@ -1934,20 +1903,7 @@ class FunctionsCustomServerTest extends Scope }, 10000, 500); // Domain Executions test - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), - ], - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $domain = $this->getFunctionDomain($functionId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); @@ -1985,4 +1941,144 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } + + public function testFunctionSpecifications() + { + // Check if the function specifications are correctly set in builds + $function = $this->createFunction([ + 'functionId' => ID::unique(), + 'runtime' => 'node-18.0', + 'name' => 'Specification Test', + 'entrypoint' => 'index.js', + 'logging' => false, + 'execute' => ['any'], + 'specification' => Specification::S_2VCPU_2GB, + 'commands' => 'echo $APPWRITE_FUNCTION_MEMORY:$APPWRITE_FUNCTION_CPUS', + ]); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertEquals(Specification::S_2VCPU_2GB, $function['body']['specification']); + $this->assertNotEmpty($function['body']['$id']); + + $functionId = $functionId = $function['body']['$id'] ?? ''; + + $deploymentId = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertTrue(str_contains($deployment['body']['buildLogs'], '2048:2')); + }, 10000, 500); + + // Check if the function specifications are correctly set in executions + $execution = $this->createExecution($functionId); + + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertNotEmpty($execution['body']['$id']); + + $executionResponse = json_decode($execution['body']['responseBody'], true); + $this->assertEquals('2048', $executionResponse['APPWRITE_FUNCTION_MEMORY']); + $this->assertEquals('2', $executionResponse['APPWRITE_FUNCTION_CPUS']); + + $this->cleanupFunction($functionId); + } + + public function testDuplicateDeployment(): void + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'runtime' => 'node-18.0', + 'name' => 'Duplicate Deployment Test', + 'entrypoint' => 'index.js', + 'commands' => '' + ]); + $this->assertNotEmpty($functionId); + + $deploymentId1 = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('node'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentId1); + + $execution = $this->createExecution($functionId); + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertStringContainsString('APPWRITE_FUNCTION_ID', $execution['body']['responseBody']); + + $site = $this->updateFunction($functionId, [ + 'runtime' => 'node-18.0', + 'name' => 'Duplicate Deployment Test', + 'entrypoint' => 'index.js', + 'commands' => 'rm index.js && mv maintenance.js index.js' + ]); + $this->assertEquals(200, $site['headers']['status-code']); + $this->assertStringContainsString('maintenance.js', $site['body']['commands']); + + $deploymentId2 = $this->setupDuplicateDeployment($functionId, $deploymentId1); + $this->assertNotEmpty($deploymentId2); + + $execution = $this->createExecution($functionId); + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertStringContainsString('Maintenance', $execution['body']['responseBody']); + + $this->cleanupFunction($functionId); + } + + public function testUpdateDeploymentStatus(): void + { + + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'runtime' => 'php-8.0', + 'name' => 'Re-activate Test', + 'entrypoint' => 'index.php', + ]); + $this->assertNotEmpty($functionId); + + $deploymentId1 = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('php-cookie'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentId1); + + $execution = $this->createExecution($functionId, [ + 'headers' => [ 'cookie' => 'cookieName=cookieValue' ] + ]); + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertNotEmpty($execution['body']['$id']); + $this->assertStringContainsString('cookieValue', $execution['body']['responseBody']); + + $deploymentId2 = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('php'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentId2); + + $execution = $this->createExecution($functionId); + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertNotEmpty($execution['body']['$id']); + $this->assertStringContainsString('UNICODE_TEST', $execution['body']['responseBody']); + + $function = $this->getFunction($functionId); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($deploymentId2, $function['body']['deployment']); + + $function = $this->updateFunctionDeployment($functionId, $deploymentId1); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($deploymentId1, $function['body']['deployment']); + + $function = $this->getFunction($functionId); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($deploymentId1, $function['body']['deployment']); + + $execution = $this->createExecution($functionId, [ + 'headers' => [ 'cookie' => 'cookieName=cookieValue' ] + ]); + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertNotEmpty($execution['body']['$id']); + $this->assertStringContainsString('cookieValue', $execution['body']['responseBody']); + + $this->cleanupFunction($functionId); + } } diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 0b3250cecf..5c38a10472 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -1620,7 +1620,7 @@ trait Base }'; case self::$RETRY_BUILD: return 'mutation retryBuild($functionId: String!, $deploymentId: String!, $buildId: String!) { - functionsCreateBuild(functionId: $functionId, deploymentId: $deploymentId, buildId: $buildId) { + functionsCreateDuplicateDeployment(functionId: $functionId, deploymentId: $deploymentId, buildId: $buildId) { status } }'; diff --git a/tests/e2e/Services/GraphQL/FunctionsClientTest.php b/tests/e2e/Services/GraphQL/FunctionsClientTest.php index 0b43ed5772..14b714d551 100644 --- a/tests/e2e/Services/GraphQL/FunctionsClientTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsClientTest.php @@ -133,7 +133,7 @@ class FunctionsClientTest extends Scope $deployment = $deployment['body']['data']['functionsGetDeployment']; $this->assertEquals('ready', $deployment['status']); - }); + }, 60000); return $deployment; } diff --git a/tests/e2e/Services/GraphQL/FunctionsServerTest.php b/tests/e2e/Services/GraphQL/FunctionsServerTest.php index e49ac43619..e45e75228b 100644 --- a/tests/e2e/Services/GraphQL/FunctionsServerTest.php +++ b/tests/e2e/Services/GraphQL/FunctionsServerTest.php @@ -130,7 +130,7 @@ class FunctionsServerTest extends Scope $deployment = $deployment['body']['data']['functionsGetDeployment']; $this->assertEquals('ready', $deployment['status']); - }); + }, 30000); return $deployment; } @@ -186,8 +186,8 @@ class FunctionsServerTest extends Scope 'x-appwrite-project' => $projectId, ], $this->getHeaders()), $gqlPayload); - $this->assertIsNotArray($response['body']); - $this->assertEquals(204, $response['headers']['status-code']); + $this->assertIsArray($response['body']['data']); + $this->assertEquals(200, $response['headers']['status-code']); } public function testGetFunctions(): array diff --git a/tests/e2e/Services/Health/HealthCustomServerTest.php b/tests/e2e/Services/Health/HealthCustomServerTest.php index f2c6a2e5c2..04b1408cd0 100644 --- a/tests/e2e/Services/Health/HealthCustomServerTest.php +++ b/tests/e2e/Services/Health/HealthCustomServerTest.php @@ -494,12 +494,12 @@ class HealthCustomServerTest extends Scope return []; } - public function testUsageSuccess() + public function testStatsResources() { /** * Test for SUCCESS */ - $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage', array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-resources', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), []); @@ -511,19 +511,19 @@ class HealthCustomServerTest extends Scope /** * Test for FAILURE */ - $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage?threshold=0', array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-resources?threshold=0', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), []); $this->assertEquals(503, $response['headers']['status-code']); } - public function testUsageDumpSuccess() + public function testUsageSuccess() { /** * Test for SUCCESS */ - $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage-dump', array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-usage', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), []); @@ -535,7 +535,31 @@ class HealthCustomServerTest extends Scope /** * Test for FAILURE */ - $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage-dump?threshold=0', array_merge([ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-usage?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(503, $response['headers']['status-code']); + } + + public function testStatsUsageDumpSuccess() + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-usage-dump', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertIsInt($response['body']['size']); + $this->assertLessThan(100, $response['body']['size']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/stats-usage-dump?threshold=0', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), []); diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index e6c1f81595..e49aedf643 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -4,6 +4,7 @@ namespace Tests\E2E\Services\Migrations; use CURLFile; use Tests\E2E\Client; +use Tests\E2E\General\UsageTest; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Services\Functions\FunctionsBase; use Utopia\Database\Helpers\ID; @@ -20,13 +21,13 @@ trait MigrationsBase /** * @var array */ - protected static $destinationProject = []; + protected static array $destinationProject = []; /** * @param bool $fresh * @return array */ - public function getDesintationProject(bool $fresh = false): array + public function getDestinationProject(bool $fresh = false): array { if (!empty(self::$destinationProject) && !$fresh) { return self::$destinationProject; @@ -40,13 +41,12 @@ trait MigrationsBase return self::$destinationProject; } - public function performMigrationSync( - array $body, - ): array { + public function performMigrationSync(array $body): array + { $migration = $this->client->call(Client::METHOD_POST, '/migrations/appwrite', [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ], $body); $this->assertEquals(202, $migration['headers']['status-code']); @@ -57,8 +57,8 @@ trait MigrationsBase while ($attempts < 5) { $response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migration['body']['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -83,12 +83,14 @@ trait MigrationsBase $attempts++; sleep(5); } + + return []; } /** * Appwrite E2E Migration Tests */ - public function testCreateAppwriteMigration() + public function testCreateAppwriteMigration(): void { $response = $this->performMigrationSync([ 'resources' => Appwrite::getSupportedResources(), @@ -106,7 +108,7 @@ trait MigrationsBase /** * Auth */ - public function testAppwriteMigrationAuthUserPassword() + public function testAppwriteMigrationAuthUserPassword(): void { $response = $this->client->call(Client::METHOD_POST, '/users', [ 'content-type' => 'application/json', @@ -145,8 +147,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/users/' . $user['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -158,8 +160,8 @@ trait MigrationsBase // Cleanup $this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [ @@ -169,7 +171,7 @@ trait MigrationsBase ]); } - public function testAppwriteMigrationAuthUserPhone() + public function testAppwriteMigrationAuthUserPhone(): void { $response = $this->client->call(Client::METHOD_POST, '/users', [ 'content-type' => 'application/json', @@ -207,8 +209,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/users/' . $user['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -225,12 +227,12 @@ trait MigrationsBase $this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); } - public function testAppwriteMigrationAuthTeam() + public function testAppwriteMigrationAuthTeam(): void { $user = $this->client->call(Client::METHOD_POST, '/users', [ 'content-type' => 'application/json', @@ -310,8 +312,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/teams/' . $team['body']['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -321,8 +323,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/teams/' . $team['body']['$id'] . '/memberships', [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -343,8 +345,8 @@ trait MigrationsBase $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->client->call(Client::METHOD_DELETE, '/users/' . $user['body']['$id'], [ @@ -355,8 +357,8 @@ trait MigrationsBase $this->client->call(Client::METHOD_DELETE, '/users/' . $user['body']['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [ @@ -367,15 +369,15 @@ trait MigrationsBase $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); } /** * Databases */ - public function testAppwriteMigrationDatabase() + public function testAppwriteMigrationDatabase(): array { $response = $this->client->call(Client::METHOD_POST, '/databases', [ 'content-type' => 'application/json', @@ -401,7 +403,6 @@ trait MigrationsBase 'apiKey' => $this->getProject()['apiKey'], ]); - $this->assertEquals('completed', $result['status']); $this->assertEquals([Resource::TYPE_DATABASE], $result['resources']); $this->assertArrayHasKey(Resource::TYPE_DATABASE, $result['statusCounters']); @@ -413,8 +414,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -427,8 +428,8 @@ trait MigrationsBase // Cleanup on destination $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); return [ @@ -439,7 +440,7 @@ trait MigrationsBase /** * @depends testAppwriteMigrationDatabase */ - public function testAppwriteMigrationDatabasesCollection(array $data) + public function testAppwriteMigrationDatabasesCollection(array $data): array { $databaseId = $data['databaseId']; @@ -507,8 +508,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -519,8 +520,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -533,8 +534,8 @@ trait MigrationsBase // Cleanup $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); return [ @@ -546,7 +547,7 @@ trait MigrationsBase /** * @depends testAppwriteMigrationDatabasesCollection */ - public function testAppwriteMigrationDatabasesDocument(array $data) + public function testAppwriteMigrationDatabasesDocument(array $data): void { $databaseId = $data['databaseId']; $collectionId = $data['collectionId']; @@ -580,6 +581,14 @@ trait MigrationsBase 'apiKey' => $this->getProject()['apiKey'], ]); + $finalStats = $this->client->call(Client::METHOD_GET, '/project/usage', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'startDate' => UsageTest::getYesterday(), + 'endDate' => UsageTest::getTomorrow(), + ]); + $this->assertEquals('completed', $result['status']); $this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE, Resource::TYPE_DOCUMENT], $result['resources']); @@ -595,8 +604,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -608,15 +617,15 @@ trait MigrationsBase // Cleanup $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); } /** * Storage */ - public function testAppwriteMigrationStorageBucket() + public function testAppwriteMigrationStorageBucket(): void { $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ 'content-type' => 'application/json', @@ -664,8 +673,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -684,8 +693,8 @@ trait MigrationsBase // Cleanup $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucket['body']['$id'], [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucket['body']['$id'], [ @@ -695,7 +704,7 @@ trait MigrationsBase ]); } - public function testAppwriteMigrationStorageFiles() + public function testAppwriteMigrationStorageFiles(): void { $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ 'content-type' => 'application/json', @@ -768,8 +777,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -787,15 +796,15 @@ trait MigrationsBase $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); } /** * Functions */ - public function testAppwriteMigrationFunction() + public function testAppwriteMigrationFunction(): void { $functionId = $this->setupFunction([ 'functionId' => ID::unique(), @@ -840,8 +849,8 @@ trait MigrationsBase $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); $this->assertEquals(200, $response['headers']['status-code']); @@ -857,8 +866,8 @@ trait MigrationsBase $this->assertEventually(function () use ($functionId) { $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/', array_merge([ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ])); $this->assertEquals(200, $deployments['headers']['status-code']); @@ -871,8 +880,8 @@ trait MigrationsBase // Attempt execution $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ], [ 'body' => 'test' ]); @@ -889,8 +898,8 @@ trait MigrationsBase $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getDesintationProject()['$id'], - 'x-appwrite-key' => $this->getDesintationProject()['apiKey'], + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]); } } diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 34c1142619..ed9171e46a 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -3923,10 +3923,14 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-mode' => 'admin', ], $this->getHeaders()), [ 'key' => 'APP_TEST', - 'value' => 'TESTINGVALUE' + 'value' => 'TESTINGVALUE', + 'secret' => false ]); $this->assertEquals(201, $variable['headers']['status-code']); + $this->assertEquals('APP_TEST', $variable['body']['key']); + $this->assertEquals('TESTINGVALUE', $variable['body']['value']); + $this->assertFalse($variable['body']['secret']); $variableId = $variable['body']['$id']; // test for secret variable @@ -4047,6 +4051,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals("APP_TEST_1", $response['body']['key']); $this->assertEmpty($response['body']['value']); + $this->assertTrue($response['body']['secret']); /** * Test for FAILURE @@ -4118,6 +4123,17 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals("APP_TEST_UPDATE_1", $variable['body']['key']); $this->assertEmpty($variable['body']['value']); + $response = $this->client->call(Client::METHOD_PUT, '/project/variables/' . $data['secretVariableId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $data['projectId'], + 'x-appwrite-mode' => 'admin', + ], $this->getHeaders()), [ + 'key' => 'APP_TEST_UPDATE_1', + 'secret' => false, + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + $response = $this->client->call(Client::METHOD_GET, '/project/variables', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $data['projectId'], diff --git a/tests/e2e/Services/Projects/ProjectsCustomServerTest.php b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php index f81290e707..6d9431290f 100644 --- a/tests/e2e/Services/Projects/ProjectsCustomServerTest.php +++ b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php @@ -24,35 +24,79 @@ class ProjectsCustomServerTest extends Scope 'cookie' => 'a_session_console=' . $this->getRoot()['session'], ]); - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'api', + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ 'domain' => 'api.appwrite.test', ]); $this->assertEquals(201, $response['headers']['status-code']); + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ + 'resourceType' => 'api', + 'domain' => 'abc.test.io', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // duplicate rule + $response2 = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ + 'domain' => 'abc.test.io', + ]); + + $this->assertEquals(409, $response2['headers']['status-code']); + $response = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $response['body']['$id'], $headers); $this->assertEquals(204, $response['headers']['status-code']); - // prevent functions domain $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'api', + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ + 'domain' => $functionsDomain, + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ + 'domain' => $sitesDomain, + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // prevent functions domain + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', $headers, [ 'domain' => $functionsDomain, ]); $this->assertEquals(400, $response['headers']['status-code']); // prevent sites domain - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - - $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ - 'resourceType' => 'api', + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', $headers, [ 'domain' => $sitesDomain, ]); $this->assertEquals(400, $response['headers']['status-code']); + + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + foreach ($deniedDomains as $deniedDomain) { + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [ + 'domain' => $deniedDomain, + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } } } diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php new file mode 100644 index 0000000000..ed289ea8a5 --- /dev/null +++ b/tests/e2e/Services/Proxy/ProxyBase.php @@ -0,0 +1,296 @@ +client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $rule; + } + + protected function createAPIRule(string $domain): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + ]); + + return $rule; + } + + protected function updateRuleVerification(string $ruleId): mixed + { + $rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/verification', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + return $rule; + } + + protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + 'siteId' => $siteId, + 'branch' => $branch, + ]); + + return $rule; + } + + protected function getRule(string $ruleId): mixed + { + $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + return $rule; + } + + protected function createRedirectRule(string $domain, string $target): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + 'target' => $target, + ]); + + return $rule; + } + + protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed + { + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $domain, + 'functionId' => $functionId, + 'branch' => $branch, + ]); + + return $rule; + } + + protected function deleteRule(string $ruleId): mixed + { + $rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + return $rule; + } + + protected function setupAPIRule(string $domain): string + { + $rule = $this->createAPIRule($domain); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function setupRedirectRule(string $domain, string $target): string + { + $rule = $this->createRedirectRule($domain, $target); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string + { + $rule = $this->createFunctionRule($domain, $functionId, $branch); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string + { + $rule = $this->createSiteRule($domain, $siteId, $branch); + + $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule)); + + return $rule['body']['$id']; + } + + protected function cleanupRule(string $ruleId): void + { + $rule = $this->deleteRule($ruleId); + $this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule)); + } + + protected function cleanupSite(string $siteId): void + { + $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site)); + } + + protected function cleanupFunction(string $functionId): void + { + $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function)); + } + + protected function setupSite(): mixed + { + // Site + $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'siteId' => ID::unique(), + 'name' => 'Proxy site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + + $siteId = $site['body']['$id']; + + // Deployment + $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'code' => $this->packageSite('static'), + 'activate' => 'true' + ]); + + $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + $deploymentId = $deployment['body']['$id'] ?? ''; + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + return ['siteId' => $siteId, 'deploymentId' => $deploymentId]; + } + + protected function setupFunction(): mixed + { + // Function + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'functionId' => ID::unique(), + 'runtime' => 'node-18.0', + 'name' => 'Proxy Function', + 'entrypoint' => 'index.js', + 'commands' => '', + 'execute' => ['any'] + ]); + + $this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); + + $functionId = $function['body']['$id']; + + // Deployment + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'code' => $this->packageFunction('node'), + 'activate' => 'true' + ]); + + $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + $deploymentId = $deployment['body']['$id'] ?? ''; + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $function['body']['deployment'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + return ['functionId' => $functionId, 'deploymentId' => $deploymentId]; + } + + private function packageSite(string $site): CURLFile + { + $stdout = ''; + $stderr = ''; + + $folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site"; + $tarPath = "$folderPath/code.tar.gz"; + + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + + if (filesize($tarPath) > 1024 * 1024 * 5) { + throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + } + + return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); + } + + private function packageFunction(string $function): CURLFile + { + $stdout = ''; + $stderr = ''; + + $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function"; + $tarPath = "$folderPath/code.tar.gz"; + + Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr); + + if (filesize($tarPath) > 1024 * 1024 * 5) { + throw new \Exception('Code package is too large. Use the chunked upload method instead.'); + } + + return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath)); + } +} diff --git a/tests/e2e/Services/Proxy/ProxyCustomServerTest.php b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php new file mode 100644 index 0000000000..78faffebd0 --- /dev/null +++ b/tests/e2e/Services/Proxy/ProxyCustomServerTest.php @@ -0,0 +1,457 @@ +createAPIRule($domain); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals($domain, $rule['body']['domain']); + $this->assertArrayHasKey('$id', $rule['body']); + $this->assertArrayHasKey('type', $rule['body']); + $this->assertArrayHasKey('value', $rule['body']); + $this->assertArrayHasKey('automation', $rule['body']); + $this->assertArrayHasKey('status', $rule['body']); + $this->assertArrayHasKey('logs', $rule['body']); + $this->assertArrayHasKey('renewAt', $rule['body']); + + $ruleId = $rule['body']['$id']; + + $rule = $this->createAPIRule($domain); + $this->assertEquals(409, $rule['headers']['status-code']); + + $rule = $this->deleteRule($ruleId); + + $this->assertEquals(204, $rule['headers']['status-code']); + } + + public function testCreateRuleSetup(): void + { + $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com'); + $this->cleanupRule($ruleId); + } + + public function testCreateRuleApex(): void + { + $rule = $this->createAPIRule('myapp.com'); + $this->assertEquals(400, $rule['headers']['status-code']); + } + + public function testCreateRuleVcs(): void + { + $domain = \uniqid() . '-vcs.myapp.com'; + + $rule = $this->createAPIRule('commit-' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createAPIRule('branch-' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createAPIRule('anything-' . $domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['body']['$id']); + } + + public function testCreateAPIRule(): void + { + $domain = \uniqid() . '-api.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + // We should ideally assert 400, but server allows unknown domains, and serves API by default + $response = $proxyClient->call(Client::METHOD_GET, '/versions'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(APP_VERSION_STABLE, $response['body']['server']); + + $ruleId = $this->setupAPIRule($domain); + + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/versions'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(APP_VERSION_STABLE, $response['body']['server']); + + $this->cleanupRule($ruleId); + + $rule = $this->createAPIRule('http://' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + $rule = $this->createAPIRule('https://' . $domain); + $this->assertEquals(400, $rule['headers']['status-code']); + + // Unexpected I would say, but it is the current behaviour + $rule = $this->createAPIRule('wss://' . $domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['body']['$id']); + + // Unexpected I would say, but it is the current behaviour + $rule = $this->createAPIRule($domain . '/some-path'); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->cleanupRule($rule['body']['$id']); + } + + public function testCreateRedirectRule(): void + { + $domain = \uniqid() . '-redirect.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); + $this->assertEquals(404, $response['headers']['status-code']); + + $ruleId = $this->setupRedirectRule($domain, 'jsonplaceholder.typicode.com'); + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/todos/1'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['id']); + + $this->cleanupRule($ruleId); + } + + public function testCreateFunctionRule(): void + { + $domain = \uniqid() . '-function.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/ping'); + $this->assertEquals(404, $response['headers']['status-code']); + + $setup = $this->setupFunction(); + $functionId = $setup['functionId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($functionId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupFunctionRule($domain, $functionId); + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/ping'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']); + + $this->cleanupRule($ruleId); + + $this->cleanupFunction($functionId); + + $this->assertEventually(function () use ($functionId, $deploymentId) { + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('automation', ['function=' . $functionId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('value', [$deploymentId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + }); + } + + public function testCreateSiteRule(): void + { + $domain = \uniqid() . '-site.custom.localhost'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://appwrite'); + $proxyClient->addHeader('x-appwrite-hostname', $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/contact'); + $this->assertEquals(404, $response['headers']['status-code']); + + $setup = $this->setupSite(); + $siteId = $setup['siteId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($siteId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupSiteRule($domain, $siteId); + $this->assertNotEmpty($ruleId); + + $response = $proxyClient->call(Client::METHOD_GET, '/contact'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('Contact page', $response['body']); + + $this->cleanupRule($ruleId); + + $this->cleanupSite($siteId); + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('automation', ['site=' . $siteId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('value', [$deploymentId])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + }); + } + + public function testCreatSiteBranchRule(): void + { + $domain = \uniqid() . '-site-branch.custom.localhost'; + + $setup = $this->setupSite(); + $siteId = $setup['siteId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($siteId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupSiteRule($domain, $siteId, 'dev'); + $this->assertNotEmpty($ruleId); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('branch=dev', $rule['body']['automation']); + + $this->cleanupRule($ruleId); + } + + public function testCreatFunctionBranchRule(): void + { + $domain = \uniqid() . '-function-branch.custom.localhost'; + + $setup = $this->setupFunction(); + $functionId = $setup['functionId']; + $deploymentId = $setup['deploymentId']; + + $this->assertNotEmpty($functionId); + $this->assertNotEmpty($deploymentId); + + $ruleId = $this->setupFunctionRule($domain, $functionId, 'dev'); + $this->assertNotEmpty($ruleId); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals('branch=dev', $rule['body']['automation']); + + $this->cleanupRule($ruleId); + } + + public function testUpdateRule(): void + { + // Create function appwrite-network domain + $domain = \uniqid() . '-cname-api.' . App::getEnv('_APP_DOMAIN_FUNCTIONS'); + + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verified', $rule['body']['status']); + + $this->cleanupRule($rule['body']['$id']); + + // Create site appwrite-network domain + $domain = \uniqid() . '-cname-api.' . App::getEnv('_APP_DOMAIN_SITES'); + + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('verified', $rule['body']['status']); + + $this->cleanupRule($rule['body']['$id']); + + // Create + update + $domain = \uniqid() . '-cname-api.custom.localhost'; + + $rule = $this->createAPIRule($domain); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertEquals('created', $rule['body']['status']); + + $ruleId = $rule['body']['$id']; + + $rule = $this->updateRuleVerification($ruleId); + $this->assertEquals(401, $rule['headers']['status-code']); + + $this->cleanupRule($ruleId); + } + + public function testGetRule() + { + $domain = \uniqid() . '-get.custom.localhost'; + $ruleId = $this->setupAPIRule($domain); + + $this->assertNotEmpty($ruleId); + + $rule = $this->getRule($ruleId); + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals($domain, $rule['body']['domain']); + $this->assertArrayHasKey('$id', $rule['body']); + $this->assertArrayHasKey('type', $rule['body']); + $this->assertArrayHasKey('value', $rule['body']); + $this->assertArrayHasKey('automation', $rule['body']); + $this->assertArrayHasKey('status', $rule['body']); + $this->assertArrayHasKey('logs', $rule['body']); + $this->assertArrayHasKey('renewAt', $rule['body']); + + $this->cleanupRule($ruleId); + } + + public function testListRules() + { + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + foreach ($rules['body']['rules'] as $rule) { + $rule = $this->deleteRule($rule['$id']); + $this->assertEquals(204, $rule['headers']['status-code']); + } + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + + $rule1Domain = \uniqid() . '-list1.custom.localhost'; + $rule1Id = $this->setupAPIRule($rule1Domain); + $this->assertNotEmpty($rule1Id); + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(1, $rules['body']['total']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); + $this->assertArrayHasKey('$id', $rules['body']['rules'][0]); + $this->assertArrayHasKey('type', $rules['body']['rules'][0]); + $this->assertArrayHasKey('value', $rules['body']['rules'][0]); + $this->assertArrayHasKey('automation', $rules['body']['rules'][0]); + $this->assertArrayHasKey('status', $rules['body']['rules'][0]); + $this->assertArrayHasKey('logs', $rules['body']['rules'][0]); + $this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]); + + $rule2Domain = \uniqid() . '-list1.custom.localhost'; + $rule2Id = $this->setupAPIRule($rule2Domain); + $this->assertNotEmpty($rule2Id); + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(2, $rules['body']['total']); + $this->assertCount(2, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::limit(1)->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(2, $rules['body']['total']); + $this->assertCount(1, $rules['body']['rules']); + + $rules = $this->listRules([ + 'queries' => [ + Query::equal('$id', [$rule1Id])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']); + + $rules = $this->listRules([ + 'queries' => [ + Query::orderDesc('$id')->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertCount(2, $rules['body']['rules']); + $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); + + $rules = $this->listRules([ + 'queries' => [ + Query::equal('domain', [$rule2Domain])->toString() + ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertCount(1, $rules['body']['rules']); + $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']); + + $rules = $this->listRules([ + 'search' => $rule1Domain, + 'queries' => [ Query::orderDesc('$createdAt') ] + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleIds = \array_column($rules['body']['rules'], '$id'); + $this->assertContains($rule1Id, $ruleIds); + + $rules = $this->listRules([ + 'search' => $rule2Domain, + 'queries' => [ Query::orderDesc('$createdAt') ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleIds = \array_column($rules['body']['rules'], '$id'); + $this->assertContains($rule2Id, $ruleIds); + + $rules = $this->listRules([ + 'search' => $rule1Id, + 'queries' => [ Query::orderDesc('$createdAt') ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleDomains = \array_column($rules['body']['rules'], 'domain'); + $this->assertContains($rule1Domain, $ruleDomains); + + $rules = $this->listRules([ + 'search' => $rule2Id, + 'queries' => [ Query::orderDesc('$createdAt') ] + ]); + $this->assertEquals(200, $rules['headers']['status-code']); + $ruleDomains = \array_column($rules['body']['rules'], 'domain'); + $this->assertContains($rule2Domain, $ruleDomains); + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + foreach ($rules['body']['rules'] as $rule) { + $rule = $this->deleteRule($rule['$id']); + $this->assertEquals(204, $rule['headers']['status-code']); + } + + $rules = $this->listRules(); + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertEquals(0, $rules['body']['total']); + $this->assertCount(0, $rules['body']['rules']); + } +} diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 277064ead5..a7e0293409 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -6,7 +6,9 @@ use Appwrite\Tests\Async; use CURLFile; use Tests\E2E\Client; use Utopia\CLI\Console; +use Utopia\Database\Helpers\ID; use Utopia\Database\Query; +use Utopia\System\System; trait SitesBase { @@ -47,7 +49,19 @@ trait SitesBase 'x-appwrite-key' => $this->getProject()['apiKey'], ])); $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); - }, 50000, 500); + }, 100000, 500); + + // Not === so multipart/form-data works fine too + if (($params['activate'] ?? false) == true) { + $this->assertEventually(function () use ($siteId, $deploymentId) { + $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + } return $deploymentId; } @@ -104,6 +118,46 @@ trait SitesBase return $variable; } + protected function getVariable(string $siteId, string $variableId): mixed + { + $variable = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $variable; + } + + protected function listVariables(string $siteId, mixed $params = []): mixed + { + $variables = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/variables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $variables; + } + + protected function updateVariable(string $siteId, string $variableId, mixed $params): mixed + { + $variable = $this->client->call(Client::METHOD_PUT, '/sites/' . $siteId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $variable; + } + + protected function deleteVariable(string $siteId, string $variableId): mixed + { + $variable = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId . '/variables/' . $variableId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $variable; + } + protected function getSite(string $siteId): mixed { $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([ @@ -154,12 +208,14 @@ trait SitesBase return $deployments; } - protected function listLogs(string $siteId, mixed $params = []): mixed + protected function listLogs(string $siteId, array $queries = []): mixed { $logs = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/logs', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), $params); + ], $this->getHeaders()), [ + 'queries' => $queries + ]); return $logs; } @@ -188,6 +244,49 @@ trait SitesBase return $deployment; } + protected function setupDuplicateDeployment(string $siteId, string $deploymentId): string + { + $deployment = $this->createDuplicateDeployment($siteId, $deploymentId); + $this->assertEquals(202, $deployment['headers']['status-code']); + + $deploymentId = $deployment['body']['$id']; + $this->assertNotEmpty($deploymentId); + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $deployment = $this->getDeployment($siteId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + $this->assertEventually(function () use ($siteId, $deploymentId) { + $site = $this->getSite($siteId); + $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT)); + }, 100000, 500); + + return $deploymentId; + } + + protected function createDuplicateDeployment(string $siteId, string $deploymentId): mixed + { + $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments/duplicate', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'deploymentId' => $deploymentId, + ]); + + return $deployment; + } + + protected function createTemplateDeployment(string $siteId, mixed $params = []): mixed + { + $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments/template', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $params); + + return $deployment; + } + protected function getSiteUsage(string $siteId, mixed $params): mixed { $usage = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/usage', array_merge([ @@ -217,6 +316,26 @@ trait SitesBase return $site; } + protected function setupSiteDomain(string $siteId, string $subdomain = ''): string + { + $subdomain = $subdomain ? $subdomain : ID::unique(); + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''), + 'siteId' => $siteId, + ]); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertNotEmpty($rule['body']['$id']); + $this->assertNotEmpty($rule['body']['domain']); + + $domain = $rule['body']['domain']; + + return $domain; + } + protected function getSiteDomain(string $siteId): string { $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ @@ -224,8 +343,8 @@ trait SitesBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'queries' => [ - Query::equal('resourceId', [$siteId])->toString(), - Query::equal('resourceType', ['site'])->toString(), + Query::equal('automation', ['site=' . $siteId])->toString(), + Query::equal('type', ['deployment'])->toString(), ], ]); @@ -238,4 +357,61 @@ trait SitesBase return $domain; } + + protected function getDeploymentDomain(string $deploymentId): string + { + $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::equal('value', [$deploymentId])->toString(), + Query::equal('type', ['deployment'])->toString(), + Query::equal('automation', [''])->toString(), + ], + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertGreaterThanOrEqual(1, $rules['body']['total']); + $this->assertGreaterThanOrEqual(1, \count($rules['body']['rules'])); + $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + + $domain = $rules['body']['rules'][0]['domain']; + + return $domain; + } + + protected function getDeploymentDownload(string $siteId, string $deploymentId, string $type): mixed + { + $response = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId . '/download', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'type' => $type, + ]); + + return $response; + } + + protected function updateSiteDeployment(string $siteId, string $deploymentId): mixed + { + $site = $this->client->call(Client::METHOD_PATCH, '/sites/' . $siteId . '/deployment', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'deploymentId' => $deploymentId + ]); + + return $site; + } + + protected function cancelDeployment(string $siteId, string $deploymentId): mixed + { + $deployment = $this->client->call(Client::METHOD_PATCH, '/sites/' . $siteId . '/deployments/' . $deploymentId . '/status', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + return $deployment; + } } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index ff3aa7b348..bd6236291c 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -3,6 +3,7 @@ namespace Tests\E2E\Services\Sites; use Appwrite\Sites\Specification; +use Appwrite\Tests\Retry; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; @@ -11,6 +12,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\System\System; class SitesCustomServerTest extends Scope { @@ -24,7 +26,7 @@ class SitesCustomServerTest extends Scope * Test for SUCCESS */ $site = $this->createSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -41,7 +43,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals('other', $site['body']['framework']); $this->assertEquals(true, $dateValidator->isValid($site['body']['$createdAt'])); $this->assertEquals(true, $dateValidator->isValid($site['body']['$updatedAt'])); - $this->assertEquals('ssr-22', $site['body']['buildRuntime']); + $this->assertEquals('node-22', $site['body']['buildRuntime']); $this->assertEquals(null, $site['body']['fallbackFile']); $this->assertEquals('./', $site['body']['outputDirectory']); @@ -65,13 +67,271 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } + public function testConsoleAvailabilityEndpoint(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Test Site', + 'framework' => 'other', + 'buildRuntime' => 'node-22', + 'outputDirectory' => './', + 'fallbackFile' => null, + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $response = $this->client->call(Client::METHOD_GET, '/console/resources', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ], [ + 'type' => 'rules', + 'value' => $domain, + ]); + + $this->assertEquals(409, $response['headers']['status-code']); // domain unavailable + + $nonExistingDomain = "non-existent-subdomain.sites.localhost"; + + $response = $this->client->call(Client::METHOD_GET, '/console/resources', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ], [ + 'type' => 'rules', + 'value' => $nonExistingDomain, + ]); + + $this->assertEquals(204, $response['headers']['status-code']); // domain available + + $this->cleanupSite($siteId); + + $this->assertEventually(function () use ($siteId) { + $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::equal('automation', ['site=' . $siteId]) + ] + ]); + + $this->assertEquals(200, $rule['headers']['status-code']); + $this->assertEquals(0, $rule['body']['total']); + }, 5000, 500); + + $response = $this->client->call(Client::METHOD_GET, '/console/resources', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ], [ + 'type' => 'rules', + 'value' => $domain, + ]); + + $this->assertEquals(204, $response['headers']['status-code']); // domain available as site is deleted + } + + public function testVariables(): void + { + $site = $this->createSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => null, + 'framework' => 'other', + 'name' => 'Test Site', + 'outputDirectory' => './', + 'siteId' => ID::unique() + ]); + + $siteId = $site['body']['$id'] ?? ''; + + $this->assertEquals(201, $site['headers']['status-code']); + $this->assertNotEmpty($site['body']['$id']); + $this->assertEquals('Test Site', $site['body']['name']); + + $variable = $this->createVariable($siteId, [ + 'key' => 'siteKey1', + 'value' => 'siteValue1', + 'secret' => false, + ]); + + $this->assertEquals(201, $variable['headers']['status-code']); + $this->assertNotEmpty($variable['body']['$id']); + $this->assertEquals('siteKey1', $variable['body']['key']); + $this->assertEquals('siteValue1', $variable['body']['value']); + $this->assertEquals(false, $variable['body']['secret']); + + $variable2 = $this->createVariable($siteId, [ + 'key' => 'siteKey2', + 'value' => 'siteValue2', + 'secret' => false, + ]); + + $this->assertEquals(201, $variable2['headers']['status-code']); + $this->assertNotEmpty($variable2['body']['$id']); + $this->assertEquals('siteKey2', $variable2['body']['key']); + $this->assertEquals('siteValue2', $variable2['body']['value']); + $this->assertEquals(false, $variable2['body']['secret']); + + $secretVariable = $this->createVariable($siteId, [ + 'key' => 'siteKey3', + 'value' => 'siteValue3', + 'secret' => true, + ]); + + $this->assertEquals(201, $secretVariable['headers']['status-code']); + $this->assertNotEmpty($secretVariable['body']['$id']); + $this->assertEquals('siteKey3', $secretVariable['body']['key']); + $this->assertEquals('', $secretVariable['body']['value']); + $this->assertEquals(true, $secretVariable['body']['secret']); + + $variable = $this->getVariable($siteId, $variable['body']['$id']); + + $this->assertEquals(200, $variable['headers']['status-code']); + $this->assertNotEmpty($variable['body']['$id']); + $this->assertEquals('siteKey1', $variable['body']['key']); + $this->assertEquals('siteValue1', $variable['body']['value']); + $this->assertEquals(false, $variable['body']['secret']); + + $secretVariable = $this->getVariable($siteId, $secretVariable['body']['$id']); + + $this->assertEquals(200, $secretVariable['headers']['status-code']); + $this->assertNotEmpty($secretVariable['body']['$id']); + $this->assertEquals('siteKey3', $secretVariable['body']['key']); + $this->assertEquals('', $secretVariable['body']['value']); + $this->assertEquals(true, $secretVariable['body']['secret']); + + $variable = $this->updateVariable($siteId, $variable['body']['$id'], [ + 'key' => 'siteKey1Updated', + 'value' => 'siteValue1Updated', + ]); + + $this->assertEquals(200, $variable['headers']['status-code']); + $this->assertNotEmpty($variable['body']['$id']); + $this->assertEquals('siteKey1Updated', $variable['body']['key']); + $this->assertEquals('siteValue1Updated', $variable['body']['value']); + $this->assertEquals(false, $variable['body']['secret']); + + $variable = $this->updateVariable($siteId, $variable['body']['$id'], [ + 'key' => 'siteKey1Updated', + 'secret' => true, + ]); + + $this->assertEquals(200, $variable['headers']['status-code']); + $this->assertNotEmpty($variable['body']['$id']); + $this->assertEquals('siteKey1Updated', $variable['body']['key']); + $this->assertEquals('', $variable['body']['value']); + $this->assertEquals(true, $variable['body']['secret']); + + $secretVariable = $this->updateVariable($siteId, $secretVariable['body']['$id'], [ + 'key' => 'siteKey3', + 'value' => 'siteValue3Updated', + ]); + + $this->assertEquals(200, $secretVariable['headers']['status-code']); + $this->assertNotEmpty($secretVariable['body']['$id']); + $this->assertEquals('siteKey3', $secretVariable['body']['key']); + $this->assertEquals('', $secretVariable['body']['value']); + $this->assertEquals(true, $secretVariable['body']['secret']); + + $response = $this->updateVariable($siteId, $secretVariable['body']['$id'], [ + 'key' => 'siteKey3', + 'secret' => false, + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $secretVariable = $this->getVariable($siteId, $secretVariable['body']['$id']); + + $this->assertEquals(200, $secretVariable['headers']['status-code']); + $this->assertNotEmpty($secretVariable['body']['$id']); + $this->assertEquals('siteKey3', $secretVariable['body']['key']); + $this->assertEquals('', $secretVariable['body']['value']); + $this->assertEquals(true, $secretVariable['body']['secret']); + + $variables = $this->listVariables($siteId); + + $this->assertEquals(200, $variables['headers']['status-code']); + $this->assertCount(3, $variables['body']['variables']); + + $response = $this->deleteVariable($siteId, $variable['body']['$id']); + $this->assertEquals(204, $response['headers']['status-code']); + $response = $this->deleteVariable($siteId, $variable2['body']['$id']); + $this->assertEquals(204, $response['headers']['status-code']); + $response = $this->deleteVariable($siteId, $secretVariable['body']['$id']); + $this->assertEquals(204, $response['headers']['status-code']); + + $variables = $this->listVariables($siteId); + + $this->assertEquals(200, $variables['headers']['status-code']); + $this->assertCount(0, $variables['body']['variables']); + + $this->cleanupSite($siteId); + } + + // This is first Sites test with Proxy + // If this fails, it may not be related to variables; but Router flow failing + public function testVariablesE2E(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Astro site', + 'framework' => 'astro', + 'adapter' => 'ssr', + 'buildRuntime' => 'node-22', + 'outputDirectory' => './dist', + 'buildCommand' => 'npm run build', + 'installCommand' => 'npm install', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $secretVariable = $this->createVariable($siteId, [ + 'key' => 'name', + 'value' => 'Appwrite', + ]); + + $this->assertEquals(201, $secretVariable['headers']['status-code']); + $this->assertNotEmpty($secretVariable['body']['$id']); + $this->assertEquals('name', $secretVariable['body']['key']); + $this->assertEquals('', $secretVariable['body']['value']); + $this->assertEquals(true, $secretVariable['body']['secret']); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('astro'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->getSiteDomain($siteId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Env variable is Appwrite", $response['body']); + $this->assertStringNotContainsString("Variable not found", $response['body']); + + $this->cleanupSite($siteId); + } + public function testListSites(): void { /** * Test for SUCCESS */ $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -151,7 +411,7 @@ class SitesCustomServerTest extends Scope * Test pagination */ $siteId2 = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site 2', @@ -207,7 +467,7 @@ class SitesCustomServerTest extends Scope public function testGetSite(): void { $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -240,7 +500,7 @@ class SitesCustomServerTest extends Scope public function testUpdateSite(): void { $site = $this->createSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -257,7 +517,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals('Test Site', $site['body']['name']); $site = $this->updateSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site Updated', @@ -308,11 +568,6 @@ class SitesCustomServerTest extends Scope 'installCommand' => $nextjsFramework['installCommand'], 'outputDirectory' => $nextjsFramework['outputDirectory'], 'providerRootDirectory' => $nextjsFramework['providerRootDirectory'], - 'templateOwner' => $starterTemplate['body']['providerOwner'], - 'templateRepository' => $starterTemplate['body']['providerRepositoryId'], - 'templateRootDirectory' => $nextjsFramework['providerRootDirectory'], - 'templateVersion' => $starterTemplate['body']['providerVersion'], - 'providerBranch' => 'main', ] ); @@ -321,6 +576,20 @@ class SitesCustomServerTest extends Scope $siteId = $site['body']['$id'] ?? ''; + $deployment = $this->createTemplateDeployment( + $siteId, + [ + 'owner' => $starterTemplate['body']['providerOwner'], + 'repository' => $starterTemplate['body']['providerRepositoryId'], + 'rootDirectory' => $nextjsFramework['providerRootDirectory'], + 'version' => $starterTemplate['body']['providerVersion'], + 'activate' => true, + ] + ); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + $deployments = $this->listDeployments($siteId); $this->assertEquals(200, $deployments['headers']['status-code']); @@ -349,7 +618,7 @@ class SitesCustomServerTest extends Scope public function testCreateDeployment() { $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -406,10 +675,11 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } + #[Retry(count: 3)] public function testCancelDeploymentBuild(): void { $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -438,14 +708,9 @@ class SitesCustomServerTest extends Scope $this->assertEquals('building', $deployment['body']['status']); }, 100000, 250); - // Cancel the deployment - $cancel = $this->client->call(Client::METHOD_PATCH, '/sites/' . $siteId . '/deployments/' . $deploymentId . '/build', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(200, $cancel['headers']['status-code']); - $this->assertEquals('canceled', $cancel['body']['status']); + $deployment = $this->cancelDeployment($siteId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals('canceled', $deployment['body']['status']); /** * Build worker still runs the build. @@ -466,7 +731,7 @@ class SitesCustomServerTest extends Scope public function testUpdateDeployment(): void { $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -497,11 +762,7 @@ class SitesCustomServerTest extends Scope */ $dateValidator = new DatetimeValidator(); - $response = $this->client->call(Client::METHOD_PATCH, '/sites/' . $siteId . '/deployments/' . $deploymentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - + $response = $this->updateSiteDeployment($siteId, $deploymentId); $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertEquals(true, $dateValidator->isValid($response['body']['$createdAt'])); @@ -515,7 +776,7 @@ class SitesCustomServerTest extends Scope public function testListDeployments(): void { $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -694,7 +955,7 @@ class SitesCustomServerTest extends Scope public function testGetDeployment(): void { $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -742,45 +1003,10 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($siteId); } - // public function testLoadSite(): void - // { - // $site = $this->createSite([ - // 'buildRuntime' => 'ssr-22', - // 'fallbackFile' => null, - // 'framework' => 'other', - // 'name' => 'Test Site', - // 'outputDirectory' => './', - // 'providerBranch' => 'main', - // 'providerRootDirectory' => './', - // 'siteId' => ID::unique() - // ]); - - // $siteId = $site['body']['$id'] ?? ''; - // $this->assertNotEmpty($siteId); - - // $deployment = $this->createDeployment($siteId, [ - // 'code' => $this->packageSite('static'), - // 'activate' => 'false' - // ]); - - // $deploymentId = $deployment['body']['$id'] ?? ''; - - // $this->assertEventually(function () use ($siteId, $deploymentId) { - // $deployment = $this->getDeployment($siteId, $deploymentId); - - // $this->assertEquals('ready', $deployment['body']['status']); - // }, 30000, 300); - - // // get rule for this site from rules collection - - // $response = $this->client->call(Client::METHOD_GET, $domain); - // var_dump($response); - // } - public function testUpdateSpecs(): void { $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -797,7 +1023,7 @@ class SitesCustomServerTest extends Scope */ // Change the function specs $site = $this->updateSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -814,7 +1040,7 @@ class SitesCustomServerTest extends Scope // Change the specs to 1vcpu 512mb $site = $this->updateSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -834,7 +1060,7 @@ class SitesCustomServerTest extends Scope */ $site = $this->updateSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -854,7 +1080,7 @@ class SitesCustomServerTest extends Scope public function testDeleteDeployment(): void { $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -898,7 +1124,7 @@ class SitesCustomServerTest extends Scope public function testDeleteSite(): void { $siteId = $this->setupSite([ - 'buildRuntime' => 'ssr-22', + 'buildRuntime' => 'node-22', 'fallbackFile' => null, 'framework' => 'other', 'name' => 'Test Site', @@ -939,6 +1165,118 @@ class SitesCustomServerTest extends Scope $this->assertArrayHasKey('adapters', $framework); } + public function testSiteStatic(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Non-SPA site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-spa'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->setupSiteDomain($siteId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Index page", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/contact', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Contact page", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/non-existing', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertStringContainsString("Page not found", $response['body']); // Title + $this->assertStringContainsString("Go to homepage", $response['body']); // Button + $this->assertStringContainsString("Powered by", $response['body']); // Brand + + $this->cleanupSite($siteId); + } + + public function testSiteStaticSPA(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'SPA site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '404.html', + ]); + + $this->assertNotEmpty($siteId); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-spa'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->setupSiteDomain($siteId); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Index page", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/contact', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Contact page", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/non-existing', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Customized 404 page", $response['body']); + $this->assertStringNotContainsString("Powered by", $response['body']); // Brand + + $this->cleanupSite($siteId); + } + public function testSiteTemplate(): void { $template = $this->getTemplate('astro-starter'); @@ -956,34 +1294,37 @@ class SitesCustomServerTest extends Scope 'buildCommand' => $template['frameworks'][0]['buildCommand'], 'installCommand' => $template['frameworks'][0]['installCommand'], 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], - 'templateRepository' => $template['providerRepositoryId'], - 'templateOwner' => $template['providerOwner'], - 'templateRootDirectory' => $template['frameworks'][0]['providerRootDirectory'], - 'templateVersion' => $template['providerVersion'], ]); + $this->assertNotEmpty($siteId); + + $deployment = $this->createTemplateDeployment($siteId, [ + 'repository' => $template['providerRepositoryId'], + 'owner' => $template['providerOwner'], + 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], + 'version' => $template['providerVersion'], + 'activate' => true + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + $this->assertEventually(function () use ($siteId) { $site = $this->getSite($siteId); $this->assertNotEmpty($site['body']['deploymentId']); }, 50000, 500); - $domain = $this->getSiteDomain($siteId); + $domain = $this->setupSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("Hello, Astronaut!", $response['body']); - $response = $proxyClient->call(Client::METHOD_GET, '/about', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/about'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringContainsString("Astro Blog", $response['body']); @@ -994,8 +1335,6 @@ class SitesCustomServerTest extends Scope public function testSiteDomainReclaiming(): void { - $subdomain = 'startup' . \uniqid(); - $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Startup site', @@ -1006,11 +1345,13 @@ class SitesCustomServerTest extends Scope 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', - 'subdomain' => $subdomain ]); $this->assertNotEmpty($siteId); + $subdomain = 'startup' . \uniqid(); + $domain = $this->setupSiteDomain($siteId, $subdomain); + $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static'), 'activate' => 'true' @@ -1022,15 +1363,12 @@ class SitesCustomServerTest extends Scope $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringNotContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); - $site = $this->createSite([ + $site2 = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Startup 2 site', 'framework' => 'other', @@ -1040,11 +1378,19 @@ class SitesCustomServerTest extends Scope 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', - 'subdomain' => $subdomain ]); - $this->assertEquals(400, $site['headers']['status-code']); - $this->assertStringContainsString("Subdomain already exists.", $site['body']['message']); + $siteId2 = $site2['body']['$id']; + + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''), + 'siteId' => $siteId2, + ]); + + $this->assertEquals(409, $rule['headers']['status-code']); $this->cleanupSite($siteId); @@ -1062,10 +1408,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(0, $rules['body']['total']); }, 50000, 500); - $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); + $response = $proxyClient->call(Client::METHOD_GET, '/'); $this->assertEquals(401, $response['headers']['status-code']); $this->assertStringContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); @@ -1080,13 +1423,420 @@ class SitesCustomServerTest extends Scope 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', - 'subdomain' => $subdomain ]); $this->assertEquals(201, $site['headers']['status-code']); + $this->assertNotEmpty($site['body']['$id']); + + $siteId = $site['body']['$id']; + + $domain = $this->setupSiteDomain($siteId, $subdomain); + + $this->assertNotEmpty($domain); $this->cleanupSite($site['body']['$id']); } - // TODO: Add tests for deletion of resources when site is deleted + public function testSitePreviewBranding(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'A site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->getSiteDomain($siteId); + $previewDomain = $this->getDeploymentDomain($deploymentId); + + $this->assertNotEmpty($domain); + $this->assertNotEmpty($previewDomain); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Hello Appwrite", $response['body']); + $this->assertStringNotContainsString("Preview by", $response['body']); + + $contentLength = $response['headers']['content-length']; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $previewDomain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Hello Appwrite", $response['body']); + $this->assertStringContainsString("Preview by", $response['body']); + $this->assertGreaterThan($contentLength, $response['headers']['content-length']); + + $this->cleanupSite($siteId); + } + + public function testSiteCors(): void + { + // Create rule together with site + $subdomain = 'startup' . \uniqid(); + + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Startup site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + 'subdomain' => $subdomain + ]); + + $this->assertNotEmpty($siteId); + + $this->setupSiteDomain($siteId, $subdomain); + $domain = $this->getSiteDomain($siteId); + + $this->assertNotEmpty($domain); + + $url = 'http://' . $domain; + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'referer' => $url, + 'origin' => $url + ])); + + $this->assertEquals($url, $response['headers']['access-control-allow-origin']); + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'unknown', + 'referer' => $url, + 'origin' => $url + ])); + + $this->assertNotEquals($url, $response['headers']['access-control-allow-origin']); + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'referer' => 'http://unknown.com', + 'origin' => 'http://unknown.com' + ])); + + $this->assertNotEquals($url, $response['headers']['access-control-allow-origin']); + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + } + + public function testSiteScreenshot(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Themed site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-themed'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->getSiteDomain($siteId); + $this->assertNotEmpty($domain); + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Themed website", $response['body']); + $this->assertStringContainsString("@media (prefers-color-scheme: dark)", $response['body']); + + $deployment = $this->getDeployment($siteId, $deploymentId); + + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['screenshotLight']); + $this->assertNotEmpty($deployment['body']['screenshotDark']); + + $screenshotId = $deployment['body']['screenshotLight']; + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ + ], $this->getHeaders())); + + $this->assertEquals(200, $file['headers']['status-code']); + $this->assertNotEmpty(200, $file['body']); + $this->assertGreaterThan(1, $file['headers']['content-length']); + $this->assertEquals('image/png', $file['headers']['content-type']); + + $screenshotHash = \md5($file['body']); + $this->assertNotEmpty($screenshotHash); + + $screenshotId = $deployment['body']['screenshotDark']; + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([ + ], $this->getHeaders())); + + $this->assertEquals(200, $file['headers']['status-code']); + $this->assertNotEmpty(200, $file['body']); + $this->assertGreaterThan(1, $file['headers']['content-length']); + $this->assertEquals('image/png', $file['headers']['content-type']); + + $screenshotDarkHash = \md5($file['body']); + $this->assertNotEmpty($screenshotDarkHash); + + $this->assertNotEquals($screenshotDarkHash, $screenshotHash); + + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin"); + $this->assertEquals(404, $file['headers']['status-code']); + + $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin"); + $this->assertEquals(404, $file['headers']['status-code']); + + $this->cleanupSite($siteId); + } + + public function testSiteDownload(): void + { + $siteId = $this->setupSite([ + 'buildRuntime' => 'node-22', + 'fallbackFile' => null, + 'framework' => 'other', + 'name' => 'Test Site', + 'adapter' => 'static', + 'outputDirectory' => './', + 'providerBranch' => 'main', + 'providerRootDirectory' => './', + 'siteId' => ID::unique() + ]); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => true + ]); + + $this->assertNotEmpty($deploymentId); + + $response = $this->getDeploymentDownload($siteId, $deploymentId, 'source'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('application/gzip', $response['headers']['content-type']); + $this->assertGreaterThan(0, $response['headers']['content-length']); + $this->assertGreaterThan(0, \strlen($response['body'])); + + $deploymentMd5 = \md5($response['body']); + + $response = $this->getDeploymentDownload($siteId, $deploymentId, 'output'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('application/gzip', $response['headers']['content-type']); + $this->assertGreaterThan(0, $response['headers']['content-length']); + $this->assertGreaterThan(0, \strlen($response['body'])); + + $buildMd5 = \md5($response['body']); + + $this->assertNotEquals($deploymentMd5, $buildMd5); + + $this->cleanupSite($siteId); + } + + public function testSSRLogs(): void + { + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'SSR site', + 'framework' => 'astro', + 'adapter' => 'ssr', + 'buildRuntime' => 'node-22', + 'outputDirectory' => './dist', + 'buildCommand' => 'npm run build', + 'installCommand' => 'npm install', + 'fallbackFile' => '', + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + + $deploymentId = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('astro'), + 'activate' => 'true' + ]); + + $this->assertNotEmpty($deploymentId); + + $domain = $this->getSiteDomain($siteId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/logs-inline'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Inline logs printed.", $response['body']); + + $logs = $this->listLogs($siteId, [ + Query::orderDesc('$createdAt')->toString(), + Query::limit(1)->toString(), + ]); + $this->assertEquals(200, $logs['headers']['status-code']); + $this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']); + $this->assertStringContainsString("/logs-inline", $logs['body']['executions'][0]['requestPath']); + $this->assertStringContainsString("Log1", $logs['body']['executions'][0]['logs']); + $this->assertStringContainsString("Log2", $logs['body']['executions'][0]['logs']); + $this->assertStringContainsString("Error1", $logs['body']['executions'][0]['errors']); + $this->assertStringContainsString("Error2", $logs['body']['executions'][0]['errors']); + $log1Id = $logs['body']['executions'][0]['$id']; + $this->assertNotEmpty($log1Id); + + $response = $proxyClient->call(Client::METHOD_GET, '/logs-action'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Action logs printed.", $response['body']); + + $logs = $this->listLogs($siteId, [ + Query::orderDesc('$createdAt')->toString(), + Query::limit(1)->toString(), + ]); + $this->assertEquals(200, $logs['headers']['status-code']); + $this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']); + $this->assertStringContainsString("/logs-action", $logs['body']['executions'][0]['requestPath']); + $this->assertStringContainsString("Log1", $logs['body']['executions'][0]['logs']); + $this->assertStringContainsString("Log2", $logs['body']['executions'][0]['logs']); + $this->assertStringContainsString("Error1", $logs['body']['executions'][0]['errors']); + $this->assertStringContainsString("Error2", $logs['body']['executions'][0]['errors']); + $log2Id = $logs['body']['executions'][0]['$id']; + $this->assertNotEmpty($log2Id); + + $this->assertNotEquals($log1Id, $log2Id); + + $this->cleanupSite($siteId); + } + + public function testDuplicateDeployment(): void + { + $siteId = $this->setupSite([ + 'buildRuntime' => 'node-22', + 'framework' => 'other', + 'name' => 'Duplicate deployment Site', + 'adapter' => 'static', + 'fallbackFile' => '404.html', + 'siteId' => ID::unique() + ]); + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + $this->assertNotEmpty($domain); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $deploymentId1 = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-spa'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentId1); + + $response = $proxyClient->call(Client::METHOD_GET, '/not-found'); + $this->assertStringContainsString("Customized 404 page", $response['body']); + + $site = $this->updateSite([ + '$id' => $siteId, + 'buildRuntime' => 'node-22', + 'framework' => 'other', + 'name' => 'Duplicate deployment Site', + 'adapter' => 'static', + 'fallbackFile' => 'index.html', + ]); + $this->assertEquals(200, $site['headers']['status-code']); + $this->assertEquals('index.html', $site['body']['fallbackFile']); + + $deploymentId2 = $this->setupDuplicateDeployment($siteId, $deploymentId1); + $this->assertNotEmpty($deploymentId2); + + $response = $proxyClient->call(Client::METHOD_GET, '/not-found'); + $this->assertStringContainsString("Index page", $response['body']); + + $this->cleanupSite($siteId); + } + + public function testUpdateDeploymentStatus(): void + { + $siteId = $this->setupSite([ + 'buildRuntime' => 'node-22', + 'framework' => 'other', + 'name' => 'Activate test Site', + 'siteId' => ID::unique(), + 'adapter' => 'static', + ]); + $this->assertNotEmpty($siteId); + + $domain = $this->setupSiteDomain($siteId); + $this->assertNotEmpty($domain); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $deploymentId1 = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentId1); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('Hello Appwrite', $response['body']); + + $deploymentId2 = $this->setupDeployment($siteId, [ + 'code' => $this->packageSite('static-spa'), + 'activate' => true + ]); + $this->assertNotEmpty($deploymentId2); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('Index page', $response['body']); + + $function = $this->getSite($siteId); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($deploymentId2, $function['body']['deploymentId']); + + $function = $this->updateSiteDeployment($siteId, $deploymentId1); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($deploymentId1, $function['body']['deploymentId']); + + $function = $this->getSite($siteId); + $this->assertEquals(200, $function['headers']['status-code']); + $this->assertEquals($deploymentId1, $function['body']['deploymentId']); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString('Hello Appwrite', $response['body']); + + $this->cleanupSite($siteId); + } } diff --git a/tests/e2e/Services/Teams/TeamsBaseClient.php b/tests/e2e/Services/Teams/TeamsBaseClient.php index 3aad86c670..3fcd9c043d 100644 --- a/tests/e2e/Services/Teams/TeamsBaseClient.php +++ b/tests/e2e/Services/Teams/TeamsBaseClient.php @@ -226,10 +226,6 @@ trait TeamsBaseClient $this->assertEquals($response['body']['teamId'], substr($lastEmail['text'], strpos($lastEmail['text'], '&teamId=', 0) + 8, 20)); $this->assertEquals($teamName, substr($lastEmail['text'], strpos($lastEmail['text'], '&teamName=', 0) + 10, 7)); - $secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256); - $membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20); - $userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20); - /** * Test with UserId * Create user @@ -308,6 +304,11 @@ trait TeamsBaseClient $this->assertEquals(201, $response['headers']['status-code']); + $lastEmail = $this->getLastEmail(); + $membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20); + $userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20); + $secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256); + /** * Test for FAILURE */ diff --git a/tests/e2e/Services/Teams/TeamsBaseServer.php b/tests/e2e/Services/Teams/TeamsBaseServer.php index bade16cf2f..0c6d85e276 100644 --- a/tests/e2e/Services/Teams/TeamsBaseServer.php +++ b/tests/e2e/Services/Teams/TeamsBaseServer.php @@ -175,17 +175,10 @@ trait TeamsBaseServer $userUid = $response['body']['userId']; $membershipUid = $response['body']['$id']; - // $response = $this->client->call(Client::METHOD_GET, '/users/'.$userUid, array_merge([ - // 'content-type' => 'application/json', - // 'x-appwrite-project' => $this->getProject()['$id'], - // ], $this->getHeaders()), []); + /** + * Test for FAILURE + */ - // $this->assertEquals($userUid, $response['body']['$id']); - // $this->assertContains('team:'.$teamUid, $response['body']['roles']); - // $this->assertContains('team:'.$teamUid.'/admin', $response['body']['roles']); - // $this->assertContains('team:'.$teamUid.'/editor', $response['body']['roles']); - - // test for resending invitation $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -196,11 +189,7 @@ trait TeamsBaseServer 'url' => 'http://localhost:5000/join-us#title' ]); - $this->assertEquals(201, $response['headers']['status-code']); - - /** - * Test for FAILURE - */ + $this->assertEquals(409, $response['headers']['status-code']); // membership already created $response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([ 'content-type' => 'application/json', diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml index da7c4f0833..67bb9a1aef 100644 --- a/tests/resources/docker/docker-compose.yml +++ b/tests/resources/docker/docker-compose.yml @@ -87,9 +87,9 @@ services: - _APP_COMPUTE_MEMORY - _APP_EXECUTOR_HOST - appwrite-worker-usage: - entrypoint: worker-usage - container_name: appwrite-worker-usage + appwrite-worker-stats-usage: + entrypoint: worker-stats-usage + container_name: appwrite-worker-stats-usage build: context: . restart: unless-stopped diff --git a/tests/resources/functions/node/index.js b/tests/resources/functions/node/index.js index 041e4a8c12..e8eb938a15 100644 --- a/tests/resources/functions/node/index.js +++ b/tests/resources/functions/node/index.js @@ -14,6 +14,8 @@ module.exports = async(context) => { 'APPWRITE_FUNCTION_USER_ID' : context.req.headers['x-appwrite-user-id'] ?? '', 'APPWRITE_FUNCTION_JWT' : context.req.headers['x-appwrite-user-jwt'] ?? '', 'APPWRITE_FUNCTION_PROJECT_ID' : process.env.APPWRITE_FUNCTION_PROJECT_ID, + 'APPWRITE_FUNCTION_MEMORY' : process.env.APPWRITE_FUNCTION_MEMORY, + 'APPWRITE_FUNCTION_CPUS' : process.env.APPWRITE_FUNCTION_CPUS, 'CUSTOM_VARIABLE' : process.env.CUSTOM_VARIABLE }); } \ No newline at end of file diff --git a/tests/resources/functions/node/maintenance.js b/tests/resources/functions/node/maintenance.js new file mode 100644 index 0000000000..0885f11041 --- /dev/null +++ b/tests/resources/functions/node/maintenance.js @@ -0,0 +1,3 @@ +module.exports = async(context) => { + return context.res.send('Maintenance'); +} \ No newline at end of file diff --git a/tests/resources/functions/php-large/index.php b/tests/resources/functions/php-large/index.php index 98104fbf35..abcb2e53d9 100644 --- a/tests/resources/functions/php-large/index.php +++ b/tests/resources/functions/php-large/index.php @@ -9,8 +9,8 @@ return function ($context) { 'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '', 'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '', - 'APPWRITE_COMPUTE_CPUS' => \getenv('APPWRITE_COMPUTE_CPUS') ?: '', - 'APPWRITE_COMPUTE_MEMORY' => \getenv('APPWRITE_COMPUTE_MEMORY') ?: '', + 'APPWRITE_FUNCTION_CPUS' => \getenv('APPWRITE_FUNCTION_CPUS') ?: '', + 'APPWRITE_FUNCTION_MEMORY' => \getenv('APPWRITE_FUNCTION_MEMORY') ?: '', 'UNICODE_TEST' => "êä" ]); }; diff --git a/tests/resources/functions/php/index.php b/tests/resources/functions/php/index.php index c115a78bf3..27a9418b3c 100644 --- a/tests/resources/functions/php/index.php +++ b/tests/resources/functions/php/index.php @@ -27,7 +27,7 @@ return function ($context) { 'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '', 'UNICODE_TEST' => "êä", 'GLOBAL_VARIABLE' => \getenv('GLOBAL_VARIABLE') ?: '', - 'APPWRITE_COMPUTE_CPUS' => \getenv('APPWRITE_COMPUTE_CPUS') ?: '', - 'APPWRITE_COMPUTE_MEMORY' => \getenv('APPWRITE_COMPUTE_MEMORY') ?: '', + 'APPWRITE_FUNCTION_CPUS' => \getenv('APPWRITE_FUNCTION_CPUS') ?: '', + 'APPWRITE_FUNCTION_MEMORY' => \getenv('APPWRITE_FUNCTION_MEMORY') ?: '', ], \intval($statusCode)); }; diff --git a/tests/resources/sites/astro/astro.config.mjs b/tests/resources/sites/astro/astro.config.mjs new file mode 100644 index 0000000000..a87942461e --- /dev/null +++ b/tests/resources/sites/astro/astro.config.mjs @@ -0,0 +1,9 @@ +import { defineConfig } from 'astro/config'; +import node from '@astrojs/node'; + +export default defineConfig({ + output: 'server', + adapter: node({ + mode: 'standalone' + }), +}); diff --git a/tests/resources/sites/astro/package.json b/tests/resources/sites/astro/package.json new file mode 100644 index 0000000000..8cbcb910f5 --- /dev/null +++ b/tests/resources/sites/astro/package.json @@ -0,0 +1,15 @@ +{ + "name": "my-astro-app", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/node": "^9.0.2", + "astro": "^5.2.5" + } +} diff --git a/tests/resources/sites/astro/src/pages/index.astro b/tests/resources/sites/astro/src/pages/index.astro new file mode 100644 index 0000000000..cc8fd9411f --- /dev/null +++ b/tests/resources/sites/astro/src/pages/index.astro @@ -0,0 +1,15 @@ +--- +const value = import.meta.env.name || 'Variable not found'; +--- + + + + + + + Astro SSR + + +

Env variable is {value}

+ + diff --git a/tests/resources/sites/astro/src/pages/logs-action.js b/tests/resources/sites/astro/src/pages/logs-action.js new file mode 100644 index 0000000000..348297a616 --- /dev/null +++ b/tests/resources/sites/astro/src/pages/logs-action.js @@ -0,0 +1,7 @@ +export async function GET(context) { + console.log("Log1"); + console.error("Error1"); + console.log("Log2"); + console.error("Error2"); + return new Response('Action logs printed.'); +} diff --git a/tests/resources/sites/astro/src/pages/logs-inline.astro b/tests/resources/sites/astro/src/pages/logs-inline.astro new file mode 100644 index 0000000000..c539142d83 --- /dev/null +++ b/tests/resources/sites/astro/src/pages/logs-inline.astro @@ -0,0 +1,18 @@ +--- +console.log("Log1"); +console.error("Error1"); +console.log("Log2"); +console.error("Error2"); +--- + + + + + + + Astro Logs + + +

Inline logs printed.

+ + diff --git a/tests/resources/sites/static-spa/404.html b/tests/resources/sites/static-spa/404.html new file mode 100644 index 0000000000..2a51f36d22 --- /dev/null +++ b/tests/resources/sites/static-spa/404.html @@ -0,0 +1,10 @@ + + + + + + + +

Customized 404 page

+ + diff --git a/tests/resources/sites/static-spa/contact.html b/tests/resources/sites/static-spa/contact.html new file mode 100644 index 0000000000..1ef7dc9497 --- /dev/null +++ b/tests/resources/sites/static-spa/contact.html @@ -0,0 +1,10 @@ + + + + + + + +

Contact page

+ + diff --git a/tests/resources/sites/static-spa/index.html b/tests/resources/sites/static-spa/index.html new file mode 100644 index 0000000000..3fd2262803 --- /dev/null +++ b/tests/resources/sites/static-spa/index.html @@ -0,0 +1,10 @@ + + + + + + + +

Index page

+ + diff --git a/tests/resources/sites/static-themed/index.html b/tests/resources/sites/static-themed/index.html new file mode 100644 index 0000000000..955696b473 --- /dev/null +++ b/tests/resources/sites/static-themed/index.html @@ -0,0 +1,23 @@ + + + + + + Themed website + + + + diff --git a/tests/resources/sites/static/contact.html b/tests/resources/sites/static/contact.html new file mode 100644 index 0000000000..b2c16fc471 --- /dev/null +++ b/tests/resources/sites/static/contact.html @@ -0,0 +1,11 @@ + + + + + + Contact page + + +

Contact page

+ + \ No newline at end of file diff --git a/tests/unit/Auth/KeyTest.php b/tests/unit/Auth/KeyTest.php new file mode 100644 index 0000000000..8ae2114697 --- /dev/null +++ b/tests/unit/Auth/KeyTest.php @@ -0,0 +1,56 @@ + $projectId,]); + $decoded = Key::decode($project, $key); + + $this->assertEquals($projectId, $decoded->getProjectId()); + $this->assertEquals(API_KEY_DYNAMIC, $decoded->getType()); + $this->assertEquals(Auth::USER_ROLE_APPS, $decoded->getRole()); + $this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes()); + } + + private static function generateKey( + string $projectId, + bool $usage, + array $scopes, + ): string { + $jwt = new JWT( + key: System::getEnv('_APP_OPENSSL_KEY_V1'), + algo: 'HS256', + maxAge: 86400, + leeway: 0, + ); + + $apiKey = $jwt->encode([ + 'projectId' => $projectId, + 'usage' => $usage, + 'scopes' => $scopes, + ]); + + return API_KEY_DYNAMIC . '_' . $apiKey; + } +} diff --git a/tests/unit/Transformation/TransformationTest.php b/tests/unit/Transformation/TransformationTest.php new file mode 100644 index 0000000000..a3169026a3 --- /dev/null +++ b/tests/unit/Transformation/TransformationTest.php @@ -0,0 +1,43 @@ +addAdapter(new Mock()); + + $transformer->setInput($input); + $transformer->setTraits([]); + + $this->assertFalse($transformer->transform()); + + $transformer->setTraits(['mock' => true]); + $this->assertFalse($transformer->transform()); + + $transformer->setTraits(['mock' => true, 'content-type' => 'text/plain']); + $this->assertFalse($transformer->transform()); + + $transformer->setTraits(['mock' => true, 'content-type' => 'tExT/HtML']); + $this->assertFalse($transformer->transform()); + + $transformer->setTraits(['mock' => false, 'content-type' => 'text/plain, text/html; charset=utf-8']); + $this->assertFalse($transformer->transform()); + + $transformer->setTraits(['mock' => true, 'content-type' => 'text/plain, text/html; charset=utf-8']); + $this->assertTrue($transformer->transform()); + + $this->assertStringContainsString("Hello world", $transformer->getOutput()); + $this->assertStringContainsString("Preview by", $transformer->getOutput()); + $this->assertStringContainsString("Mock:", $transformer->getOutput()); + } +}