1
0
Fork 0
mirror of synced 2024-06-29 19:50:26 +12:00

Merge remote-tracking branch 'origin/master' into fix-db-attr-number-params

This commit is contained in:
Jake Barnby 2022-01-27 22:47:15 +13:00
commit 1bcabf8f90
30 changed files with 443 additions and 206 deletions

View file

@ -1,10 +1,9 @@
name: "Tests"
on: [pull_request]
jobs:
tests:
name: Unit & E2E
runs-on: self-hosted
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@ -19,16 +18,29 @@ jobs:
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Build Appwrite
# Upstream bug causes buildkit pulls to fail so prefetch base images
# https://github.com/moby/moby/issues/41864
- name: Prepare Docker
run: |
export COMPOSE_INTERACTIVE_NO_CLI
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
echo "_APP_FUNCTIONS_RUNTIMES=php-8.0" >> .env
docker pull composer:2.0
docker pull php:8.0-cli-alpine
docker compose build --progress=plain
docker compose pull
- name: Prepare Cache
uses: satackey/action-docker-layer-caching@v0.0.11
# Ignore the failure of a step and avoid terminating the job.
continue-on-error: true
- name: Build Appwrite
run: docker compose build --progress=plain
- name: Start Appwrite
run: |
docker compose up -d
sleep 30
- name: Doctor
run: docker compose exec -T appwrite doctor
@ -37,9 +49,3 @@ jobs:
- name: Run Tests
run: docker compose exec -T appwrite test --debug
- name: Teardown
if: always()
run: |
docker compose down -v
docker ps -aq | xargs docker rm --force

View file

@ -1,29 +0,0 @@
#!/bin/bash bash
RED='\033[0;31m'
NC='\033[0m' # No Color
if [ -z "$1" ]
then
echo "Missing tag number"
exit 1
fi
if [ -z "$2" ]
then
echo "Missing version number"
exit 1
fi
if test $(find "./app/db/DBIP/dbip-country-lite-2021-12.mmdb" -mmin +259200)
then
printf "${RED}GEO country DB has not been updated for more than 6 months. Go to https://db-ip.com/db/download/ip-to-country-lite to download a newer version${NC}\n"
fi
echo 'Starting build...'
docker build --build-arg VERSION="$2" --tag appwrite/appwrite:"$1" .
echo 'Pushing build to registry...'
docker push appwrite/appwrite:"$1"

View file

@ -1 +0,0 @@
echo 'Nothing to deploy right now.'

View file

@ -1,87 +0,0 @@
dist: focal
arch:
- amd64
os: linux
vm:
size: large
language: shell
notifications:
email:
- team@appwrite.io
before_install:
# Install latest Docker
- curl -fsSL https://get.docker.com | sh
# Enable Buildkit in Docker config
- echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json
- mkdir -p $HOME/.docker
- echo '{"experimental":"enabled"}' | sudo tee $HOME/.docker/config.json
- sudo service docker start
# Login to increase Docker Hub ratelimit
- >
if [ ! -z "${DOCKERHUB_PULL_USERNAME:-}" ]; then
echo "${DOCKERHUB_PULL_PASSWORD}" | docker login --username "${DOCKERHUB_PULL_USERNAME}" --password-stdin
fi
- docker --version
# Install latest Compose
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/1.29.2/docker-compose-`uname -s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
- docker-compose --version
# Enable Buildkit
- docker buildx create --name travis_builder --use
- export COMPOSE_INTERACTIVE_NO_CLI
- export DOCKER_BUILDKIT=1
- export COMPOSE_DOCKER_CLI_BUILD=1
- export BUILDKIT_PROGRESS=plain
# Only pass a single runtime for CI stability
- echo "_APP_FUNCTIONS_RUNTIMES=php-8.0" >> .env
# Ensure Travis scripts are executable
- chmod -R u+x ./.travis-ci
install:
- docker-compose pull
# Upstream bug causes buildkit pulls to fail so prefetch base images
# https://github.com/moby/moby/issues/41864
- docker pull composer:2.0
- docker pull php:8.0-cli-alpine
- docker-compose build
- docker-compose up -d
- sleep 60
script:
- docker ps -a
# Tests should fail if any container is in exited status
# - ALL_UP=`docker ps -aq --filter "status=exited"`
# - >
# if [[ "$ALL_UP" != "" ]]; then
# exit 1
# fi
- docker-compose logs appwrite
- docker-compose logs appwrite-realtime
- docker-compose logs mariadb
- docker-compose logs appwrite-worker-functions
- docker-compose exec appwrite doctor
- docker-compose exec appwrite vars
- docker-compose exec appwrite test --debug
after_script:
# travis re-uses their build nodes so clean them up
- docker buildx rm travis_builder
after_failure:
- docker-compose logs appwrite
deploy:
- provider: script
edge: true
script: ./.travis-ci/deploy.sh
on:
repo: appwrite/appwrite
branch: deploy

View file

@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM node:16-alpine as node
FROM node:16.13.2-alpine3.15 as node
WORKDIR /usr/local/src/
@ -24,7 +24,7 @@ COPY public /usr/local/src/public
RUN npm ci
RUN npm run build
FROM php:8.0-cli-alpine as compile
FROM php:8.0.14-cli-alpine3.15 as compile
ARG DEBUG=false
ENV DEBUG=$DEBUG
@ -123,7 +123,7 @@ RUN \
./configure && \
make && make install
FROM php:8.0-cli-alpine as final
FROM php:8.0.14-cli-alpine3.15 as final
LABEL maintainer="team@appwrite.io"

171
README-CN.md Normal file
View file

@ -0,0 +1,171 @@
<br />
<p align="center">
<a href="https://appwrite.io" target="_blank"><img width="260" height="39" src="https://appwrite.io/images/appwrite.svg" alt="Appwrite Logo"></a>
<br />
<br />
<b>适用于[Flutter/Vue/Angular/React/iOS/Android/* 等等平台 *]的完整后端服务</b>
<br />
<br />
</p>
<!-- [![Hacktoberfest](https://img.shields.io/static/v1?label=hacktoberfest&message=friendly&color=90a88b&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)
[![Docker Pulls](https://img.shields.io/docker/pulls/appwrite/appwrite?color=f02e65&style=flat-square)](https://hub.docker.com/r/appwrite/appwrite)
[![Build Status](https://img.shields.io/travis/com/appwrite/appwrite?style=flat-square)](https://travis-ci.com/appwrite/appwrite)
[![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
[![翻译](https://img.shields.io/badge/translate-f02e65?style=flat-square)](docs/tutorials/add-translations.md)
[![周边商店](https://img.shields.io/badge/swag%20store-f02e65?style=flat-square)](https://store.appwrite.io)
[English](README.md) | 简体中文
Appwrite是一个基于dcoker的端到端开发者平台其容器化的微服务库可应用于网页端移动端以及后端。Appwrite 通过视觉化界面极简了从零编写 API 的繁琐过程,在保证软件安全的前提下为开发者创造了一个高效的开发环境。
Appwrite 可以提供给开发者用户验证,外部授权,用户数据读写检索,文件储存, 图像处理,云函数计算,[等多种服务]https/ /appwrite.io/docs
![Appwrite](public/images/github.png)
更多信息请到 Appwrite 官网查看: [https://appwrite.io](https://appwrite.io)
内容:
- [安装](#安装)
- [Unix](#unix)
- [Windows](#windows)
- [CMD](#cmd)
- [PowerShell](#powershell)
- [从旧版本升级](#从旧版本升级)
- [快速入门](#入门)
- [软件服务](#软件服务)
- [开发套件](#开发套件)
- [客户端](#客户端)
- [服务器](#服务器)
- [开发者社区](#开发者社区)
- [软件架构]](#软件架构)
- [贡献代码](#贡献代码)
- [安全](#安全)
- [订阅我们](#订阅我们)
- [版权说明](#版权说明)
## 安装
Appwrite 的容器化服务器只需要一行指令就可以运行。您可以使用 docker-compose 在本地主机上运行 Appwrite也可以在任何其他容器化工具如 Kubernetes、Docker Swarm 或 Rancher上运行 Appwrite。
开始运行 Appwrite 服务器的最简单方法是运行我们的 docker-compose 文件。在运行安装命令之前,请确保您的机器上安装了 [Docker](https://dockerdocs.cn/get-docker/index.html)
### Unix
```bash
docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:0.12.1
```
### Windows
#### CMD
```cmd
docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:0.12.1
```
#### PowerShell
```powershell
docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" ,
appwrite/appwrite:0.12.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
需要自定义容器构架,请查看我们的 Docker [环境变量](https://appwrite.io/docs/environment-variables) 文档。您还可以参考我们的 [docker-compose.yml](https://gist.github.com/eldadfux/977869ff6bdd7312adfd4e629ee15cc5#file-docker-compose-yml) 文件手动设置环境。
### 从旧版本升级
如果您从旧版本升级 Appwrite 服务器,则应在设置完成后使用 Appwrite 迁移工具。有关这方面的更多信息,请查看 [安装文档](https://appwrite.io/docs/installation)。
## 入门
开始使用 Appwrite 只需要在控制台创建一个新项目,选择开发平台,然后抓取我们的开发套件。您可以从以下的教程中找到你喜欢的平台开始使用 Appwrite。
* [开始使用 Web](https://appwrite.io/docs/getting-started-for-web)
* [开始使用 Flutter](https://appwrite.io/docs/getting-started-for-flutter)
* [开始使用 Apple](https://appwrite.io/docs/getting-started-for-apple)
* [开始使用 Android](https://appwrite.io/docs/getting-started-for-android)
* [开始使用 Server](https://appwrite.io/docs/getting-started-for-server)
* [开始使用 CLI](https://appwrite.io/docs/command-line)
### 软件服务
* [**帐户**](https://appwrite.io/docs/client/account) -管理当前用户的帐户和登录方式。跟踪和管理用户 Session登录设备登录方法和查看相关记录。
* [**用户**](https://appwrite.io/docs/server/users) - 在以管理员模式登录时管理和列出所有用户。
* [**团队**](https://appwrite.io/docs/client/teams) - 管理用户分组。邀请成员,管理团队中的用户权限和用户角色。
* [**数据库**](https://appwrite.io/docs/client/database) - 管理数据库文档和文档集。用检索界面来对文档和文档集进行读取,创建,更新,和删除。
* [**贮存**](https://appwrite.io/docs/client/storage) - 管理文件的阅读、创建、删除和预览。设置文件的预览来满足程序的个性化需求。所有文件都由 ClamAV 扫描并安全存储和加密。
* [**云函数**](https://appwrite.io/docs/server/functions) - 在安全隔离的环境中运行自定义代码。这些代码可以被事件CRON或者手动操作触发。
* [**语言适配**](https://appwrite.io/docs/client/locale) - 根据用户所在的的国家和地区做出合适的语言适配。
* [**头像**](https://appwrite.io/docs/client/avatars) -管理用户头像、国家旗帜、浏览器图标、信用卡符号,和生成二维码。
如需完整的 API 界面文档,请访问 [https://appwrite.io/docs](https://appwrite.io/docs)。如需更多教程、新闻和公告,请订阅我们的 [博客](https://medium.com/appwrite-io) 和 加入我们的[Discord 社区](https://discord.gg/GSeTUeA)。
### 开发套件
以下是当前支持的平台和语言列表。如果您想帮助我们为您选择的平台添加支持,您可以访问我们的 [SDK 生成器](https://github.com/appwrite/sdk-generator) 项目并查看我们的 [贡献指南]( https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md
#### 客户端
* ✅ &nbsp; [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护)
* ✅ &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护)
* ✅ &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
* ✅ &nbsp; [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护)
#### 服务器
* ✅ &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护)
* ✅ &nbsp; [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwr实验 团队维护)
* ✅ &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart) - (由 Appwrite 团队维护)
* ✅ &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno) - **公测** (由 Appwrite 团队维护)
* ✅ &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护)
* ✅ &nbsp; [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护)
* ✅ &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin) - **公测** (由 Appwrite 团队维护)
* ✅ &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
* ✅ &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护)
#### 开发者社区
* ✅ &nbsp; [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/))
* ✅ &nbsp; [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (维护者 [fenix-hub @GodotNuts](https://github.com/fenix-hub))
找不到需要的的 SDK - 欢迎通过发起PR来帮助我们完善Appwrite的软件生态环境 [SDK 生成器](https://github.com/appwrite/sdk-generator)!
## 软件架构
![Appwrite 软件架构](docs/specs/overview.drawio.svg)
Appwrite 使用高拓展性的微服务架构。此外Appwrite 支持多种 APIREST、WebSocket 和 即将推出的 GraphQL来迎合您的个性化开发习惯。
Appwrite API 界面层利用后台缓存和任务委派来提供极速的响应时间。后台的 Worker 代理还允许您使用消息队列来处理负载,并精确控制硬件合理分配和成本。您可以在 [贡献指南](CONTRIBUTING.md#architecture-1) 中了解有关我们架构的更多信息。
## 贡献代码
所有代码贡献 - 包括来自具有直接提交更改权限的贡献者 - 都必须提交PR请求并在合并分支之前得到核心开发人员的批准。这是为了确保正确审查所有代码。
我们欢迎所有人提交PR如果您愿意提供帮助可以在 [贡献指南](CONTRIBUTING.md) 中了解有关如何为项目做出贡献的更多信息。
## 安全
为了保护您的隐私请避免在GitHub 上发布安全问题。发送问题至 security@appwrite.io我们将为您做更细致的解答。
## 订阅我们
加入我们在世界各地不断发展的社区!请参阅我们的官方 [博客](https://medium.com/appwrite-io)。在 [Twitter](https://twitter.com/appwrite)、[Facebook 页面](https://www.facebook.com/appwrite.io)、[Facebook 群组](https://www.facebook)、[开发者社区](https://dev.to/appwrite) 等平台订阅我们或加入我们的 [Discord 社区](https://discord.gg/GSeTUeA) 以获得更多帮助,想法和讨论。
## 版权说明
版权详情,访问 [BSD 3-Clause License](./LICENSE)。

View file

@ -16,6 +16,8 @@
[![Translate](https://img.shields.io/badge/translate-f02e65?style=flat-square)](docs/tutorials/add-translations.md)
[![Swag Store](https://img.shields.io/badge/swag%20store-f02e65?style=flat-square)](https://store.appwrite.io)
English | [简体中文](README-CN.md)
[**Appwrite 0.12 has been released! Learn what's new!**](https://dev.to/appwrite/its-here-announcing-the-release-of-appwrite-012-5c8b)
Appwrite is an end-to-end backend server for Web, Mobile, Native, or Backend apps packaged as a set of Docker<nobr> microservices. Appwrite abstracts the complexity and repetitiveness required to build a modern backend API from scratch and allows you to build secure apps faster.
@ -169,4 +171,4 @@ Join our growing community around the world! See our official [Blog](https://med
## License
This repository is available under the [BSD 3-Clause License](./LICENSE).
This repository is available under the [BSD 3-Clause License](./LICENSE).

View file

@ -7,35 +7,35 @@ return [
'name' => 'Email/Password',
'key' => 'emailPassword',
'icon' => '/images/users/email.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateSession',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateSession',
'enabled' => true,
],
'magic-url' => [
'name' => 'Magic URL',
'key' => 'usersAuthMagicURL',
'icon' => '/images/users/magic-url.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateMagicURLSession',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateMagicURLSession',
'enabled' => true,
],
'anonymous' => [
'name' => 'Anonymous',
'key' => 'anonymous',
'icon' => '/images/users/anonymous.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateAnonymousSession',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateAnonymousSession',
'enabled' => true,
],
'invites' => [
'name' => 'Invites',
'key' => 'invites',
'icon' => '/images/users/invites.png',
'docs' => 'https://appwrite.io/docs/client/teams?sdk=web#teamsCreateMembership',
'docs' => 'https://appwrite.io/docs/client/teams?sdk=web-default#teamsCreateMembership',
'enabled' => true,
],
'jwt' => [
'name' => 'JWT',
'key' => 'JWT',
'icon' => '/images/users/jwt.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateJWT',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateJWT',
'enabled' => true,
],
'phone' => [

View file

@ -166,8 +166,6 @@ App::post('/v1/database/collections')
$collectionId = $collectionId == 'unique()' ? $dbForProject->getId() : $collectionId;
try {
$dbForProject->createCollection('collection_' . $collectionId);
$collection = $dbForProject->createDocument('collections', new Document([
'$id' => $collectionId,
'$read' => $read ?? [], // Collection permissions for collection documents (based on permission model)
@ -179,8 +177,12 @@ App::post('/v1/database/collections')
'name' => $name,
'search' => implode(' ', [$collectionId, $name]),
]));
$dbForProject->createCollection('collection_' . $collectionId);
} catch (DuplicateException $th) {
throw new Exception('Collection already exists', 409);
} catch (LimitException $th) {
throw new Exception('Collection limit exceeded', 400);
}
$audits
@ -230,7 +232,7 @@ App::get('/v1/database/collections')
$queries = [];
if (!empty($search)) {
$queries[] = new Query('name', Query::TYPE_SEARCH, [$search]);
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
}
$usage->setParam('database.collections.read', 1);

View file

@ -46,7 +46,7 @@ App::post('/v1/functions')
->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, 900), 'Function maximum execution time in seconds.', true)
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true)
->inject('response')
->inject('dbForProject')
->action(function ($functionId, $name, $execute, $runtime, $vars, $events, $schedule, $timeout, $response, $dbForProject) {
@ -294,7 +294,7 @@ App::put('/v1/functions/:functionId')
->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, 900), 'Maximum execution time in seconds.', true)
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true)
->inject('response')
->inject('dbForProject')
->inject('project')
@ -714,7 +714,7 @@ App::post('/v1/functions/:functionId/executions')
throw new Exception('Tag not found. Deploy tag before trying to execute a function', 404);
}
$validator = new Authorization($function, 'execute');
$validator = new Authorization('execute');
if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function
throw new Exception($validator->getDescription(), 401);

View file

@ -16,6 +16,7 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
@ -761,7 +762,11 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception('Team not found', 404);
}
if (!$dbForProject->deleteDocument('memberships', $membership->getId())) {
try {
$dbForProject->deleteDocument('memberships', $membership->getId());
} catch (AuthorizationException $exception) {
throw new Exception('Unauthorized permissions', 401);
} catch (\Exception $exception) {
throw new Exception('Failed to remove membership from DB', 500);
}
@ -782,7 +787,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
if ($membership->getAttribute('confirm')) { // Count only confirmed members
$team->setAttribute('sum', \max($team->getAttribute('sum', 0) - 1, 0));
$team = $dbForProject->updateDocument('teams', $team->getId(), $team);
Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
}
$audits

View file

@ -167,6 +167,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
break;
case version_compare ($responseFormat , '0.8.0', '<=') :
Response::setFilter(new V08());
break;
case version_compare ($responseFormat , '0.11.0', '<=') :
Response::setFilter(new V11());
break;

View file

@ -86,6 +86,9 @@ const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244';
const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
// Database Worker Types
const DATABASE_TYPE_CREATE_ATTRIBUTE = 'createAttribute';
const DATABASE_TYPE_CREATE_INDEX = 'createIndex';

View file

@ -91,13 +91,32 @@ $server->error($logError);
function getDatabase(Registry &$register, string $namespace)
{
$db = $register->get('dbPool')->get();
$redis = $register->get('redisPool')->get();
$attempts = 0;
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace);
do {
try {
$attempts++;
$db = $register->get('dbPool')->get();
$redis = $register->get('redisPool')->get();
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace);
if (!$database->exists($database->getDefaultDatabase(), 'realtime')) {
throw new Exception('Collection not ready');
}
break; // leave loop if successful
} catch(\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: '. $e->getMessage());
}
sleep(DATABASE_RECONNECT_SLEEP);
}
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
return [
$database,
@ -106,6 +125,7 @@ function getDatabase(Registry &$register, string $namespace)
$register->get('redisPool')->put($redis);
}
];
};
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {

View file

@ -620,6 +620,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -675,6 +676,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -739,6 +741,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -803,6 +806,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -855,6 +859,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -911,6 +916,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -963,6 +969,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />
@ -1015,6 +1022,7 @@ $logs = $this->getParam('logs', null);
data-failure="alert"
data-failure-param-alert-text="Failed to create attribute"
data-failure-param-alert-classname="error"
@reset="array = required = false"
x-data="{ array: false, required: false }">
<input type="hidden" name="projectId" data-ls-bind="{{router.params.project}}" />

View file

@ -174,7 +174,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<input type="email" class="full-width" id="user-email" name="email" required autocomplete="off" />
<label for="user-password">Password</label>
<input type="password" class="full-width" id="user-password" name="password" required pattern=".{6,}" title="Six or more characters" autocomplete="off" />
<input type="password" class="full-width" id="user-password" name="password" required minlength="8" title="Eight or more characters" autocomplete="off" />
<hr />

View file

@ -28,10 +28,10 @@
<input type="hidden" name="secret" data-ls-bind="{{router.params.secret}}">
<label>Password</label>
<input name="password" type="password" autocomplete="off" placeholder="" required data-forms-password-meter pattern=".{6,}" title="Six or more characters">
<input name="password" type="password" autocomplete="off" placeholder="" required data-forms-password-meter minlength="8" title="Eight or more characters">
<label>Confirm Password</label>
<input name="passwordAgain" type="password" autocomplete="off" placeholder="" required data-forms-password-meter pattern=".{6,}" title="Six or more characters">
<input name="passwordAgain" type="password" autocomplete="off" placeholder="" required data-forms-password-meter minlength="8" title="Eight or more characters">
<button type="submit" class="btn btn-primary"><i class="fa fa-sign-in"></i> Apply</button>
</form>

View file

@ -37,7 +37,7 @@ $root = ($this->getParam('root') !== 'disabled');
<input name="email" type="email" class="full-width" autocomplete="email" placeholder="Email" required>
<input name="password" type="password" class="full-width" autocomplete="off" placeholder="Password" required pattern=".{6,}" title="Six or more characters">
<input name="password" type="password" class="full-width" autocomplete="off" placeholder="Password" required minlength="8" title="Eight or more characters">
<button>Sign In</button>
</form>

View file

@ -46,7 +46,7 @@ $root = ($this->getParam('root') !== 'disabled');
<input name="email" type="email" autocomplete="email" placeholder="" required data-ls-bind="{{router.params.email}}">
<label>Password</label>
<input name="password" type="password" autocomplete="off" placeholder="" required data-forms-password-meter pattern=".{8,}" title="Eight or more characters">
<input name="password" type="password" autocomplete="off" placeholder="" required data-forms-password-meter minlength="8" title="Eight or more characters">
<div class="agree margin-top-large margin-bottom-large">
<div class="pull-start margin-end-small margin-bottom">

View file

@ -46,7 +46,7 @@
"utopia-php/cache": "0.4.*",
"utopia-php/cli": "0.11.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.13.*",
"utopia-php/database": "0.14.*",
"utopia-php/locale": "0.4.*",
"utopia-php/orchestration": "0.2.*",
"utopia-php/registry": "0.5.*",

31
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f022f43cc2d6023c3dad3805b7c4455a",
"content-hash": "ab493f0a7f01a1105f8bc5caaf9b928b",
"packages": [
{
"name": "adhocore/jwt",
@ -355,16 +355,16 @@
},
{
"name": "composer/package-versions-deprecated",
"version": "1.11.99.4",
"version": "1.11.99.5",
"source": {
"type": "git",
"url": "https://github.com/composer/package-versions-deprecated.git",
"reference": "b174585d1fe49ceed21928a945138948cb394600"
"reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600",
"reference": "b174585d1fe49ceed21928a945138948cb394600",
"url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b4f54f74ef3453349c24a845d22392cd31e65f1d",
"reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d",
"shasum": ""
},
"require": {
@ -408,7 +408,7 @@
"description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
"support": {
"issues": "https://github.com/composer/package-versions-deprecated/issues",
"source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4"
"source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5"
},
"funding": [
{
@ -424,7 +424,7 @@
"type": "tidelift"
}
],
"time": "2021-09-13T08:41:34+00:00"
"time": "2022-01-17T14:14:24+00:00"
},
{
"name": "dragonmantank/cron-expression",
@ -2141,16 +2141,16 @@
},
{
"name": "utopia-php/database",
"version": "0.13.2",
"version": "0.14.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "bf92279b707b3a10ee5ec5df5c065023b2221357"
"reference": "2f2527bb080cf578fba327ea2ec637064561d403"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/bf92279b707b3a10ee5ec5df5c065023b2221357",
"reference": "bf92279b707b3a10ee5ec5df5c065023b2221357",
"url": "https://api.github.com/repos/utopia-php/database/zipball/2f2527bb080cf578fba327ea2ec637064561d403",
"reference": "2f2527bb080cf578fba327ea2ec637064561d403",
"shasum": ""
},
"require": {
@ -2198,9 +2198,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.13.2"
"source": "https://github.com/utopia-php/database/tree/0.14.0"
},
"time": "2022-01-04T10:51:22+00:00"
"time": "2022-01-21T16:34:34+00:00"
},
{
"name": "utopia-php/domains",
@ -3703,9 +3703,6 @@
"require": {
"php": "^7.1 || ^8.0"
},
"replace": {
"myclabs/deep-copy": "self.version"
},
"require-dev": {
"doctrine/collections": "^1.0",
"doctrine/common": "^2.6",
@ -6669,5 +6666,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.1.0"
"plugin-api-version": "2.2.0"
}

View file

@ -1 +1 @@
Use this endpoint to log out the currently logged in user from all their account sessions across all of their different devices. When using the option id argument, only the session unique ID provider will be deleted.
Use this endpoint to log out the currently logged in user from all their account sessions across all of their different devices. When using the Session ID argument, only the unique session ID provided is deleted.

28
package-lock.json generated
View file

@ -9,8 +9,8 @@
"version": "0.1.0",
"license": "BSD-3-Clause",
"dependencies": {
"chart.js": "^3.6.2",
"markdown-it": "^12.3.0",
"chart.js": "^3.7.0",
"markdown-it": "^12.3.2",
"pell": "^1.0.6",
"prismjs": "^1.25.0",
"turndown": "^7.1.1"
@ -549,9 +549,9 @@
}
},
"node_modules/chart.js": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg=="
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz",
"integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg=="
},
"node_modules/chokidar": {
"version": "2.1.8",
@ -2862,9 +2862,9 @@
}
},
"node_modules/markdown-it": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.0.tgz",
"integrity": "sha512-T345UZZ6ejQWTjG6PSEHplzNy5m4kF6zvUpHVDv8Snl/pEU0OxIK0jGg8YLVNwJvT8E0YJC7/2UvssJDk/wQCQ==",
"version": "12.3.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
"dependencies": {
"argparse": "^2.0.1",
"entities": "~2.1.0",
@ -5484,9 +5484,9 @@
"dev": true
},
"chart.js": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg=="
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz",
"integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg=="
},
"chokidar": {
"version": "2.1.8",
@ -7413,9 +7413,9 @@
}
},
"markdown-it": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.0.tgz",
"integrity": "sha512-T345UZZ6ejQWTjG6PSEHplzNy5m4kF6zvUpHVDv8Snl/pEU0OxIK0jGg8YLVNwJvT8E0YJC7/2UvssJDk/wQCQ==",
"version": "12.3.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
"requires": {
"argparse": "^2.0.1",
"entities": "~2.1.0",

View file

@ -17,8 +17,8 @@
"gulp-less": "^5.0.0"
},
"dependencies": {
"chart.js": "^3.6.2",
"markdown-it": "^12.3.0",
"chart.js": "^3.7.0",
"markdown-it": "^12.3.2",
"pell": "^1.0.6",
"prismjs": "^1.25.0",
"turndown": "^7.1.1"

View file

@ -70,9 +70,6 @@ abstract class Worker
throw new Exception("Please implement getName method in worker");
}
const MAX_ATTEMPTS = 10;
const SLEEP_TIME = 2;
const DATABASE_PROJECT = 'project';
const DATABASE_CONSOLE = 'console';
@ -174,7 +171,7 @@ abstract class Worker
global $register;
$namespace = '';
$sleep = self::SLEEP_TIME; // overwritten when necessary
$sleep = DATABASE_RECONNECT_SLEEP; // overwritten when necessary
switch ($type) {
case self::DATABASE_PROJECT:
@ -201,18 +198,24 @@ abstract class Worker
$database = new Database(new MariaDB($register->get('db')), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace); // Main DB
if (!empty($projectId) && !$database->getDocument('projects', $projectId)->isEmpty()) {
throw new \Exception("Project does not exist: {$projectId}");
}
if ($type === self::DATABASE_CONSOLE && !$database->exists($database->getDefaultDatabase(), 'realtime')) {
throw new \Exception('Console project not ready');
}
break; // leave loop if successful
} catch(\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= self::MAX_ATTEMPTS) {
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: '. $e->getMessage());
}
sleep($sleep);
}
} while ($attempts < self::MAX_ATTEMPTS);
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
return $database;
}

View file

@ -33,7 +33,7 @@ abstract class Scope extends TestCase
protected function getLastEmail():array
{
sleep(5);
sleep(3);
$emails = json_decode(file_get_contents('http://maildev:1080/email'), true);
@ -46,7 +46,7 @@ abstract class Scope extends TestCase
protected function getLastRequest():array
{
sleep(5);
sleep(2);
$resquest = json_decode(file_get_contents('http://request-catcher:5000/__last_request__'), true);
$resquest['data'] = json_decode($resquest['data'], true);
@ -167,4 +167,4 @@ abstract class Scope extends TestCase
return self::$user[$this->getProject()['$id']];
}
}
}

View file

@ -125,6 +125,39 @@ class DatabaseCustomServerTest extends Scope
$this->assertCount(0, $collections['body']['collections']);
$this->assertEmpty($collections['body']['collections']);
/**
* Test for Search
*/
$collections = $this->client->call(Client::METHOD_GET, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'first'
]);
$this->assertEquals(1, $collections['body']['sum']);
$this->assertEquals('first', $collections['body']['collections'][0]['$id']);
$collections = $this->client->call(Client::METHOD_GET, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'Test'
]);
$this->assertEquals(2, $collections['body']['sum']);
$this->assertEquals('Test 1', $collections['body']['collections'][0]['name']);
$this->assertEquals('Test 2', $collections['body']['collections'][1]['name']);
$collections = $this->client->call(Client::METHOD_GET, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'Nonexistent'
]);
$this->assertEquals(0, $collections['body']['sum']);
/**
* Test for FAILURE
*/
@ -136,6 +169,21 @@ class DatabaseCustomServerTest extends Scope
]);
$this->assertEquals($response['headers']['status-code'], 400);
// This collection already exists
$response = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Test 1',
'collectionId' => 'first',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document'
]);
$this->assertEquals($response['headers']['status-code'], 409);
}
public function testDeleteAttribute(): array

View file

@ -218,6 +218,32 @@ class FunctionsCustomClientTest extends Scope
];
}
public function testCreateExecutionUnauthorized():array
{
$function = $this->client->call(Client::METHOD_POST, '/functions', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'functionId' => 'unique()',
'name' => 'Test',
'execute' => [],
'runtime' => 'php-8.0',
'timeout' => 10,
]);
$execution = $this->client->call(Client::METHOD_POST, '/functions/'.$function['body']['$id'].'/executions', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'async' => 1,
]);
$this->assertEquals(401, $execution['headers']['status-code']);
return [];
}
/**
* @depends testCreateCustomExecution
*/

View file

@ -66,6 +66,7 @@ trait RealtimeBase
$this->assertEquals('error', $payload['type']);
$this->assertEquals(1008, $payload['data']['code']);
$this->assertEquals('Missing or unknown project ID', $payload['data']['message']);
\usleep(250000); // 250ms
$this->expectException(ConnectionException::class); // Check if server disconnnected client
$client->close();
}

View file

@ -391,11 +391,75 @@ trait TeamsBaseClient
{
$teamUid = $data['teamUid'] ?? '';
$membershipUid = $data['membershipUid'] ?? '';
$session = $data['session'] ?? '';
$response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid.'/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(2, $response['body']['sum']);
$ownerMembershipUid = $response['body']['memberships'][0]['$id'];
/**
* Test for FAILURE
*/
/**
* Test deleting a membership that does not exists
*/
$response = $this->client->call(Client::METHOD_DELETE, '/teams/'.$teamUid.'/memberships/dne', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'='.$session,
]);
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test deleting another user's membership
*/
$response = $this->client->call(Client::METHOD_DELETE, '/teams/'.$teamUid.'/memberships/'.$ownerMembershipUid, [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'='.$session,
]);
$this->assertEquals(401, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_DELETE, '/teams/'.$teamUid.'/memberships/'.$membershipUid, array_merge([
/**
* Test for when a user other than the owner tries to delete their membership
*/
$response = $this->client->call(Client::METHOD_DELETE, '/teams/'.$teamUid.'/memberships/'.$membershipUid, [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_'.$this->getProject()['$id'].'='.$session,
]);
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEmpty($response['body']);
$response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid.'/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['sum']);
/**
* Test for when the owner tries to delete their membership
*/
$response = $this->client->call(Client::METHOD_DELETE, '/teams/'.$teamUid.'/memberships/'.$ownerMembershipUid, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -404,10 +468,7 @@ trait TeamsBaseClient
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEmpty($response['body']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid.'/memberships/'.$membershipUid, array_merge([
$response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid.'/memberships/'.$ownerMembershipUid, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],