1
0
Fork 0
mirror of synced 2024-05-20 12:42:39 +12:00

Merge branch 'feat-database-indexing' of https://github.com/appwrite/appwrite into sync-realtime-with-db-refactor

This commit is contained in:
Torsten Dittmann 2021-10-07 12:08:24 +02:00
commit 8db80e164d
50 changed files with 3206 additions and 569 deletions

View file

@ -174,7 +174,7 @@ $collections = [
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
'filters' => ['casting'],
],
[
'$id' => 'signed',
@ -214,7 +214,7 @@ $collections = [
'required' => false,
'default' => new stdClass,
'array' => false,
'filters' => ['json'],
'filters' => ['json', 'range'],
],
[
'$id' => 'filters',
@ -510,8 +510,8 @@ $collections = [
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => ['json'],
'array' => false,
'filters' => ['subQueryPlatforms'],
],
[
'$id' => 'webhooks',
@ -521,8 +521,8 @@ $collections = [
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => ['json'],
'array' => false,
'filters' => ['subQueryWebhooks'],
],
[
'$id' => 'keys',
@ -532,8 +532,8 @@ $collections = [
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => ['json'],
'array' => false,
'filters' => ['subQueryKeys'],
],
[
'$id' => 'domains',
@ -543,16 +543,381 @@ $collections = [
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => ['json'],
'array' => false,
'filters' => ['subQueryDomains'],
],
[
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_fulltext_name',
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['name'],
'lengths' => [1024],
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
],
],
'platforms' => [
'$collection' => Database::METADATA,
'$id' => 'platforms',
'name' => 'platforms',
'attributes' => [
[
'$id' => 'projectId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'type',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'name',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'key',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'store',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'hostname',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'dateCreated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'dateUpdated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_project',
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
'domains' => [
'$collection' => Database::METADATA,
'$id' => 'domains',
'name' => 'domains',
'attributes' => [
[
'$id' => 'projectId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'updated',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'domain',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'tld',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'registerable',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'verification',
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'certificateId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_project',
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
'keys' => [
'$collection' => Database::METADATA,
'$id' => 'keys',
'name' => 'keys',
'attributes' => [
[
'$id' => 'projectId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'name',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'scopes',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => true,
'filters' => [],
],
[
'$id' => 'secret',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256, // var_dump of \bin2hex(\random_bytes(128)) => string(256)
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_project',
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
'webhooks' => [
'$collection' => Database::METADATA,
'$id' => 'webhooks',
'name' => 'webhooks',
'attributes' => [
[
'$id' => 'projectId',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'name',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'url',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'httpUser',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'httpPass',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'security',
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'events',
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_project',
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
@ -578,7 +943,7 @@ $collections = [
'$id' => 'email',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 1024,
'size' => 320,
'signed' => true,
'required' => false,
'default' => null,
@ -695,15 +1060,51 @@ $collections = [
'array' => true,
'filters' => ['json'],
],
[
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'deleted',
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_email',
'type' => Database::INDEX_UNIQUE,
'attributes' => ['email'],
'lengths' => [1024],
'lengths' => [320],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_deleted_email',
'type' => Database::INDEX_KEY,
'attributes' => ['deleted', 'email'],
'lengths' => [0, 320],
'orders' => [Database::ORDER_ASC, Database::ORDER_ASC],
],
],
],
@ -993,13 +1394,24 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_fulltext_name',
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['name'],
'lengths' => [1024],
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
],
@ -1273,6 +1685,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@ -1283,10 +1706,10 @@ $collections = [
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_fulltext_name',
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['name'],
'lengths' => [1024],
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
],
@ -1441,13 +1864,24 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_fulltext_name',
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['name'],
'lengths' => [1024],
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
],
@ -1514,6 +1948,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@ -1523,6 +1968,13 @@ $collections = [
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
],
],
],
@ -1631,6 +2083,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@ -1640,6 +2103,13 @@ $collections = [
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_fulltext_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [16384],
'orders' => [Database::ORDER_ASC],
],
],
],
@ -1815,4 +2285,4 @@ $collections = [
]
];
return $collections;
return $collections;

View file

@ -5,6 +5,7 @@ return [
'key' => 'homepage',
'name' => 'Homepage',
'subtitle' => '',
'description' => '',
'controller' => 'web/home.php',
'sdk' => false,
'docs' => false,
@ -16,6 +17,8 @@ return [
'console' => [
'key' => 'console',
'name' => 'Console',
'subtitle' => '',
'description' => '',
'controller' => 'web/console.php',
'sdk' => false,
'docs' => false,
@ -93,6 +96,7 @@ return [
'key' => 'projects',
'name' => 'Projects',
'subtitle' => 'The Project service allows you to manage all the projects in your Appwrite server.',
'description' => '',
'controller' => 'api/projects.php',
'sdk' => true,
'docs' => true,

View file

@ -78,7 +78,9 @@ App::post('/v1/account')
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
$sum = $dbForInternal->count('users', [], APP_LIMIT_USERS);
$sum = $dbForInternal->count('users', [
new Query('deleted', Query::TYPE_EQUAL, [false]),
], APP_LIMIT_USERS);
if ($sum >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501);
@ -105,6 +107,8 @@ App::post('/v1/account')
'sessions' => [],
'tokens' => [],
'memberships' => [],
'search' => implode(' ', [$userId, $email, $name]),
'deleted' => false
]));
} catch (Duplicate $th) {
throw new Exception('Account already exists', 409);
@ -165,7 +169,7 @@ App::post('/v1/account/sessions')
$email = \strtolower($email);
$protocol = $request->getProtocol();
$profile = $dbForInternal->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
$profile = $dbForInternal->findOne('users', [new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
if (!$profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) {
$audits
@ -202,8 +206,8 @@ App::post('/v1/account/sessions')
Authorization::setRole('user:' . $profile->getId());
$session = $dbForInternal->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $profile->getId()])
->setAttribute('$write', ['user:' . $profile->getId()])
->setAttribute('$read', ['user:' . $profile->getId()])
->setAttribute('$write', ['user:' . $profile->getId()])
);
$profile->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
@ -470,13 +474,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$name = $oauth2->getUserName($accessToken);
$email = $oauth2->getUserEmail($accessToken);
$user = $dbForInternal->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
$user = $dbForInternal->findOne('users', [new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
if ($user === false || $user->isEmpty()) { // Last option -> create the user, generate random password
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
$sum = $dbForInternal->count('users', [], APP_LIMIT_COUNT);
$sum = $dbForInternal->count('users', [ new Query('deleted', Query::TYPE_EQUAL, [false]),], APP_LIMIT_COUNT);
if ($sum >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501);
@ -503,6 +507,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'sessions' => [],
'tokens' => [],
'memberships' => [],
'search' => implode(' ', [$userId, $email, $name]),
'deleted' => false
]));
} catch (Duplicate $th) {
throw new Exception('Account already exists', 409);
@ -552,8 +558,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
Authorization::setRole('user:' . $user->getId());
$session = $dbForInternal->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()])
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()])
);
$user = $dbForInternal->updateDocument('users', $user->getId(), $user);
@ -932,7 +938,9 @@ App::post('/v1/account/sessions/anonymous')
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
$sum = $dbForInternal->count('users', [], APP_LIMIT_COUNT);
$sum = $dbForInternal->count('users', [
new Query('deleted', Query::TYPE_EQUAL, [false]),
], APP_LIMIT_COUNT);
if ($sum >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501);
@ -958,6 +966,8 @@ App::post('/v1/account/sessions/anonymous')
'sessions' => [],
'tokens' => [],
'memberships' => [],
'search' => $userId,
'deleted' => false
]));
Authorization::reset();
@ -1325,7 +1335,10 @@ App::patch('/v1/account/name')
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('name', $name));
$user = $dbForInternal->updateDocument('users', $user->getId(), $user
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$user->getId(), $name, $user->getAttribute('email')]))
);
$audits
->setParam('userId', $user->getId())
@ -1424,11 +1437,18 @@ App::patch('/v1/account/email')
}
$email = \strtolower($email);
$profile = $dbForInternal->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
if ($profile) {
throw new Exception('User already registered', 409);
}
try {
$user = $dbForInternal->updateDocument('users', $user->getId(), $user
->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', ''))
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name'), $user->getAttribute('email')]))
);
} catch(Duplicate $th) {
throw new Exception('Email already exists', 409);
@ -1514,6 +1534,8 @@ App::delete('/v1/account')
$protocol = $request->getProtocol();
$user = $dbForInternal->updateDocument('users', $user->getId(), $user->setAttribute('status', false));
// TODO Seems to be related to users.php/App::delete('/v1/users/:userId'). Can we share code between these two? Do todos below apply to users.php?
// TODO delete all tokens or only current session?
// TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later
/*
@ -1760,7 +1782,7 @@ App::post('/v1/account/recovery')
$isAppUser = Auth::isAppUser(Authorization::$roles);
$email = \strtolower($email);
$profile = $dbForInternal->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
$profile = $dbForInternal->findOne('users', [new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
if (!$profile) {
throw new Exception('User not found', 404);
@ -1863,7 +1885,7 @@ App::put('/v1/account/recovery')
$profile = $dbForInternal->getDocument('users', $userId);
if ($profile->isEmpty()) {
if ($profile->isEmpty() || $profile->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -1883,10 +1905,9 @@ App::put('/v1/account/recovery')
);
/**
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
foreach ($tokens as $key => $token) {
if ($recovery === $token->getId()) {
$recovery = $token;
@ -2058,9 +2079,9 @@ App::put('/v1/account/verification')
$profile = $dbForInternal->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
/**
* We act like we're updating and validating
* the verification token but actually we don't need it anymore.
*/
* We act like we're updating and validating
* the verification token but actually we don't need it anymore.
*/
foreach ($tokens as $key => $token) {
if ($token->getId() === $verification) {
$verification = $token;

View file

@ -15,6 +15,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\QueryValidator;
@ -25,32 +26,38 @@ use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\Structure as StructureException;
use Appwrite\Utopia\Response;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Response;
use DeviceDetector\DeviceDetector;
use Utopia\Database\Validator\Authorization;
$attributesCallback = function ($collectionId, $attribute, $response, $dbForInternal, $database, $audits, $usage) {
/** @var Utopia\Database\Document $attribute*/
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
/**
* Create attribute of varying type
*
* @param string $collectionId
* @param Utopia\Database\Document $attribute
* @param Appwrite\Utopia\Response $response
* @param Utopia\Database\Database $dbForInternal
* @param Appwrite\Event\Event $database
* @param Appwrite\Event\Event $audits
* @param Appwrite\Stats\Stats $usage
*
* @return Document Newly created attribute document
*/
function createAttribute($collectionId, $attribute, $response, $dbForInternal, $database, $audits, $usage): Document
{
$attributeId = $attribute->getId();
$type = $attribute->getAttribute('type', '');
$size = $attribute->getAttribute('size', 0);
$required = $attribute->getAttribute('required', true);
$min = $attribute->getAttribute('min', null);
$max = $attribute->getAttribute('max', null);
$signed = $attribute->getAttribute('signed', true); // integers are signed by default
$array = $attribute->getAttribute('array', false);
$format = $attribute->getAttribute('format', '');
$formatOptions = $attribute->getAttribute('formatOptions', []);
$filters = $attribute->getAttribute('filters', []); // filters are hidden from the endpoint
$default = $attribute->getAttribute('default', null);
$default = (empty($default)) ? null : (int)$default;
$collection = $dbForInternal->getDocument('collections', $collectionId);
@ -58,23 +65,19 @@ $attributesCallback = function ($collectionId, $attribute, $response, $dbForInte
throw new Exception('Collection not found', 404);
}
// TODO@kodumbeats how to depend on $size for Text validator length
// Ensure attribute default is within required size
if ($size > 0 && !\is_null($default)) {
$validator = new Text($size);
if (!$validator->isValid($default)) {
throw new Exception('Length of default attribute exceeds attribute size', 400);
}
}
if (!empty($format)) {
if (!Structure::hasFormat($format, $type)) {
throw new Exception("Format {$format} not available for {$type} attributes.", 400);
}
}
// Must throw here since dbForExternal->createAttribute is performed by db worker
if ($required && $default) {
throw new Exception('Cannot set default value for required attribute', 400);
}
try {
$attribute = $dbForInternal->createDocument('attributes', new Document([
$attribute = new Document([
'$id' => $collectionId.'_'.$attributeId,
'key' => $attributeId,
'collectionId' => $collectionId,
@ -87,18 +90,28 @@ $attributesCallback = function ($collectionId, $attribute, $response, $dbForInte
'array' => $array,
'format' => $format,
'formatOptions' => $formatOptions,
'filters' => $filters,
]));
} catch (DuplicateException $th) {
]);
$dbForInternal->checkAttribute($collection, $attribute);
$attribute = $dbForInternal->createDocument('attributes', $attribute);
}
catch (DuplicateException $exception) {
throw new Exception('Attribute already exists', 409);
}
catch (LimitException $exception) {
throw new Exception('Attribute limit exceeded', 400);
}
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
// Pass clone of $attribute object to workers
// so we can later modify Document to fit response model
$clone = clone $attribute;
$database
->setParam('type', DATABASE_TYPE_CREATE_ATTRIBUTE)
->setParam('collection', $collection)
->setParam('document', $attribute)
->setParam('document', $clone)
;
$usage->setParam('database.collections.update', 1);
@ -106,11 +119,12 @@ $attributesCallback = function ($collectionId, $attribute, $response, $dbForInte
$audits
->setParam('event', 'database.attributes.create')
->setParam('resource', 'collection/'.$collection->getId())
->setParam('data', $attribute)
->setParam('data', $clone)
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE);
return $attribute;
};
App::post('/v1/database/collections')
@ -195,12 +209,6 @@ App::get('/v1/database/collections')
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$queries = [];
if (!empty($search)) {
$queries[] = new Query('name', Query::TYPE_SEARCH, [$search]);
}
if (!empty($after)) {
$afterCollection = $dbForInternal->getDocument('collections', $after);
@ -209,6 +217,12 @@ App::get('/v1/database/collections')
}
}
$queries = [];
if (!empty($search)) {
$queries[] = new Query('name', Query::TYPE_SEARCH, [$search]);
}
$usage->setParam('database.collections.read', 1);
$response->dynamic(new Document([
@ -655,7 +669,7 @@ App::post('/v1/database/collections/:collectionId/attributes/string')
->label('sdk.description', '/docs/references/database/create-attribute-string.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_STRING)
->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('size', null, new Integer(), 'Attribute size for text attributes, in number of characters.')
@ -667,14 +681,20 @@ App::post('/v1/database/collections/:collectionId/attributes/string')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) use ($attributesCallback) {
->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
return $attributesCallback($collectionId, new Document([
// Ensure attribute default is within required size
$validator = new Text($size);
if (!is_null($default) && !$validator->isValid($default)) {
throw new Exception($validator->getDescription(), 400);
}
$attribute = createAttribute($collectionId, new Document([
'$id' => $attributeId,
'type' => Database::VAR_STRING,
'size' => $size,
@ -682,6 +702,8 @@ App::post('/v1/database/collections/:collectionId/attributes/string')
'default' => $default,
'array' => $array,
]), $response, $dbForInternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING);
});
App::post('/v1/database/collections/:collectionId/attributes/email')
@ -695,33 +717,35 @@ App::post('/v1/database/collections/:collectionId/attributes/email')
->label('sdk.description', '/docs/references/database/create-attribute-email.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_EMAIL)
->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Text(0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('default', null, new Email(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) use ($attributesCallback) {
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
return $attributesCallback($collectionId, new Document([
$attribute = createAttribute($collectionId, new Document([
'$id' => $attributeId,
'type' => Database::VAR_STRING,
'size' => 254,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => 'email',
'format' => APP_DATABASE_ATTRIBUTE_EMAIL,
]), $response, $dbForInternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL);
});
App::post('/v1/database/collections/:collectionId/attributes/ip')
@ -735,37 +759,39 @@ App::post('/v1/database/collections/:collectionId/attributes/ip')
->label('sdk.description', '/docs/references/database/create-attribute-ip.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_IP)
->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Text(0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('default', null, new IP(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) use ($attributesCallback) {
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
return $attributesCallback($collectionId, new Document([
$attribute = createAttribute($collectionId, new Document([
'$id' => $attributeId,
'type' => Database::VAR_STRING,
'size' => 39,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => 'ip',
'format' => APP_DATABASE_ATTRIBUTE_IP,
]), $response, $dbForInternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP);
});
App::post('/v1/database/collections/:collectionId/attributes/url')
->desc('Create IP Address Attribute')
->desc('Create URL Attribute')
->groups(['api', 'database'])
->label('event', 'database.attributes.create')
->label('scope', 'collections.write')
@ -775,33 +801,35 @@ App::post('/v1/database/collections/:collectionId/attributes/url')
->label('sdk.description', '/docs/references/database/create-attribute-url.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_URL)
->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Text(0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('default', null, new URL(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForInternal')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) use ($attributesCallback) {
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForExternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
return $attributesCallback($collectionId, new Document([
$attribute = createAttribute($collectionId, new Document([
'$id' => $attributeId,
'type' => Database::VAR_STRING,
'size' => 2000,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => 'url',
'format' => APP_DATABASE_ATTRIBUTE_URL,
]), $response, $dbForInternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL);
});
App::post('/v1/database/collections/:collectionId/attributes/integer')
@ -815,7 +843,7 @@ App::post('/v1/database/collections/:collectionId/attributes/integer')
->label('sdk.description', '/docs/references/database/create-attribute-integer.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_INTEGER)
->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
@ -828,26 +856,44 @@ App::post('/v1/database/collections/:collectionId/attributes/integer')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForInternal, $database, $audits, $usage) use ($attributesCallback) {
->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
return $attributesCallback($collectionId, new Document([
// Ensure attribute default is within range
$min = (is_null($min)) ? PHP_INT_MIN : \intval($min);
$max = (is_null($max)) ? PHP_INT_MAX : \intval($max);
$validator = new Range($min, $max, Database::VAR_INTEGER);
if (!is_null($default) && !$validator->isValid($default)) {
throw new Exception($validator->getDescription(), 400);
}
$attribute = createAttribute($collectionId, new Document([
'$id' => $attributeId,
'type' => Database::VAR_INTEGER,
'size' => 0,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => 'int-range',
'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE,
'formatOptions' => [
'min' => (is_null($min)) ? PHP_INT_MIN : \intval($min),
'max' => (is_null($max)) ? PHP_INT_MAX : \intval($max),
'min' => $min,
'max' => $max,
],
]), $response, $dbForInternal, $database, $audits, $usage);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \intval($formatOptions['min']));
$attribute->setAttribute('max', \intval($formatOptions['max']));
}
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_INTEGER);
});
App::post('/v1/database/collections/:collectionId/attributes/float')
@ -861,7 +907,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float')
->label('sdk.description', '/docs/references/database/create-attribute-float.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_FLOAT)
->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
@ -874,26 +920,44 @@ App::post('/v1/database/collections/:collectionId/attributes/float')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForInternal, $database, $audits, $usage) use ($attributesCallback) {
->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
return $attributesCallback($collectionId, new Document([
// Ensure attribute default is within range
$min = (is_null($min)) ? PHP_FLOAT_MIN : \floatval($min);
$max = (is_null($max)) ? PHP_FLOAT_MAX : \floatval($max);
$validator = new Range($min, $max, Database::VAR_FLOAT);
if (!is_null($default) && !$validator->isValid($default)) {
throw new Exception($validator->getDescription(), 400);
}
$attribute = createAttribute($collectionId, new Document([
'$id' => $attributeId,
'type' => Database::VAR_FLOAT,
'required' => $required,
'size' => 0,
'default' => $default,
'array' => $array,
'format' => 'float-range',
'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
'formatOptions' => [
'min' => (is_null($min)) ? PHP_FLOAT_MIN : \floatval($min),
'max' => (is_null($max)) ? PHP_FLOAT_MAX : \floatval($max),
'min' => $min,
'max' => $max,
],
]), $response, $dbForInternal, $database, $audits, $usage);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \floatval($formatOptions['min']));
$attribute->setAttribute('max', \floatval($formatOptions['max']));
}
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_FLOAT);
});
App::post('/v1/database/collections/:collectionId/attributes/boolean')
@ -907,7 +971,7 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean')
->label('sdk.description', '/docs/references/database/create-attribute-boolean.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_BOOLEAN)
->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
@ -918,14 +982,14 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean')
->inject('database')
->inject('audits')
->inject('usage')
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) use ($attributesCallback) {
->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal*/
/** @var Appwrite\Event\Event $database */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Stats\Stats $usage */
return $attributesCallback($collectionId, new Document([
$attribute = createAttribute($collectionId, new Document([
'$id' => $attributeId,
'type' => Database::VAR_BOOLEAN,
'size' => 0,
@ -933,6 +997,8 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean')
'default' => $default,
'array' => $array,
]), $response, $dbForInternal, $database, $audits, $usage);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN);
});
App::get('/v1/database/collections/:collectionId/attributes')
@ -960,13 +1026,7 @@ App::get('/v1/database/collections/:collectionId/attributes')
throw new Exception('Collection not found', 404);
}
$attributes = $collection->getAttributes();
$attributes = array_map(function ($attribute) use ($collection) {
return new Document([\array_merge($attribute, [
'collectionId' => $collection->getId(),
])]);
}, $attributes);
$attributes = $collection->getAttribute('attributes');
$usage->setParam('database.collections.read', 1);
@ -986,7 +1046,14 @@ App::get('/v1/database/collections/:collectionId/attributes/:attributeId')
->label('sdk.description', '/docs/references/database/get-attribute.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE)
->label('sdk.response.model', [
Response::MODEL_ATTRIBUTE_BOOLEAN,
Response::MODEL_ATTRIBUTE_INTEGER,
Response::MODEL_ATTRIBUTE_FLOAT,
Response::MODEL_ATTRIBUTE_EMAIL,
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_STRING,])// needs to be last, since its condition would dominate any other string attribute
->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->inject('response')
@ -1002,22 +1069,32 @@ App::get('/v1/database/collections/:collectionId/attributes/:attributeId')
throw new Exception('Collection not found', 404);
}
$attributes = $collection->getAttributes();
$attribute = $collection->find('$id', $attributeId, 'attributes');
// Search for attribute
$attributeIndex = array_search($attributeId, array_column($attributes, '$id'));
if ($attributeIndex === false) {
if (!$attribute) {
throw new Exception('Attribute not found', 404);
}
$attribute = new Document([\array_merge($attributes[$attributeIndex], [
'collectionId' => $collectionId,
])]);
// Select response model based on type and format
$type = $attribute->getAttribute('type');
$format = $attribute->getAttribute('format');
$model = match($type) {
Database::VAR_BOOLEAN => Response::MODEL_ATTRIBUTE_BOOLEAN,
Database::VAR_INTEGER => Response::MODEL_ATTRIBUTE_INTEGER,
Database::VAR_FLOAT => Response::MODEL_ATTRIBUTE_FLOAT,
Database::VAR_STRING => match($format) {
APP_DATABASE_ATTRIBUTE_EMAIL => Response::MODEL_ATTRIBUTE_EMAIL,
APP_DATABASE_ATTRIBUTE_IP => Response::MODEL_ATTRIBUTE_IP,
APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_URL,
default => Response::MODEL_ATTRIBUTE_STRING,
},
default => Response::MODEL_ATTRIBUTE,
};
$usage->setParam('database.collections.read', 1);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE);
$response->dynamic($attribute, $model);
});
App::delete('/v1/database/collections/:collectionId/attributes/:attributeId')
@ -1060,7 +1137,7 @@ App::delete('/v1/database/collections/:collectionId/attributes/:attributeId')
}
$attribute = $dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
$database
->setParam('type', DATABASE_TYPE_DELETE_ATTRIBUTE)
@ -1167,7 +1244,7 @@ App::post('/v1/database/collections/:collectionId/indexes')
throw new Exception('Index already exists', 409);
}
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
$database
->setParam('type', DATABASE_TYPE_CREATE_INDEX)
@ -1312,7 +1389,7 @@ App::delete('/v1/database/collections/:collectionId/indexes/:indexId')
}
$index = $dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting'));
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
$database
->setParam('type', DATABASE_TYPE_DELETE_INDEX)
@ -1382,13 +1459,28 @@ App::post('/v1/database/collections/:collectionId/documents')
throw new Exception('Collection not found', 404);
}
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('write');
if (!$validator->isValid($collection->getWrite())) {
throw new Exception('Unauthorized permissions', 401);
}
}
$data['$collection'] = $collection->getId(); // Adding this param to make API easier for developers
$data['$id'] = $documentId == 'unique()' ? $dbForExternal->getId() : $documentId;
$data['$read'] = (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? []; // By default set read permissions for user
$data['$write'] = (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? []; // By default set write permissions for user
try {
$document = $dbForExternal->createDocument($collectionId, new Document($data));
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(function() use ($dbForExternal, $collectionId, $data) {
return $dbForExternal->createDocument($collectionId, new Document($data));
});
} else {
$document = $dbForExternal->createDocument($collectionId, new Document($data));
}
}
catch (StructureException $exception) {
throw new Exception($exception->getMessage(), 400);
@ -1446,6 +1538,14 @@ App::get('/v1/database/collections/:collectionId/documents')
throw new Exception('Collection not found', 404);
}
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('read');
if (!$validator->isValid($collection->getRead())) {
throw new Exception('Unauthorized permissions', 401);
}
}
$queries = \array_map(function ($query) {
return Query::parse($query);
}, $queries);
@ -1465,6 +1565,15 @@ App::get('/v1/database/collections/:collectionId/documents')
}
}
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document[] $documents */
$documents = Authorization::skip(function() use ($dbForExternal, $collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $afterDocument) {
return $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $afterDocument ?? null);
});
} else {
$documents = $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $afterDocument ?? null);
}
$usage
->setParam('database.documents.read', 1)
->setParam('collectionId', $collectionId)
@ -1472,7 +1581,7 @@ App::get('/v1/database/collections/:collectionId/documents')
$response->dynamic(new Document([
'sum' => $dbForExternal->count($collectionId, $queries, APP_LIMIT_COUNT),
'documents' => $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $afterDocument ?? null),
'documents' => $documents,
]), Response::MODEL_DOCUMENT_LIST);
});
@ -1504,7 +1613,22 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('Collection not found', 404);
}
$document = $dbForExternal->getDocument($collectionId, $documentId);
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('read');
if (!$validator->isValid($collection->getRead())) {
throw new Exception('Unauthorized permissions', 401);
}
}
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(function() use ($dbForExternal, $collectionId, $documentId) {
return $dbForExternal->getDocument($collectionId, $documentId);
});
} else {
$document = $dbForExternal->getDocument($collectionId, $documentId);
}
if ($document->isEmpty()) {
throw new Exception('No document found', 404);
@ -1553,6 +1677,14 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('Collection not found', 404);
}
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('write');
if (!$validator->isValid($collection->getWrite())) {
throw new Exception('Unauthorized permissions', 401);
}
}
$document = $dbForExternal->getDocument($collectionId, $documentId);
if ($document->isEmpty()) {
@ -1577,7 +1709,14 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
$data['$write'] = (is_null($write)) ? ($document->getWrite() ?? []) : $write; // By default inherit write permissions
try {
$document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data));
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(function() use ($dbForExternal, $collection, $document, $data) {
return $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data));
});
} else {
$document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data));
}
}
catch (AuthorizationException $exception) {
throw new Exception('Unauthorized permissions', 401);
@ -1635,7 +1774,22 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
throw new Exception('Collection not found', 404);
}
$document = $dbForExternal->getDocument($collectionId, $documentId);
// Check collection permissions when enforced
if ($collection->getAttribute('permission') === 'collection') {
$validator = new Authorization('write');
if (!$validator->isValid($collection->getWrite())) {
throw new Exception('Unauthorized permissions', 401);
}
}
if ($collection->getAttribute('permission') === 'collection') {
/** @var Document $document */
$document = Authorization::skip(function() use ($dbForExternal, $collectionId, $documentId) {
return $dbForExternal->getDocument($collectionId, $documentId);
});
} else {
$document = $dbForExternal->getDocument($collectionId, $documentId);
}
if ($document->isEmpty()) {
throw new Exception('No document found', 404);

View file

@ -53,8 +53,9 @@ App::post('/v1/functions')
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$functionId = ($functionId == 'unique()') ? $dbForInternal->getId() : $functionId;
$function = $dbForInternal->createDocument('functions', new Document([
'$id' => $functionId == 'unique()' ? $dbForInternal->getId() : $functionId,
'$id' => $functionId,
'execute' => $execute,
'dateCreated' => time(),
'dateUpdated' => time(),
@ -68,6 +69,7 @@ App::post('/v1/functions')
'schedulePrevious' => 0,
'scheduleNext' => 0,
'timeout' => $timeout,
'search' => implode(' ', [$functionId, $name, $runtime]),
]));
$response->setStatusCode(Response::STATUS_CODE_CREATED);
@ -96,8 +98,6 @@ App::get('/v1/functions')
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, [$search])] : [];
if (!empty($after)) {
$afterFunction = $dbForInternal->getDocument('functions', $after);
@ -105,6 +105,12 @@ App::get('/v1/functions')
throw new Exception("Function '{$after}' for the 'after' value not found.", 400);
}
}
$queries = [];
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
}
$response->dynamic(new Document([
'functions' => $dbForInternal->find('functions', $queries, $limit, $offset, [], [$orderType], $afterFunction ?? null),
@ -271,6 +277,7 @@ App::put('/v1/functions/:functionId')
'schedule' => $schedule,
'scheduleNext' => (int)$next,
'timeout' => $timeout,
'search' => implode(' ', [$functionId, $name, $function->getAttribute('runtime')]),
])));
if ($next && $schedule !== $original) {
@ -447,8 +454,9 @@ App::post('/v1/functions/:functionId/tags')
throw new Exception('Failed moving file', 500);
}
$tagId = $dbForInternal->getId();
$tag = $dbForInternal->createDocument('tags', new Document([
'$id' => $dbForInternal->getId(),
'$id' => $tagId,
'$read' => [],
'$write' => [],
'functionId' => $function->getId(),
@ -456,6 +464,7 @@ App::post('/v1/functions/:functionId/tags')
'command' => $command,
'path' => $path,
'size' => $size,
'search' => implode(' ', [$tagId, $command]),
]));
$usage
@ -495,8 +504,6 @@ App::get('/v1/functions/:functionId/tags')
throw new Exception('Function not found', 404);
}
$queries[] = new Query('functionId', Query::TYPE_EQUAL, [$function->getId()]);
if (!empty($after)) {
$afterTag = $dbForInternal->getDocument('tags', $after);
@ -505,6 +512,14 @@ App::get('/v1/functions/:functionId/tags')
}
}
$queries = [];
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
}
$queries[] = new Query('functionId', Query::TYPE_EQUAL, [$function->getId()]);
$results = $dbForInternal->find('tags', $queries, $limit, $offset, [], [$orderType], $afterTag ?? null);
$sum = $dbForInternal->count('tags', $queries, APP_LIMIT_COUNT);
@ -665,8 +680,10 @@ App::post('/v1/functions/:functionId/executions')
Authorization::disable();
$executionId = $dbForInternal->getId();
$execution = $dbForInternal->createDocument('executions', new Document([
'$id' => $dbForInternal->getId(),
'$id' => $executionId,
'$read' => (!$user->isEmpty()) ? ['user:' . $user->getId()] : [],
'$write' => [],
'dateCreated' => time(),
@ -678,6 +695,7 @@ App::post('/v1/functions/:functionId/executions')
'stdout' => '',
'stderr' => '',
'time' => 0.0,
'search' => implode(' ', [$functionId, $executionId]),
]));
Authorization::reset();
@ -732,10 +750,11 @@ App::get('/v1/functions/:functionId/executions')
->param('functionId', '', new UID(), 'Function unique ID.')
->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('after', '', new UID(), 'ID of the execution used as the starting point for the query, excluding the execution itself. Should be used for efficient pagination when working with large sets of data.', true)
->inject('response')
->inject('dbForInternal')
->action(function ($functionId, $limit, $offset, $after, $response, $dbForInternal) {
->action(function ($functionId, $limit, $offset, $search, $after, $response, $dbForInternal) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
@ -755,13 +774,17 @@ App::get('/v1/functions/:functionId/executions')
}
}
$results = $dbForInternal->find('executions', [
new Query('functionId', Query::TYPE_EQUAL, [$function->getId()]),
], $limit, $offset, [], [Database::ORDER_DESC], $afterExecution ?? null);
$queries = [
new Query('functionId', Query::TYPE_EQUAL, [$function->getId()])
];
$sum = $dbForInternal->count('executions', [
new Query('functionId', Query::TYPE_EQUAL, [$function->getId()]),
], APP_LIMIT_COUNT);
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
}
$results = $dbForInternal->find('executions', $queries, $limit, $offset, [], [Database::ORDER_DESC], $afterExecution ?? null);
$sum = $dbForInternal->count('executions', $queries, APP_LIMIT_COUNT);
$response->dynamic(new Document([
'executions' => $results,

View file

@ -8,6 +8,7 @@ use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
use Utopia\CLI\CLI;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Database\Database;
@ -78,6 +79,7 @@ App::post('/v1/projects')
$auths[$method['key'] ?? ''] = true;
}
$projectId = ($projectId == 'unique()') ? $dbForConsole->getId() : $projectId;
$project = $dbForConsole->createDocument('projects', new Document([
'$id' => $projectId == 'unique()' ? $dbForConsole->getId() : $projectId,
'$read' => ['team:' . $teamId],
@ -95,11 +97,13 @@ App::post('/v1/projects')
'legalAddress' => $legalAddress,
'legalTaxId' => $legalTaxId,
'services' => new stdClass(),
'platforms' => [],
'webhooks' => [],
'keys' => [],
'domains' => [],
'platforms' => null,
'providers' => [],
'webhooks' => null,
'keys' => null,
'domains' => null,
'auths' => $auths,
'search' => implode(' ', [$projectId, $name]),
]));
$collections = Config::getParam('collections2', []); /** @var array $collections */
@ -112,7 +116,7 @@ App::post('/v1/projects')
$audit = new Audit($dbForInternal);
$audit->setup();
$adapter = new TimeLimit("", 0, 1, $dbForInternal);
$adapter = new TimeLimit('', 0, 1, $dbForInternal);
$adapter->setup();
foreach ($collections as $key => $collection) {
@ -171,8 +175,6 @@ App::get('/v1/projects')
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
$queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, [$search])] : [];
if (!empty($after)) {
$afterProject = $dbForConsole->getDocument('projects', $after);
@ -181,6 +183,12 @@ App::get('/v1/projects')
}
}
$queries = [];
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
}
$results = $dbForConsole->find('projects', $queries, $limit, $offset, [], [$orderType], $afterProject ?? null);
$sum = $dbForConsole->count('projects', $queries, APP_LIMIT_COUNT);
@ -356,6 +364,7 @@ App::patch('/v1/projects/:projectId')
->setAttribute('legalCity', $legalCity)
->setAttribute('legalAddress', $legalAddress)
->setAttribute('legalTaxId', $legalTaxId)
->setAttribute('search', implode(' ', [$projectId, $name]))
);
$response->dynamic($project, Response::MODEL_PROJECT);
@ -379,6 +388,7 @@ App::patch('/v1/projects/:projectId/service')
->action(function ($projectId, $service, $status, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Boolean $status */
$project = $dbForConsole->getDocument('projects', $projectId);
@ -457,7 +467,7 @@ App::patch('/v1/projects/:projectId/auth/limit')
$auths['limit'] = $limit;
$dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths)
->setAttribute('auths', $auths)
);
$response->dynamic($project, Response::MODEL_PROJECT);
@ -581,6 +591,9 @@ App::post('/v1/projects/:projectId/webhooks')
$webhook = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'projectId' => $project->getId(),
'name' => $name,
'events' => $events,
'url' => $url,
@ -589,9 +602,9 @@ App::post('/v1/projects/:projectId/webhooks')
'httpPass' => $httpPass,
]);
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('webhooks', $webhook, Document::SET_TYPE_APPEND)
);
$webhook = $dbForConsole->createDocument('webhooks', $webhook);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
@ -620,7 +633,9 @@ App::get('/v1/projects/:projectId/webhooks')
throw new Exception('Project not found', 404);
}
$webhooks = $project->getAttribute('webhooks', []);
$webhooks = $dbForConsole->find('webhooks', [
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
$response->dynamic(new Document([
'webhooks' => $webhooks,
@ -652,9 +667,12 @@ App::get('/v1/projects/:projectId/webhooks/:webhookId')
throw new Exception('Project not found', 404);
}
$webhook = $project->find('$id', $webhookId, 'webhooks');
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if (empty($webhook) || !$webhook instanceof Document) {
if ($webhook === false || $webhook->isEmpty()) {
throw new Exception('Webhook not found', 404);
}
@ -693,22 +711,27 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
$security = ($security === '1' || $security === 'true' || $security === 1 || $security === true);
$webhook = $project->find('$id', $webhookId, 'webhooks');
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if (empty($webhook) || !$webhook instanceof Document) {
if ($webhook === false || $webhook->isEmpty()) {
throw new Exception('Webhook not found', 404);
}
$project->findAndReplace('$id', $webhook->getId(), $webhook
->setAttribute('name', $name)
->setAttribute('events', $events)
->setAttribute('url', $url)
->setAttribute('security', $security)
->setAttribute('httpUser', $httpUser)
->setAttribute('httpPass', $httpPass)
, 'webhooks');
$webhook
->setAttribute('name', $name)
->setAttribute('events', $events)
->setAttribute('url', $url)
->setAttribute('security', $security)
->setAttribute('httpUser', $httpUser)
->setAttribute('httpPass', $httpPass)
;
$dbForConsole->updateDocument('projects', $project->getId(), $project);
$dbForConsole->updateDocument('webhooks', $webhook->getId(), $webhook);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
});
@ -736,11 +759,18 @@ App::delete('/v1/projects/:projectId/webhooks/:webhookId')
throw new Exception('Project not found', 404);
}
if (!$project->findAndRemove('$id', $webhookId, 'webhooks')) {
$webhook = $dbForConsole->findOne('webhooks', [
new Query('_uid', Query::TYPE_EQUAL, [$webhookId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if($webhook === false || $webhook->isEmpty()) {
throw new Exception('Webhook not found', 404);
}
$dbForConsole->updateDocument('projects', $project->getId(), $project);
$dbForConsole->deleteDocument('webhooks', $webhook->getId());
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->noContent();
});
@ -774,14 +804,17 @@ App::post('/v1/projects/:projectId/keys')
$key = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'projectId' => $project->getId(),
'name' => $name,
'scopes' => $scopes,
'secret' => \bin2hex(\random_bytes(128)),
]);
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('keys', $key, Document::SET_TYPE_APPEND)
);
$key = $dbForConsole->createDocument('keys', $key);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($key, Response::MODEL_KEY);
@ -810,7 +843,9 @@ App::get('/v1/projects/:projectId/keys')
throw new Exception('Project not found', 404);
}
$keys = $project->getAttribute('keys', []);
$keys = $dbForConsole->find('keys', [
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()]),
], 5000);
$response->dynamic(new Document([
'keys' => $keys,
@ -833,15 +868,21 @@ App::get('/v1/projects/:projectId/keys/:keyId')
->inject('response')
->inject('dbForConsole')
->action(function ($projectId, $keyId, $response, $dbForConsole) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForConsole */
$project = $dbForConsole->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception('Project not found', 404);
}
$key = $project->find('$id', $keyId, 'keys');
$key = $dbForConsole->findOne('keys', [
new Query('_uid', Query::TYPE_EQUAL, [$keyId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if (empty($key) || !$key instanceof Document) {
if ($key === false || $key->isEmpty()) {
throw new Exception('Key not found', 404);
}
@ -874,18 +915,23 @@ App::put('/v1/projects/:projectId/keys/:keyId')
throw new Exception('Project not found', 404);
}
$key = $project->find('$id', $keyId, 'keys');
$key = $dbForConsole->findOne('keys', [
new Query('_uid', Query::TYPE_EQUAL, [$keyId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if (empty($key) || !$key instanceof Document) {
if ($key === false || $key->isEmpty()) {
throw new Exception('Key not found', 404);
}
$project->findAndReplace('$id', $key->getId(), $key
->setAttribute('name', $name)
->setAttribute('scopes', $scopes)
, 'keys');
$key
->setAttribute('name', $name)
->setAttribute('scopes', $scopes)
;
$dbForConsole->updateDocument('projects', $project->getId(), $project);
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->dynamic($key, Response::MODEL_KEY);
});
@ -913,11 +959,18 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
throw new Exception('Project not found', 404);
}
if (!$project->findAndRemove('$id', $keyId, 'keys')) {
$key = $dbForConsole->findOne('keys', [
new Query('_uid', Query::TYPE_EQUAL, [$keyId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if($key === false || $key->isEmpty()) {
throw new Exception('Key not found', 404);
}
$dbForConsole->updateDocument('projects', $project->getId(), $project);
$dbForConsole->deleteDocument('keys', $key->getId());
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->noContent();
});
@ -954,6 +1007,9 @@ App::post('/v1/projects/:projectId/platforms')
$platform = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'projectId' => $project->getId(),
'type' => $type,
'name' => $name,
'key' => $key,
@ -963,9 +1019,9 @@ App::post('/v1/projects/:projectId/platforms')
'dateUpdated' => \time(),
]);
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('platforms', $platform, Document::SET_TYPE_APPEND)
);
$platform = $dbForConsole->createDocument('platforms', $platform);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($platform, Response::MODEL_PLATFORM);
@ -994,7 +1050,9 @@ App::get('/v1/projects/:projectId/platforms')
throw new Exception('Project not found', 404);
}
$platforms = $project->getAttribute('platforms', []);
$platforms = $dbForConsole->find('platforms', [
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
], 5000);
$response->dynamic(new Document([
'platforms' => $platforms,
@ -1026,9 +1084,12 @@ App::get('/v1/projects/:projectId/platforms/:platformId')
throw new Exception('Project not found', 404);
}
$platform = $project->find('$id', $platformId, 'platforms');
$platform = $dbForConsole->findOne('platforms', [
new Query('_uid', Query::TYPE_EQUAL, [$platformId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if (empty($platform) || !$platform instanceof Document) {
if ($platform === false || $platform->isEmpty()) {
throw new Exception('Platform not found', 404);
}
@ -1063,9 +1124,12 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
throw new Exception('Project not found', 404);
}
$platform = $project->find('$id', $platformId, 'platforms');
$platform = $dbForConsole->findOne('platforms', [
new Query('_uid', Query::TYPE_EQUAL, [$platformId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if (empty($platform) || !$platform instanceof Document) {
if ($platform === false || $platform->isEmpty()) {
throw new Exception('Platform not found', 404);
}
@ -1077,15 +1141,9 @@ App::put('/v1/projects/:projectId/platforms/:platformId')
->setAttribute('hostname', $hostname)
;
$project->findAndReplace('$id', $platform->getId(), $platform
->setAttribute('name', $name)
->setAttribute('dateUpdated', \time())
->setAttribute('key', $key)
->setAttribute('store', $store)
->setAttribute('hostname', $hostname)
, 'platforms');
$dbForConsole->updateDocument('platforms', $platform->getId(), $platform);
$dbForConsole->updateDocument('projects', $project->getId(), $project);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM);
});
@ -1113,11 +1171,18 @@ App::delete('/v1/projects/:projectId/platforms/:platformId')
throw new Exception('Project not found', 404);
}
if (!$project->findAndRemove('$id', $platformId, 'platforms')) {
$platform = $dbForConsole->findOne('platforms', [
new Query('_uid', Query::TYPE_EQUAL, [$platformId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if ($platform === false || $platform->isEmpty()) {
throw new Exception('Platform not found', 404);
}
$dbForConsole->updateDocument('projects', $project->getId(), $project);
$dbForConsole->deleteDocument('platforms', $platformId);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->noContent();
});
@ -1148,9 +1213,12 @@ App::post('/v1/projects/:projectId/domains')
throw new Exception('Project not found', 404);
}
$document = $project->find('domain', $domain, 'domains');
$document = $dbForConsole->findOne('domains', [
new Query('domain', Query::TYPE_EQUAL, [$domain]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()]),
]);
if ($document) {
if ($document && !$document->isEmpty()) {
throw new Exception('Domain already exists', 409);
}
@ -1164,6 +1232,9 @@ App::post('/v1/projects/:projectId/domains')
$domain = new Document([
'$id' => $dbForConsole->getId(),
'$read' => ['role:all'],
'$write' => ['role:all'],
'projectId' => $project->getId(),
'updated' => \time(),
'domain' => $domain->get(),
'tld' => $domain->getSuffix(),
@ -1172,9 +1243,9 @@ App::post('/v1/projects/:projectId/domains')
'certificateId' => null,
]);
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project
->setAttribute('domains', $domain, Document::SET_TYPE_APPEND)
);
$domain = $dbForConsole->createDocument('domains', $domain);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($domain, Response::MODEL_DOMAIN);
@ -1203,7 +1274,9 @@ App::get('/v1/projects/:projectId/domains')
throw new Exception('Project not found', 404);
}
$domains = $project->getAttribute('domains', []);
$domains = $dbForConsole->find('domains', [
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
], 5000);
$response->dynamic(new Document([
'domains' => $domains,
@ -1235,9 +1308,12 @@ App::get('/v1/projects/:projectId/domains/:domainId')
throw new Exception('Project not found', 404);
}
$domain = $project->find('$id', $domainId, 'domains');
$domain = $dbForConsole->findOne('domains', [
new Query('_uid', Query::TYPE_EQUAL, [$domainId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if (empty($domain) || !$domain instanceof Document) {
if ($domain === false || $domain->isEmpty()) {
throw new Exception('Domain not found', 404);
}
@ -1268,9 +1344,12 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
throw new Exception('Project not found', 404);
}
$domain = $project->find('$id', $domainId, 'domains');
$domain = $dbForConsole->findOne('domains', [
new Query('_uid', Query::TYPE_EQUAL, [$domainId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if (empty($domain) || !$domain instanceof Document) {
if ($domain === false || $domain->isEmpty()) {
throw new Exception('Domain not found', 404);
}
@ -1290,11 +1369,9 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification')
throw new Exception('Failed to verify domain', 401);
}
$project->findAndReplace('$id', $domain->getId(), $domain
->setAttribute('verification', true)
, 'domains');
$dbForConsole->updateDocument('projects', $project->getId(), $project);
$dbForConsole->updateDocument('domains', $domain->getId(), $domain->setAttribute('verification', true));
$dbForConsole->deleteCachedDocument('projects', $project->getId());
// Issue a TLS certificate when domain is verified
Resque::enqueue('v1-certificates', 'CertificatesV1', [
@ -1329,13 +1406,18 @@ App::delete('/v1/projects/:projectId/domains/:domainId')
throw new Exception('Project not found', 404);
}
$domain = $project->find('$id', $domainId, 'domains');
$domain = $dbForConsole->findOne('domains', [
new Query('_uid', Query::TYPE_EQUAL, [$domainId]),
new Query('projectId', Query::TYPE_EQUAL, [$project->getId()])
]);
if (!$project->findAndRemove('$id', $domainId, 'domains')) {
if ($domain === false || $domain->isEmpty()) {
throw new Exception('Domain not found', 404);
}
$dbForConsole->updateDocument('projects', $project->getId(), $project);
$dbForConsole->deleteDocument('domains', $domain->getId());
$dbForConsole->deleteCachedDocument('projects', $project->getId());
$deletes
->setParam('type', DELETE_TYPE_CERTIFICATES)

View file

@ -125,13 +125,14 @@ App::post('/v1/storage/files')
$sizeActual = $device->getFileSize($path);
$fileId = ($fileId == 'unique()') ? $dbForInternal->getId() : $fileId;
$file = $dbForInternal->createDocument('files', new Document([
'$id' => $fileId == 'unique()' ? $dbForInternal->getId() : $fileId,
'$id' => $fileId,
'$read' => (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? [], // By default set read permissions for user
'$write' => (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? [], // By default set write permissions for user
'dateCreated' => \time(),
'bucketId' => '',
'name' => $file['name'],
'name' => $file['name'] ?? '',
'path' => $path,
'signature' => $device->getFileHash($path),
'mimeType' => $mimeType,
@ -143,6 +144,7 @@ App::post('/v1/storage/files')
'openSSLCipher' => OpenSSL::CIPHER_AES_128_GCM,
'openSSLTag' => \bin2hex($tag),
'openSSLIV' => \bin2hex($iv),
'search' => implode(' ', [$fileId, $file['name'] ?? '',]),
]));
$audits
@ -185,8 +187,6 @@ App::get('/v1/storage/files')
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Stats\Stats $usage */
$queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, $search)] : [];
if (!empty($after)) {
$afterFile = $dbForInternal->getDocument('files', $after);
@ -195,6 +195,12 @@ App::get('/v1/storage/files')
}
}
$queries = [];
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
}
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', 'default')

View file

@ -59,6 +59,7 @@ App::post('/v1/teams')
'name' => $name,
'sum' => ($isPrivilegedUser || $isAppUser) ? 0 : 1,
'dateCreated' => \time(),
'search' => implode(' ', [$teamId, $name]),
]));
Authorization::reset();
@ -113,8 +114,6 @@ App::get('/v1/teams')
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, [$search])] : [];
if (!empty($after)) {
$afterTeam = $dbForInternal->getDocument('teams', $after);
@ -123,6 +122,12 @@ App::get('/v1/teams')
}
}
$queries = [];
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
}
$results = $dbForInternal->find('teams', $queries, $limit, $offset, [], [$orderType], $afterTeam ?? null);
$sum = $dbForInternal->count('teams', $queries, APP_LIMIT_COUNT);
@ -185,7 +190,10 @@ App::put('/v1/teams/:teamId')
throw new Exception('Team not found', 404);
}
$team = $dbForInternal->updateDocument('teams', $team->getId(), $team->setAttribute('name', $name));
$team = $dbForInternal->updateDocument('teams', $team->getId(),$team
->setAttribute('name', $name)
->setAttribute('search', implode(' ', [$teamId, $name]))
);
$response->dynamic($team, Response::MODEL_TEAM);
});
@ -333,6 +341,7 @@ App::post('/v1/teams/:teamId/memberships')
'sessions' => [],
'tokens' => [],
'memberships' => [],
'search' => implode(' ', [$userId, $email, $name]),
]));
} catch (Duplicate $th) {
throw new Exception('Account already exists', 409);

View file

@ -66,6 +66,8 @@ App::post('/v1/users')
'sessions' => [],
'tokens' => [],
'memberships' => [],
'search' => implode(' ', [$userId, $email, $name]),
'deleted' => false
]));
} catch (Duplicate $th) {
throw new Exception('Account already exists', 409);
@ -107,20 +109,25 @@ App::get('/v1/users')
$afterUser = $dbForInternal->getDocument('users', $after);
if ($afterUser->isEmpty()) {
throw new Exception('User for after not found', 400);
throw new Exception("User '{$after}' for the 'after' value not found.", 400);
}
}
$results = $dbForInternal->find('users', [], $limit, $offset, [], [$orderType], $afterUser ?? null);
$sum = $dbForInternal->count('users', [], APP_LIMIT_COUNT);
$queries = [
new Query('deleted', Query::TYPE_EQUAL, [false])
];
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
}
$usage
->setParam('users.read', 1)
;
$response->dynamic(new Document([
'users' => $results,
'sum' => $sum,
'users' => $dbForInternal->find('users', $queries, $limit, $offset, [], [$orderType], $afterUser ?? null),
'sum' => $dbForInternal->count('users', $queries, APP_LIMIT_COUNT),
]), Response::MODEL_USER_LIST);
});
@ -146,7 +153,7 @@ App::get('/v1/users/:userId')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -178,7 +185,7 @@ App::get('/v1/users/:userId/prefs')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -214,7 +221,7 @@ App::get('/v1/users/:userId/sessions')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -266,7 +273,7 @@ App::get('/v1/users/:userId/logs')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -374,7 +381,7 @@ App::patch('/v1/users/:userId/status')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -410,7 +417,7 @@ App::patch('/v1/users/:userId/verification')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -446,7 +453,7 @@ App::patch('/v1/users/:userId/name')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -485,7 +492,7 @@ App::patch('/v1/users/:userId/password')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -525,7 +532,7 @@ App::patch('/v1/users/:userId/email')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -726,7 +733,7 @@ App::patch('/v1/users/:userId/prefs')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -763,7 +770,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -818,7 +825,7 @@ App::delete('/v1/users/:userId/sessions')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
@ -867,26 +874,35 @@ App::delete('/v1/users/:userId')
$user = $dbForInternal->getDocument('users', $userId);
if ($user->isEmpty()) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
if (!$dbForInternal->deleteDocument('users', $userId)) {
throw new Exception('Failed to remove user from DB', 500);
}
// clone user object to send to workers
$clone = clone $user;
$user
->setAttribute("name", null)
->setAttribute("email", null)
->setAttribute("password", null)
->setAttribute("deleted", true)
;
$dbForInternal->updateDocument('users', $userId, $user);
$deletes
->setParam('type', DELETE_TYPE_DOCUMENT)
->setParam('document', $user)
->setParam('document', $clone)
;
$events
->setParam('eventData', $response->output($user, Response::MODEL_USER))
->setParam('eventData', $response->output($clone, Response::MODEL_USER))
;
$usage
->setParam('users.delete', 1)
;
$response->noContent();
});

View file

@ -405,11 +405,10 @@ App::get('/specs/:format')
}
$routes[] = $route;
$model = $response->getModel($route->getLabel('sdk.response.model', 'none'));
if($model) {
$models[$model->getType()] = $model;
}
$modelLabel = $route->getLabel('sdk.response.model', 'none');
$model = \is_array($modelLabel) ? \array_map(function($m) use($response) {
return $response->getModel($m);
}, $modelLabel) : $response->getModel($modelLabel);
}
}

View file

@ -32,6 +32,7 @@ use Appwrite\Event\Realtime;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\View;
use Utopia\Config\Config;
use Utopia\Locale\Locale;
@ -63,7 +64,12 @@ const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10000;
const APP_CACHE_BUSTER = 160;
const APP_VERSION_STABLE = '0.10.4';
const APP_VERSION_STABLE = '0.11.0';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
const APP_DATABASE_ATTRIBUTE_URL = 'url';
const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange';
const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange';
const APP_STORAGE_UPLOADS = '/storage/uploads';
const APP_STORAGE_FUNCTIONS = '/storage/functions';
const APP_STORAGE_CACHE = '/storage/cache';
@ -145,7 +151,7 @@ if(!empty($user) || !empty($pass)) {
}
/**
* DB Filters
* Old DB Filters
*/
DatabaseOld::addFilter('json',
function($value) {
@ -181,6 +187,43 @@ DatabaseOld::addFilter('encrypt',
}
);
/**
* New DB Filters
*/
Database::addFilter('casting',
function($value) {
return json_encode(['value' => $value]);
},
function($value) {
if (is_null($value)) {
return null;
}
return json_decode($value, true)['value'];
}
);
Database::addFilter('range',
function($value, Document $attribute) {
if ($attribute->isSet('min')) {
$attribute->removeAttribute('min');
}
if ($attribute->isSet('max')) {
$attribute->removeAttribute('max');
}
return $value;
},
function($value, Document $attribute) {
$formatOptions = json_decode($attribute->getAttribute('formatOptions', []), true);
if (isset($formatOptions['min']) || isset($formatOptions['max'])) {
$attribute
->setAttribute('min', $formatOptions['min'])
->setAttribute('max', $formatOptions['max'])
;
}
return $value;
}
);
Database::addFilter('subQueryAttributes',
function($value) {
return null;
@ -189,7 +232,7 @@ Database::addFilter('subQueryAttributes',
return $database
->find('attributes', [
new Query('collectionId', Query::TYPE_EQUAL, [$document->getId()])
], 100, 0, []);
], $database->getAttributeLimit(), 0, []);
}
);
@ -201,7 +244,55 @@ Database::addFilter('subQueryIndexes',
return $database
->find('indexes', [
new Query('collectionId', Query::TYPE_EQUAL, [$document->getId()])
], 100, 0, []);
], 64, 0, []);
}
);
Database::addFilter('subQueryPlatforms',
function($value) {
return null;
},
function($value, Document $document, Database $database) {
return $database
->find('platforms', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getIndexLimit(), 0, []);
}
);
Database::addFilter('subQueryDomains',
function($value) {
return null;
},
function($value, Document $document, Database $database) {
return $database
->find('domains', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getIndexLimit(), 0, []);
}
);
Database::addFilter('subQueryKeys',
function($value) {
return null;
},
function($value, Document $document, Database $database) {
return $database
->find('keys', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getIndexLimit(), 0, []);
}
);
Database::addFilter('subQueryWebhooks',
function($value) {
return null;
},
function($value, Document $document, Database $database) {
return $database
->find('webhooks', [
new Query('projectId', Query::TYPE_EQUAL, [$document->getId()])
], $database->getIndexLimit(), 0, []);
}
);
@ -229,25 +320,25 @@ Database::addFilter('encrypt',
/**
* DB Formats
*/
Structure::addFormat('email', function() {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function() {
return new Email();
}, Database::VAR_STRING);
Structure::addFormat('ip', function() {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_IP, function() {
return new IP();
}, Database::VAR_STRING);
Structure::addFormat('url', function() {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_URL, function() {
return new URL();
}, Database::VAR_STRING);
Structure::addFormat('int-range', function($attribute) {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_INTEGER);
}, Database::VAR_INTEGER);
Structure::addFormat('float-range', function($attribute) {
Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_FLOAT);

View file

@ -342,7 +342,7 @@ $cli
do { // list projects
try {
$attempts++;
$projects = $dbForConsole->find('projects', [], 100, orderAfter:$latestProject);
$projects = $dbForConsole->find('projects', [], 100, cursor:$latestProject);
break; // leave the do-while if successful
} catch (\Exception $e) {
Console::warning("Console DB not ready yet. Retrying ({$attempts})...");
@ -472,7 +472,7 @@ $cli
do { // Loop over all the parent collection document for each sub collection
$dbForProject->setNamespace("project_{$projectId}_{$options['namespace']}");
$parents = $dbForProject->find($collection, [], 100, orderAfter:$latestParent); // Get all the parents for the sub collections for example for documents, this will get all the collections
$parents = $dbForProject->find($collection, [], 100, cursor:$latestParent); // Get all the parents for the sub collections for example for documents, this will get all the collections
if (empty($parents)) {
continue;

View file

@ -92,7 +92,7 @@ class DatabaseV1 extends Worker
$dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
}
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
}
/**
@ -118,7 +118,58 @@ class DatabaseV1 extends Worker
$dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
}
$dbForInternal->purgeDocument('collections', $collectionId);
// The underlying database removes/rebuilds indexes when attribute is removed
// Update indexes table with changes
/** @var Document[] $indexes */
$indexes = $collection->getAttribute('indexes', []);
foreach ($indexes as $index) {
/** @var string[] $attributes */
$attributes = $index->getAttribute('attributes');
$lengths = $index->getAttribute('lengths');
$orders = $index->getAttribute('orders');
$found = \array_search($key, $attributes);
if ($found !== false) {
// If found, remove entry from attributes, lengths, and orders
// array_values wraps array_diff to reindex array keys
// when found attribute is removed from array
$attributes = \array_values(\array_diff($attributes, [$attributes[$found]]));
$lengths = \array_values(\array_diff($lengths, [$lengths[$found]]));
$orders = \array_values(\array_diff($orders, [$orders[$found]]));
if (empty($attributes)) {
$dbForInternal->deleteDocument('indexes', $index->getId());
} else {
$index
->setAttribute('attributes', $attributes, Document::SET_TYPE_ASSIGN)
->setAttribute('lengths', $lengths, Document::SET_TYPE_ASSIGN)
->setAttribute('orders', $orders, Document::SET_TYPE_ASSIGN)
;
// Check if an index exists with the same attributes and orders
$exists = false;
foreach ($indexes as $existing) {
if ($existing->getAttribute('key') !== $index->getAttribute('key') // Ignore itself
&& $existing->getAttribute('attributes') === $index->getAttribute('attributes')
&& $existing->getAttribute('orders') === $index->getAttribute('orders')
) {
$exists = true;
break;
}
}
if ($exists) { // Delete the duplicate if created, else update in db
$this->deleteIndex($collection, $index, $projectId);
} else {
$dbForInternal->updateDocument('indexes', $index->getId(), $index);
}
}
}
}
$dbForInternal->deleteCachedDocument('collections', $collectionId);
}
/**
@ -148,7 +199,7 @@ class DatabaseV1 extends Worker
$dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
}
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
}
/**
@ -166,7 +217,7 @@ class DatabaseV1 extends Worker
try {
if(!$dbForExternal->deleteIndex($collectionId, $key)) {
throw new Exception('Failed to delete Attribute');
throw new Exception('Failed to delete index');
}
$dbForInternal->deleteDocument('indexes', $index->getId());
@ -175,6 +226,6 @@ class DatabaseV1 extends Worker
$dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
}
$dbForInternal->purgeDocument('collections', $collectionId);
$dbForInternal->deleteCachedDocument('collections', $collectionId);
}
}

View file

@ -45,7 +45,7 @@
"utopia-php/cache": "0.4.*",
"utopia-php/cli": "0.11.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "dev-feat-adjusted-query-validator as 0.10.0",
"utopia-php/database": "0.10.0",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/preloader": "0.2.*",
@ -65,12 +65,7 @@
"adhocore/jwt": "1.1.2",
"slickdeals/statsd": "3.1.0"
},
"repositories": [
{
"type": "git",
"url": "https://github.com/utopia-php/database"
}
],
"repositories": [],
"require-dev": {
"appwrite/sdk-generator": "0.15.2",
"phpunit/phpunit": "9.5.6",
@ -86,4 +81,4 @@
"php": "8.0"
}
}
}
}

100
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": "da6d0977e89680534a66c2c3e4848b99",
"content-hash": "dfb8fa19daa736b3687617c98f309983",
"packages": [
{
"name": "adhocore/jwt",
@ -1984,11 +1984,17 @@
},
{
"name": "utopia-php/database",
"version": "dev-feat-adjusted-query-validator",
"version": "0.10.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database",
"reference": "cb73391371f70ddb54bc0000064b15c5f173cb7a"
"url": "https://github.com/utopia-php/database.git",
"reference": "b7c60b0ec769a9050dd2b939b78ff1f5d4fa27e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/b7c60b0ec769a9050dd2b939b78ff1f5d4fa27e8",
"reference": "b7c60b0ec769a9050dd2b939b78ff1f5d4fa27e8",
"shasum": ""
},
"require": {
"ext-mongodb": "*",
@ -2011,11 +2017,7 @@
"Utopia\\Database\\": "src/Database"
}
},
"autoload-dev": {
"psr-4": {
"Utopia\\Tests\\": "tests/Database"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@ -2037,7 +2039,11 @@
"upf",
"utopia"
],
"time": "2021-08-23T14:18:47+00:00"
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.10.0"
},
"time": "2021-10-04T17:23:25+00:00"
},
{
"name": "utopia-php/domains",
@ -3825,16 +3831,16 @@
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.5.0",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f"
"reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/30f38bffc6f24293dadd1823936372dfa9e86e2f",
"reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/a12f7e301eb7258bb68acd89d4aefa05c2906cae",
"reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae",
"shasum": ""
},
"require": {
@ -3869,9 +3875,9 @@
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.0"
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.1"
},
"time": "2021-09-17T15:28:14+00:00"
"time": "2021-10-02T14:08:47+00:00"
},
{
"name": "phpspec/prophecy",
@ -6077,55 +6083,6 @@
],
"time": "2021-08-26T08:00:08+00:00"
},
{
"name": "textalk/websocket",
"version": "1.5.2",
"source": {
"type": "git",
"url": "https://github.com/Textalk/websocket-php.git",
"reference": "b93249453806a2dd46495de46d76fcbcb0d8dee8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Textalk/websocket-php/zipball/b93249453806a2dd46495de46d76fcbcb0d8dee8",
"reference": "b93249453806a2dd46495de46d76fcbcb0d8dee8",
"shasum": ""
},
"require": {
"php": "^7.2 | ^8.0",
"psr/log": "^1.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.0",
"phpunit/phpunit": "^8.0|^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"autoload": {
"psr-4": {
"WebSocket\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"ISC"
],
"authors": [
{
"name": "Fredrik Liljegren"
},
{
"name": "Sören Jensen",
"email": "soren@abicart.se"
}
],
"description": "WebSocket client and server",
"support": {
"issues": "https://github.com/Textalk/websocket-php/issues",
"source": "https://github.com/Textalk/websocket-php/tree/1.5.2"
},
"time": "2021-02-12T15:39:23+00:00"
},
{
"name": "theseer/tokenizer",
"version": "1.2.1",
@ -6411,18 +6368,9 @@
"time": "2015-12-17T08:42:14+00:00"
}
],
"aliases": [
{
"package": "utopia-php/database",
"version": "dev-feat-adjusted-query-validator",
"alias": "0.10.0",
"alias_normalized": "0.10.0.0"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"utopia-php/database": 20
},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {

View file

@ -71,7 +71,7 @@ services:
- ./psalm.xml:/usr/src/code/psalm.xml
- ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app
# - ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
# - ./vendor:/usr/src/code/vendor
- ./docs:/usr/src/code/docs
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src

View file

@ -580,7 +580,7 @@ class Database
{
if (!isset(self::$filters[$name])) {
return $value;
throw new Exception('Filter not found');
throw new Exception("Filter '{$name}' not found");
}
try {
@ -602,7 +602,7 @@ class Database
{
if (!isset(self::$filters[$name])) {
return $value;
throw new Exception('Filter not found');
throw new Exception("Filter '{$name}' not found");
}
try {

View file

@ -26,7 +26,7 @@ class Detector
/**
* Get OS info
*
*
* @return array
*/
public function getOS(): array
@ -42,7 +42,7 @@ class Detector
/**
* Get client info
*
*
* @return array
*/
public function getClient(): array
@ -61,7 +61,7 @@ class Detector
/**
* Get device info
*
*
* @return array
*/
public function getDevice(): array
@ -78,7 +78,7 @@ class Detector
*/
protected function getDetector(): DeviceDetector
{
if(!$this->detctor) {
if (!$this->detctor) {
$this->detctor = new DeviceDetector($this->userAgent);
$this->detctor->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$this->detctor->parse();

View file

@ -4,7 +4,6 @@ namespace Appwrite\Specification\Format;
use Appwrite\Specification\Format;
use Appwrite\Template\Template;
use stdClass;
use Utopia\Validator;
class OpenAPI3 extends Format
@ -25,21 +24,27 @@ class OpenAPI3 extends Format
* Get Used Models
*
* Recursively get all used models
*
*
* @param object $model
* @param array $models
*
* @return void
*/
protected function getUsedModels($model, array &$usedModels)
{
if (is_string($model) && !in_array($model, ['string', 'integer', 'boolean', 'json', 'float'])) {
{
if (is_string($model) && !in_array($model, ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $model;
return;
}
if (!is_object($model)) return;
foreach ($model->getRules() as $rule) {
$this->getUsedModels($rule['type'], $usedModels);
if(\is_array($rule['type'])) {
foreach ($rule['type'] as $type) {
$this->getUsedModels($type, $usedModels);
}
} else {
$this->getUsedModels($rule['type'], $usedModels);
}
}
}
@ -93,7 +98,7 @@ class OpenAPI3 extends Format
if (isset($output['components']['securitySchemes']['Project'])) {
$output['components']['securitySchemes']['Project']['x-appwrite'] = ['demo' => '5df5acd0d48c2'];
}
if (isset($output['components']['securitySchemes']['Key'])) {
$output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2'];
}
@ -101,7 +106,7 @@ class OpenAPI3 extends Format
if (isset($output['securityDefinitions']['JWT'])) {
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...'];
}
if (isset($output['components']['securitySchemes']['Locale'])) {
$output['components']['securitySchemes']['Locale']['x-appwrite'] = ['demo' => 'en'];
}
@ -125,7 +130,7 @@ class OpenAPI3 extends Format
$id = $route->getLabel('sdk.method', \uniqid());
$desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__.'/../../../../'.$route->getLabel('sdk.description', '')) : null;
$produces = $route->getLabel('sdk.response.type', null);
$model = $route->getLabel('sdk.response.model', 'none');
$model = $route->getLabel('sdk.response.model', 'none');
$routeSecurity = $route->getLabel('sdk.auth', []);
$sdkPlatofrms = [];
@ -149,7 +154,7 @@ class OpenAPI3 extends Format
if(empty($routeSecurity)) {
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
}
$temp = [
'summary' => $route->getDesc(),
'operationId' => $route->getLabel('sdk.namespace', 'default').ucfirst($id),
@ -175,13 +180,24 @@ class OpenAPI3 extends Format
];
foreach ($this->models as $key => $value) {
if($value->getType() === $model) {
$model = $value;
break;
if(\is_array($model)) {
$model = \array_map(function($m) use($value) {
if($m === $value->getType()) {
return $value;
}
return $m;
}, $model);
} else {
if($value->getType() === $model) {
$model = $value;
break;
}
}
}
if($model->isNone()) {
if(!(\is_array($model)) && $model->isNone()) {
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => (in_array($produces, [
'image/*',
@ -198,17 +214,43 @@ class OpenAPI3 extends Format
// ],
];
} else {
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'content' => [
$produces => [
'schema' => [
'$ref' => '#/components/schemas/'.$model->getType(),
if(\is_array($model)) {
$modelDescription = \join(', or ', \array_map(function ($m) {
return $m->getName();
}, $model));
// model has multiple possible responses, we will use oneOf
foreach ($model as $m) {
$usedModels[] = $m->getType();
}
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $modelDescription,
'content' => [
$produces => [
'schema' => [
'oneOf' => \array_map(function($m) {
return ['$ref' => '#/components/schemas/'.$m->getType()];
}, $model)
],
],
],
],
];
];
} else {
// Response definition using one type
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'content' => [
$produces => [
'schema' => [
'$ref' => '#/components/schemas/'.$model->getType(),
],
],
],
];
}
}
if($route->getLabel('sdk.response.code', 500) === 204) {
@ -218,7 +260,7 @@ class OpenAPI3 extends Format
if ((!empty($scope))) { // && 'public' != $scope
$securities = ['Project' => []];
foreach($route->getLabel('sdk.auth', []) as $security) {
if(array_key_exists($security, $this->keys)) {
$securities[$security] = [];
@ -277,7 +319,7 @@ class OpenAPI3 extends Format
case 'Utopia\Validator\JSON':
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
$param['default'] = (empty($param['default'])) ? new stdClass() : $param['default'];
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['schema']['type'] = 'object';
$node['schema']['x-example'] = '{}';
//$node['schema']['format'] = 'json';
@ -396,7 +438,7 @@ class OpenAPI3 extends Format
if($model->isAny()) {
$output['components']['schemas'][$model->getType()]['additionalProperties'] = true;
}
if(!empty($required)) {
$output['components']['schemas'][$model->getType()]['required'] = $required;
}
@ -411,7 +453,7 @@ class OpenAPI3 extends Format
case 'json':
$type = 'string';
break;
case 'integer':
$type = 'integer';
$format = 'int32';
@ -421,18 +463,39 @@ class OpenAPI3 extends Format
$type = 'number';
$format = 'float';
break;
case 'double':
$type = 'number';
$format = 'double';
break;
case 'boolean':
$type = 'boolean';
break;
default:
$type = 'object';
$rule['type'] = ($rule['type']) ? $rule['type'] : 'none';
$items = [
'$ref' => '#/components/schemas/'.$rule['type'],
];
if(\is_array($rule['type'])) {
if($rule['array']) {
$items = [
'anyOf' => \array_map(function($type) {
return ['$ref' => '#/components/schemas/'.$type];
}, $rule['type'])
];
} else {
$items = [
'oneOf' => \array_map(function($type) {
return ['$ref' => '#/components/schemas/'.$type];
}, $rule['type'])
];
}
} else {
$items = [
'$ref' => '#/components/schemas/'.$rule['type'],
];
}
break;
}

View file

@ -4,7 +4,6 @@ namespace Appwrite\Specification\Format;
use Appwrite\Specification\Format;
use Appwrite\Template\Template;
use stdClass;
use Utopia\Validator;
class Swagger2 extends Format
@ -33,13 +32,19 @@ class Swagger2 extends Format
*/
protected function getUsedModels($model, array &$usedModels)
{
if (is_string($model) && !in_array($model, ['string', 'integer', 'boolean', 'json', 'float'])) {
if (is_string($model) && !in_array($model, ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $model;
return;
}
if (!is_object($model)) return;
foreach ($model->getRules() as $rule) {
$this->getUsedModels($rule['type'], $usedModels);
if(\is_array($rule['type'])) {
foreach ($rule['type'] as $type) {
$this->getUsedModels($type, $usedModels);
}
} else {
$this->getUsedModels($rule['type'], $usedModels);
}
}
}
@ -91,15 +96,15 @@ class Swagger2 extends Format
if (isset($output['securityDefinitions']['Project'])) {
$output['securityDefinitions']['Project']['x-appwrite'] = ['demo' => '5df5acd0d48c2'];
}
if (isset($output['securityDefinitions']['Key'])) {
$output['securityDefinitions']['Key']['x-appwrite'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2'];
}
if (isset($output['securityDefinitions']['JWT'])) {
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...'];
}
if (isset($output['securityDefinitions']['Locale'])) {
$output['securityDefinitions']['Locale']['x-appwrite'] = ['demo' => 'en'];
}
@ -147,7 +152,7 @@ class Swagger2 extends Format
if(empty($routeSecurity)) {
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
}
$temp = [
'summary' => $route->getDesc(),
'operationId' => $route->getLabel('sdk.namespace', 'default').ucfirst($id),
@ -177,13 +182,22 @@ class Swagger2 extends Format
}
foreach ($this->models as $key => $value) {
if($value->getType() === $model) {
$model = $value;
break;
if(\is_array($model)) {
$model = \array_map(function($m) use($value) {
if($m === $value->getType()) {
return $value;
}
return $m;
}, $model);
} else {
if($value->getType() === $model) {
$model = $value;
break;
}
}
}
if($model->isNone()) {
if(!(\is_array($model)) && $model->isNone()) {
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => (in_array($produces, [
'image/*',
@ -200,13 +214,41 @@ class Swagger2 extends Format
],
];
} else {
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'schema' => [
'$ref' => '#/definitions/'.$model->getType(),
],
];
if(\is_array($model)) {
$modelDescription = \join(', or ', \array_map(function ($m) {
return $m->getName();
}, $model));
// model has multiple possible responses, we will use oneOf
foreach ($model as $m) {
$usedModels[] = $m->getType();
}
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $modelDescription,
'content' => [
$produces => [
'schema' => [
'oneOf' => \array_map(function($m) {
return ['$ref' => '#/definitions/'.$m->getType()];
}, $model)
],
],
],
];
} else {
// Response definition using one type
$usedModels[] = $model->getType();
$temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [
'description' => $model->getName(),
'content' => [
$produces => [
'schema' => [
'$ref' => '#/definitions/'.$model->getType(),
],
],
],
];
}
}
if(in_array($route->getLabel('sdk.response.code', 500), [204, 301, 302, 308], true)) {
@ -216,7 +258,7 @@ class Swagger2 extends Format
if ((!empty($scope))) { // && 'public' != $scope
$securities = ['Project' => []];
foreach($route->getLabel('sdk.auth', []) as $security) {
if(array_key_exists($security, $this->keys)) {
$securities[$security] = [];
@ -226,7 +268,7 @@ class Swagger2 extends Format
$temp['x-appwrite']['auth'] = array_slice($securities, 0, $this->authCount);
$temp['security'][] = $securities;
}
$body = [
'name' => 'payload',
'in' => 'body',
@ -274,7 +316,7 @@ class Swagger2 extends Format
case 'Utopia\Validator\Mock':
case 'Utopia\Validator\Assoc':
$node['type'] = 'object';
$param['default'] = (empty($param['default'])) ? new stdClass() : $param['default'];
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
$node['x-example'] = '{}';
//$node['format'] = 'json';
break;
@ -399,7 +441,7 @@ class Swagger2 extends Format
if($model->isAny()) {
$output['definitions'][$model->getType()]['additionalProperties'] = true;
}
if(!empty($required)) {
$output['definitions'][$model->getType()]['required'] = $required;
}
@ -414,7 +456,7 @@ class Swagger2 extends Format
case 'json':
$type = 'string';
break;
case 'integer':
$type = 'integer';
$format = 'int32';
@ -424,19 +466,40 @@ class Swagger2 extends Format
$type = 'number';
$format = 'float';
break;
case 'double':
$type = 'number';
$format = 'double';
break;
case 'boolean':
$type = 'boolean';
break;
default:
$type = 'object';
$rule['type'] = ($rule['type']) ? $rule['type'] : 'none';
$items = [
'type' => $type,
'$ref' => '#/definitions/'.$rule['type'],
];
if(\is_array($rule['type'])) {
if($rule['array']) {
$items = [
'anyOf' => \array_map(function($type) {
return ['$ref' => '#/definitions/'.$type];
}, $rule['type'])
];
} else {
$items = [
'oneOf' => \array_map(function($type) {
return ['$ref' => '#/definitions/'.$type];
}, $rule['type'])
];
}
} else {
$items = [
'type' => $type,
'$ref' => '#/definitions/'.$rule['type'],
];
}
break;
}

View file

@ -10,11 +10,11 @@ class Cron extends Validator
/**
* Get Description.
*
* Returns validator description
* Returns validator description.
*
* @return string
*/
public function getDescription()
public function getDescription(): string
{
return 'String must be a valid cron expression';
}
@ -28,7 +28,7 @@ class Cron extends Validator
*
* @return bool
*/
public function isValid($value)
public function isValid($value): bool
{
if (empty($value)) {
return true;
@ -42,7 +42,7 @@ class Cron extends Validator
}
/**
* Is array
* Is array.
*
* Function will return true if object is array.
*
@ -54,7 +54,7 @@ class Cron extends Validator
}
/**
* Get Type
* Get Type.
*
* Returns validator type.
*

View file

@ -13,7 +13,7 @@ class URL
*
* @return array
*/
public static function parse(string $url):array
public static function parse(string $url): array
{
$default = [
'scheme' => '',
@ -28,7 +28,7 @@ class URL
return \array_merge($default, \parse_url($url));
}
/**
* Un-Parse URL
*
@ -39,32 +39,32 @@ class URL
*
* @return string
*/
public static function unparse(array $url, array $ommit = []):string
public static function unparse(array $url, array $ommit = []): string
{
if (isset($url['path']) && \mb_substr($url['path'], 0, 1) !== '/') {
$url['path'] = '/'.$url['path'];
}
$parts = [];
$parts['scheme'] = isset($url['scheme']) ? $url['scheme'].'://' : '';
$parts['host'] = isset($url['host']) ? $url['host'] : '';
$parts['port'] = isset($url['port']) ? ':'.$url['port'] : '';
$parts['user'] = isset($url['user']) ? $url['user'] : '';
$parts['pass'] = isset($url['pass']) ? ':'.$url['pass'] : '';
$parts['pass'] = isset($url['pass']) ? ':'.$url['pass'] : '';
$parts['pass'] = ($parts['user'] || $parts['pass']) ? $parts['pass'].'@' : '';
$parts['path'] = isset($url['path']) ? $url['path'] : '';
$parts['query'] = isset($url['query']) && !empty($url['query']) ? '?'.$url['query'] : '';
$parts['fragment'] = isset($url['fragment']) ? '#'.$url['fragment'] : '';
if ($ommit) {
foreach ($ommit as $key) {
if (isset($parts[ $key ])) {
@ -72,7 +72,7 @@ class URL
}
}
}
return $parts['scheme'].$parts['user'].$parts['pass'].$parts['host'].$parts['port'].$parts['path'].$parts['query'].$parts['fragment'];
}
@ -85,7 +85,7 @@ class URL
*
* @return array
*/
public static function parseQuery(string $query):array
public static function parseQuery(string $query): array
{
\parse_str($query, $result);
@ -101,7 +101,7 @@ class URL
*
* @return string
*/
public static function unparseQuery(array $query):string
public static function unparseQuery(array $query): string
{
return \http_build_query($query);
}

View file

@ -11,6 +11,14 @@ use Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
use Appwrite\Utopia\Response\Model\AttributeList;
use Appwrite\Utopia\Response\Model\AttributeString;
use Appwrite\Utopia\Response\Model\AttributeInteger;
use Appwrite\Utopia\Response\Model\AttributeFloat;
use Appwrite\Utopia\Response\Model\AttributeBoolean;
use Appwrite\Utopia\Response\Model\AttributeEmail;
use Appwrite\Utopia\Response\Model\AttributeIP;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Continent;
@ -51,7 +59,6 @@ use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageUsers;
use stdClass;
/**
* @method Response setStatusCode(int $code = 200)
@ -79,13 +86,22 @@ class Response extends SwooleResponse
// Database
const MODEL_COLLECTION = 'collection';
const MODEL_COLLECTION_LIST = 'collectionList';
const MODEL_ATTRIBUTE = 'attribute';
const MODEL_ATTRIBUTE_LIST = 'attributeList';
const MODEL_INDEX = 'index';
const MODEL_INDEX_LIST = 'indexList';
const MODEL_DOCUMENT = 'document';
const MODEL_DOCUMENT_LIST = 'documentList';
// Database Attributes
const MODEL_ATTRIBUTE = 'attribute';
const MODEL_ATTRIBUTE_LIST = 'attributeList';
const MODEL_ATTRIBUTE_STRING = 'attributeString';
const MODEL_ATTRIBUTE_INTEGER= 'attributeInteger';
const MODEL_ATTRIBUTE_FLOAT= 'attributeFloat';
const MODEL_ATTRIBUTE_BOOLEAN= 'attributeBoolean';
const MODEL_ATTRIBUTE_EMAIL= 'attributeEmail';
const MODEL_ATTRIBUTE_IP= 'attributeIp';
const MODEL_ATTRIBUTE_URL= 'attributeUrl';
// Users
const MODEL_USER = 'user';
const MODEL_USER_LIST = 'userList';
@ -172,7 +188,6 @@ class Response extends SwooleResponse
->setModel(new ErrorDev())
// Lists
->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION))
->setModel(new BaseList('Attributes List', self::MODEL_ATTRIBUTE_LIST, 'attributes', self::MODEL_ATTRIBUTE))
->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX))
->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT))
->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER))
@ -198,6 +213,14 @@ class Response extends SwooleResponse
// Entities
->setModel(new Collection())
->setModel(new Attribute())
->setModel(new AttributeList())
->setModel(new AttributeString())
->setModel(new AttributeInteger())
->setModel(new AttributeFloat())
->setModel(new AttributeBoolean())
->setModel(new AttributeEmail())
->setModel(new AttributeIP())
->setModel(new AttributeURL())
->setModel(new Index())
->setModel(new ModelDocument())
->setModel(new Log())
@ -304,7 +327,7 @@ class Response extends SwooleResponse
$output = self::getFilter()->parse($output, $model);
}
$this->json(!empty($output) ? $output : new stdClass());
$this->json(!empty($output) ? $output : new \stdClass());
}
/**
@ -329,7 +352,7 @@ class Response extends SwooleResponse
$document = $model->filter($document);
foreach ($model->getRules() as $key => $rule) {
if (!$document->isSet($key)) {
if (!$document->isSet($key) && $rule['require']) { // do not set attribute in response if not required
if (!is_null($rule['default'])) {
$document->setAttribute($key, $rule['default']);
} else {
@ -344,15 +367,33 @@ class Response extends SwooleResponse
foreach ($data[$key] as &$item) {
if ($item instanceof Document) {
if (!array_key_exists($rule['type'], $this->models)) {
throw new Exception('Missing model for rule: '. $rule['type']);
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $type) {
$condition = false;
foreach ($this->getModel($type)->conditions as $attribute => $val) {
$condition = $item->getAttribute($attribute) === $val;
if(!$condition) {
break;
}
}
if ($condition) {
$ruleType = $type;
break;
}
}
} else {
$ruleType = $rule['type'];
}
$item = $this->output($item, $rule['type']);
if (!array_key_exists($ruleType, $this->models)) {
throw new Exception('Missing model for rule: '. $ruleType);
}
$item = $this->output($item, $ruleType);
}
}
}
$output[$key] = $data[$key];
}

View file

@ -8,7 +8,7 @@ abstract class Model
{
const TYPE_STRING = 'string';
const TYPE_INTEGER = 'integer';
const TYPE_FLOAT = 'float';
const TYPE_FLOAT = 'double';
const TYPE_BOOLEAN = 'boolean';
const TYPE_JSON = 'json';
@ -35,7 +35,7 @@ abstract class Model
/**
* Filter Document Structure
*
* @return string
* @return Document
*/
public function filter(Document $document): Document
{
@ -68,6 +68,10 @@ abstract class Model
/**
* Add a New Rule
* If rule is an array of documents with varying models
*
* @param string $key
* @param array $options
*/
protected function addRule(string $key, array $options): self
{
@ -77,7 +81,7 @@ abstract class Model
'description' => '',
'default' => null,
'example' => '',
'array' => false,
'array' => false
], $options);
return $this;

View file

@ -28,12 +28,6 @@ class Attribute extends Model
'default' => '',
'example' => 'available',
])
->addRule('size', [
'type' => self::TYPE_STRING,
'description' => 'Attribute size.',
'default' => 0,
'example' => 128,
])
->addRule('required', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is attribute required?',
@ -45,11 +39,13 @@ class Attribute extends Model
'description' => 'Is attribute an array?',
'default' => false,
'example' => false,
'required' => false
'require' => false
])
;
}
public array $conditions = [];
/**
* Get Name
*
@ -69,4 +65,4 @@ class Attribute extends Model
{
return Response::MODEL_ATTRIBUTE;
}
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeBoolean extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('default', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => false,
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_BOOLEAN
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeBoolean';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_BOOLEAN;
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeEmail extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('format', [
'type' => self::TYPE_STRING,
'description' => 'String format.',
'default' => APP_DATABASE_ATTRIBUTE_EMAIL,
'example' => APP_DATABASE_ATTRIBUTE_EMAIL,
'array' => false,
'require' => true,
])
->addRule('default', [
'type' => self::TYPE_STRING,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 'default@example.com',
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_STRING,
'format' => \APP_DATABASE_ATTRIBUTE_EMAIL
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeEmail';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_EMAIL;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeFloat extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('min', [
'type' => self::TYPE_FLOAT,
'description' => 'Minimum value to enforce for new documents.',
'default' => null,
'example' => 1.5,
'array' => false,
'require' => false,
])
->addRule('max', [
'type' => self::TYPE_FLOAT,
'description' => 'Maximum value to enforce for new documents.',
'default' => null,
'example' => 10.5,
'array' => false,
'require' => false,
])
->addRule('default', [
'type' => self::TYPE_FLOAT,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 2.5,
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_FLOAT,
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeFloat';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_FLOAT;
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeIP extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('format', [
'type' => self::TYPE_STRING,
'description' => 'String format.',
'default' => APP_DATABASE_ATTRIBUTE_IP,
'example' => APP_DATABASE_ATTRIBUTE_IP,
'array' => false,
'require' => true,
])
->addRule('default', [
'type' => self::TYPE_STRING,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => '192.0.2.0',
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_STRING,
'format' => \APP_DATABASE_ATTRIBUTE_IP
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeIP';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_IP;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeInteger extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('min', [
'type' => self::TYPE_INTEGER,
'description' => 'Minimum value to enforce for new documents.',
'default' => null,
'example' => 1,
'array' => false,
'require' => false,
])
->addRule('max', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum value to enforce for new documents.',
'default' => null,
'example' => 10,
'array' => false,
'require' => false,
])
->addRule('default', [
'type' => self::TYPE_INTEGER,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 10,
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_INTEGER,
];
/**
* Get Name *
* @return string
*/
public function getName():string
{
return 'AttributeInteger';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_INTEGER;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Document;
class AttributeList extends Model
{
public function __construct()
{
$this
->addRule('sum', [
'type' => self::TYPE_INTEGER,
'description' => 'Total sum of items in the list.',
'default' => 0,
'example' => 5,
])
->addRule('attributes', [
'type' => [
Response::MODEL_ATTRIBUTE_BOOLEAN,
Response::MODEL_ATTRIBUTE_INTEGER,
Response::MODEL_ATTRIBUTE_FLOAT,
Response::MODEL_ATTRIBUTE_EMAIL,
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_STRING // needs to be last, since its condition would dominate any other string attribute
],
'description' => 'List of attributes.',
'default' => [],
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'Attributes List';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_LIST;
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeString extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('size', [
'type' => self::TYPE_STRING,
'description' => 'Attribute size.',
'default' => 0,
'example' => 128,
])
->addRule('default', [
'type' => self::TYPE_STRING,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 'default',
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_STRING,
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeString';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_STRING;
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeURL extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('format', [
'type' => self::TYPE_STRING,
'description' => 'String format.',
'default' => APP_DATABASE_ATTRIBUTE_URL,
'example' => APP_DATABASE_ATTRIBUTE_URL,
'array' => false,
'required' => true,
])
->addRule('default', [
'type' => self::TYPE_STRING,
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'example' => 'http://example.com',
'array' => false,
'require' => false,
])
;
}
public array $conditions = [
'type' => self::TYPE_STRING,
'format' => \APP_DATABASE_ATTRIBUTE_URL
];
/**
* Get Name
*
* @return string
*/
public function getName():string
{
return 'AttributeURL';
}
/**
* Get Type
*
* @return string
*/
public function getType():string
{
return Response::MODEL_ATTRIBUTE_URL;
}
}

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use stdClass;
class Collection extends Model
{
@ -44,17 +43,25 @@ class Collection extends Model
'example' => 'document',
])
->addRule('attributes', [
'type' => Response::MODEL_ATTRIBUTE,
'type' => [
Response::MODEL_ATTRIBUTE_BOOLEAN,
Response::MODEL_ATTRIBUTE_INTEGER,
Response::MODEL_ATTRIBUTE_FLOAT,
Response::MODEL_ATTRIBUTE_EMAIL,
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_STRING, // needs to be last, since its condition would dominate any other string attribute
],
'description' => 'Collection attributes.',
'default' => [],
'example' => new stdClass,
'array' => true
'example' => new \stdClass,
'array' => true,
])
->addRule('indexes', [
'type' => Response::MODEL_INDEX,
'description' => 'Collection indexes.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
;

View file

@ -2,7 +2,6 @@
namespace Appwrite\Utopia\Response\Model;
use stdClass;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Config\Config;
@ -100,28 +99,28 @@ class Project extends Model
'type' => Response::MODEL_PLATFORM,
'description' => 'List of Platforms.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true,
])
->addRule('webhooks', [
'type' => Response::MODEL_WEBHOOK,
'description' => 'List of Webhooks.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true,
])
->addRule('keys', [
'type' => Response::MODEL_KEY,
'description' => 'List of API Keys.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true,
])
->addRule('domains', [
'type' => Response::MODEL_DOMAIN,
'description' => 'List of Domains.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true,
])
;

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use stdClass;
class UsageBuckets extends Model
{
@ -21,35 +20,35 @@ class UsageBuckets extends Model
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of files in this bucket.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('files.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for files created.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('files.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for files read.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('files.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for files updated.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('files.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for files deleted.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
;

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use stdClass;
class UsageCollection extends Model
{
@ -21,35 +20,35 @@ class UsageCollection extends Model
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of documents.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('documents.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents created.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('documents.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents read.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('documents.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents updated.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('documents.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents deleted.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
;

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use stdClass;
class UsageDatabase extends Model
{
@ -21,70 +20,70 @@ class UsageDatabase extends Model
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of documents.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('collections.count', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of collections.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('documents.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents created.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('documents.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents read.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('documents.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents updated.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('documents.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for documents deleted.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('collections.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections created.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('collections.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections read.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('collections.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections updated.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('collections.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for collections delete.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
;

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use stdClass;
class UsageFunctions extends Model
{
@ -21,21 +20,21 @@ class UsageFunctions extends Model
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function executions.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('functions.failures', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution failures.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('functions.compute', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function execution duration.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
;

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use stdClass;
class UsageProject extends Model
{
@ -21,49 +20,49 @@ class UsageProject extends Model
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of requests.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('network', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for consumed bandwidth.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('functions', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for function executions.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('documents', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of documents.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('collections', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of collections.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('users', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for number of users.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('storage', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for the occupied storage size (in bytes).',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
;

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use stdClass;
class UsageStorage extends Model
{
@ -21,14 +20,14 @@ class UsageStorage extends Model
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for the occupied storage size (in bytes).',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('files', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of files.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
;

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use stdClass;
class UsageUsers extends Model
{
@ -21,56 +20,56 @@ class UsageUsers extends Model
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for total number of users.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('users.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users created.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('users.read', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users read.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('users.update', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users updated.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('users.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for users deleted.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('sessions.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for sessions created.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('sessions.provider.create', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for sessions created for a provider ( email, anonymous or oauth2 ).',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
->addRule('sessions.delete', [
'type' => Response::MODEL_METRIC_LIST,
'description' => 'Aggregated stats for sessions deleted.',
'default' => [],
'example' => new stdClass,
'example' => new \stdClass,
'array' => true
])
;

View file

@ -73,7 +73,6 @@ trait DatabaseBase
$this->assertEquals($releaseYear['headers']['status-code'], 201);
$this->assertEquals($releaseYear['body']['key'], 'releaseYear');
$this->assertEquals($releaseYear['body']['type'], 'integer');
$this->assertEquals($releaseYear['body']['size'], 0);
$this->assertEquals($releaseYear['body']['required'], true);
$this->assertEquals($actors['headers']['status-code'], 201);
@ -101,6 +100,420 @@ trait DatabaseBase
return $data;
}
/**
* @depends testCreateAttributes
*/
public function testAttributeResponseModels(array $data): array
{
$collection= $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']
]), [
'collectionId' => 'unique()',
'name' => 'Response Models',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
]);
$this->assertEquals($collection['headers']['status-code'], 201);
$this->assertEquals($collection['body']['name'], 'Response Models');
$collectionId = $collection['body']['$id'];
$string = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'string',
'size' => 16,
'required' => false,
'default' => 'default',
]);
$email = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/email', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'email',
'required' => false,
'default' => 'default@example.com',
]);
$ip = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/ip', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'ip',
'required' => false,
'default' => '192.0.2.0',
]);
$url = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/url', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'url',
'required' => false,
'default' => 'http://example.com',
]);
$integer = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'integer',
'required' => false,
'min' => 1,
'max' => 5,
'default' => 3
]);
$float = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/float', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'float',
'required' => false,
'min' => 1.5,
'max' => 5.5,
'default' => 3.5
]);
$boolean = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/boolean', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'boolean',
'required' => false,
'default' => true,
]);
$this->assertEquals(201, $string['headers']['status-code']);
$this->assertEquals('string', $string['body']['key']);
$this->assertEquals('string', $string['body']['type']);
$this->assertEquals('processing', $string['body']['status']);
$this->assertEquals(false, $string['body']['required']);
$this->assertEquals(false, $string['body']['array']);
$this->assertEquals(16, $string['body']['size']);
$this->assertEquals('default', $string['body']['default']);
$this->assertEquals(201, $email['headers']['status-code']);
$this->assertEquals('email', $email['body']['key']);
$this->assertEquals('string', $email['body']['type']);
$this->assertEquals('processing', $email['body']['status']);
$this->assertEquals(false, $email['body']['required']);
$this->assertEquals(false, $email['body']['array']);
$this->assertEquals('email', $email['body']['format']);
$this->assertEquals('default@example.com', $email['body']['default']);
$this->assertEquals(201, $ip['headers']['status-code']);
$this->assertEquals('ip', $ip['body']['key']);
$this->assertEquals('string', $ip['body']['type']);
$this->assertEquals('processing', $ip['body']['status']);
$this->assertEquals(false, $ip['body']['required']);
$this->assertEquals(false, $ip['body']['array']);
$this->assertEquals('ip', $ip['body']['format']);
$this->assertEquals('192.0.2.0', $ip['body']['default']);
$this->assertEquals(201, $url['headers']['status-code']);
$this->assertEquals('url', $url['body']['key']);
$this->assertEquals('string', $url['body']['type']);
$this->assertEquals('processing', $url['body']['status']);
$this->assertEquals(false, $url['body']['required']);
$this->assertEquals(false, $url['body']['array']);
$this->assertEquals('url', $url['body']['format']);
$this->assertEquals('http://example.com', $url['body']['default']);
$this->assertEquals(201, $integer['headers']['status-code']);
$this->assertEquals('integer', $integer['body']['key']);
$this->assertEquals('integer', $integer['body']['type']);
$this->assertEquals('processing', $integer['body']['status']);
$this->assertEquals(false, $integer['body']['required']);
$this->assertEquals(false, $integer['body']['array']);
$this->assertEquals(1, $integer['body']['min']);
$this->assertEquals(5, $integer['body']['max']);
$this->assertEquals(3, $integer['body']['default']);
$this->assertEquals(201, $float['headers']['status-code']);
$this->assertEquals('float', $float['body']['key']);
$this->assertEquals('double', $float['body']['type']);
$this->assertEquals('processing', $float['body']['status']);
$this->assertEquals(false, $float['body']['required']);
$this->assertEquals(false, $float['body']['array']);
$this->assertEquals(1.5, $float['body']['min']);
$this->assertEquals(5.5, $float['body']['max']);
$this->assertEquals(3.5, $float['body']['default']);
$this->assertEquals(201, $boolean['headers']['status-code']);
$this->assertEquals('boolean', $boolean['body']['key']);
$this->assertEquals('boolean', $boolean['body']['type']);
$this->assertEquals('processing', $boolean['body']['status']);
$this->assertEquals(false, $boolean['body']['required']);
$this->assertEquals(false, $boolean['body']['array']);
$this->assertEquals(true, $boolean['body']['default']);
// wait for database worker to create attributes
sleep(5);
$stringResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$string['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$emailResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$email['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$ipResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$ip['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$urlResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$url['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$integerResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$integer['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$floatResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$float['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$booleanResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$boolean['body']['key']}",array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $stringResponse['headers']['status-code']);
$this->assertEquals($string['body']['key'], $stringResponse['body']['key']);
$this->assertEquals($string['body']['type'], $stringResponse['body']['type']);
$this->assertEquals('available', $stringResponse['body']['status']);
$this->assertEquals($string['body']['required'], $stringResponse['body']['required']);
$this->assertEquals($string['body']['array'], $stringResponse['body']['array']);
$this->assertEquals(16, $stringResponse['body']['size']);
$this->assertEquals($string['body']['default'], $stringResponse['body']['default']);
$this->assertEquals(200, $emailResponse['headers']['status-code']);
$this->assertEquals($email['body']['key'], $emailResponse['body']['key']);
$this->assertEquals($email['body']['type'], $emailResponse['body']['type']);
$this->assertEquals('available', $emailResponse['body']['status']);
$this->assertEquals($email['body']['required'], $emailResponse['body']['required']);
$this->assertEquals($email['body']['array'], $emailResponse['body']['array']);
$this->assertEquals($email['body']['format'], $emailResponse['body']['format']);
$this->assertEquals($email['body']['default'], $emailResponse['body']['default']);
$this->assertEquals(200, $ipResponse['headers']['status-code']);
$this->assertEquals($ip['body']['key'], $ipResponse['body']['key']);
$this->assertEquals($ip['body']['type'], $ipResponse['body']['type']);
$this->assertEquals('available', $ipResponse['body']['status']);
$this->assertEquals($ip['body']['required'], $ipResponse['body']['required']);
$this->assertEquals($ip['body']['array'], $ipResponse['body']['array']);
$this->assertEquals($ip['body']['format'], $ipResponse['body']['format']);
$this->assertEquals($ip['body']['default'], $ipResponse['body']['default']);
$this->assertEquals(200, $urlResponse['headers']['status-code']);
$this->assertEquals($url['body']['key'], $urlResponse['body']['key']);
$this->assertEquals($url['body']['type'], $urlResponse['body']['type']);
$this->assertEquals('available', $urlResponse['body']['status']);
$this->assertEquals($url['body']['required'], $urlResponse['body']['required']);
$this->assertEquals($url['body']['array'], $urlResponse['body']['array']);
$this->assertEquals($url['body']['format'], $urlResponse['body']['format']);
$this->assertEquals($url['body']['default'], $urlResponse['body']['default']);
$this->assertEquals(200, $integerResponse['headers']['status-code']);
$this->assertEquals($integer['body']['key'], $integerResponse['body']['key']);
$this->assertEquals($integer['body']['type'], $integerResponse['body']['type']);
$this->assertEquals('available', $integerResponse['body']['status']);
$this->assertEquals($integer['body']['required'], $integerResponse['body']['required']);
$this->assertEquals($integer['body']['array'], $integerResponse['body']['array']);
$this->assertEquals($integer['body']['min'], $integerResponse['body']['min']);
$this->assertEquals($integer['body']['max'], $integerResponse['body']['max']);
$this->assertEquals($integer['body']['default'], $integerResponse['body']['default']);
$this->assertEquals(200, $floatResponse['headers']['status-code']);
$this->assertEquals($float['body']['key'], $floatResponse['body']['key']);
$this->assertEquals($float['body']['type'], $floatResponse['body']['type']);
$this->assertEquals('available', $floatResponse['body']['status']);
$this->assertEquals($float['body']['required'], $floatResponse['body']['required']);
$this->assertEquals($float['body']['array'], $floatResponse['body']['array']);
$this->assertEquals($float['body']['min'], $floatResponse['body']['min']);
$this->assertEquals($float['body']['max'], $floatResponse['body']['max']);
$this->assertEquals($float['body']['default'], $floatResponse['body']['default']);
$this->assertEquals(200, $booleanResponse['headers']['status-code']);
$this->assertEquals($boolean['body']['key'], $booleanResponse['body']['key']);
$this->assertEquals($boolean['body']['type'], $booleanResponse['body']['type']);
$this->assertEquals('available', $booleanResponse['body']['status']);
$this->assertEquals($boolean['body']['required'], $booleanResponse['body']['required']);
$this->assertEquals($boolean['body']['array'], $booleanResponse['body']['array']);
$this->assertEquals($boolean['body']['default'], $booleanResponse['body']['default']);
$attributes = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId . '/attributes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $attributes['headers']['status-code']);
$this->assertEquals(7, $attributes['body']['sum']);
$attributes = $attributes['body']['attributes'];
$this->assertIsArray($attributes);
$this->assertCount(7, $attributes);
$this->assertEquals($stringResponse['body']['key'], $attributes[0]['key']);
$this->assertEquals($stringResponse['body']['type'], $attributes[0]['type']);
$this->assertEquals($stringResponse['body']['status'], $attributes[0]['status']);
$this->assertEquals($stringResponse['body']['required'], $attributes[0]['required']);
$this->assertEquals($stringResponse['body']['array'], $attributes[0]['array']);
$this->assertEquals($stringResponse['body']['size'], $attributes[0]['size']);
$this->assertEquals($stringResponse['body']['default'], $attributes[0]['default']);
$this->assertEquals($emailResponse['body']['key'], $attributes[1]['key']);
$this->assertEquals($emailResponse['body']['type'], $attributes[1]['type']);
$this->assertEquals($emailResponse['body']['status'], $attributes[1]['status']);
$this->assertEquals($emailResponse['body']['required'], $attributes[1]['required']);
$this->assertEquals($emailResponse['body']['array'], $attributes[1]['array']);
$this->assertEquals($emailResponse['body']['default'], $attributes[1]['default']);
$this->assertEquals($emailResponse['body']['format'], $attributes[1]['format']);
$this->assertEquals($ipResponse['body']['key'], $attributes[2]['key']);
$this->assertEquals($ipResponse['body']['type'], $attributes[2]['type']);
$this->assertEquals($ipResponse['body']['status'], $attributes[2]['status']);
$this->assertEquals($ipResponse['body']['required'], $attributes[2]['required']);
$this->assertEquals($ipResponse['body']['array'], $attributes[2]['array']);
$this->assertEquals($ipResponse['body']['default'], $attributes[2]['default']);
$this->assertEquals($ipResponse['body']['format'], $attributes[2]['format']);
$this->assertEquals($urlResponse['body']['key'], $attributes[3]['key']);
$this->assertEquals($urlResponse['body']['type'], $attributes[3]['type']);
$this->assertEquals($urlResponse['body']['status'], $attributes[3]['status']);
$this->assertEquals($urlResponse['body']['required'], $attributes[3]['required']);
$this->assertEquals($urlResponse['body']['array'], $attributes[3]['array']);
$this->assertEquals($urlResponse['body']['default'], $attributes[3]['default']);
$this->assertEquals($urlResponse['body']['format'], $attributes[3]['format']);
$this->assertEquals($integerResponse['body']['key'], $attributes[4]['key']);
$this->assertEquals($integerResponse['body']['type'], $attributes[4]['type']);
$this->assertEquals($integerResponse['body']['status'], $attributes[4]['status']);
$this->assertEquals($integerResponse['body']['required'], $attributes[4]['required']);
$this->assertEquals($integerResponse['body']['array'], $attributes[4]['array']);
$this->assertEquals($integerResponse['body']['default'], $attributes[4]['default']);
$this->assertEquals($integerResponse['body']['min'], $attributes[4]['min']);
$this->assertEquals($integerResponse['body']['max'], $attributes[4]['max']);
$this->assertEquals($floatResponse['body']['key'], $attributes[5]['key']);
$this->assertEquals($floatResponse['body']['type'], $attributes[5]['type']);
$this->assertEquals($floatResponse['body']['status'], $attributes[5]['status']);
$this->assertEquals($floatResponse['body']['required'], $attributes[5]['required']);
$this->assertEquals($floatResponse['body']['array'], $attributes[5]['array']);
$this->assertEquals($floatResponse['body']['default'], $attributes[5]['default']);
$this->assertEquals($floatResponse['body']['min'], $attributes[5]['min']);
$this->assertEquals($floatResponse['body']['max'], $attributes[5]['max']);
$this->assertEquals($booleanResponse['body']['key'], $attributes[6]['key']);
$this->assertEquals($booleanResponse['body']['type'], $attributes[6]['type']);
$this->assertEquals($booleanResponse['body']['status'], $attributes[6]['status']);
$this->assertEquals($booleanResponse['body']['required'], $attributes[6]['required']);
$this->assertEquals($booleanResponse['body']['array'], $attributes[6]['array']);
$this->assertEquals($booleanResponse['body']['default'], $attributes[6]['default']);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $collection['headers']['status-code']);
$attributes = $collection['body']['attributes'];
$this->assertIsArray($attributes);
$this->assertCount(7, $attributes);
$this->assertEquals($stringResponse['body']['key'], $attributes[0]['key']);
$this->assertEquals($stringResponse['body']['type'], $attributes[0]['type']);
$this->assertEquals($stringResponse['body']['status'], $attributes[0]['status']);
$this->assertEquals($stringResponse['body']['required'], $attributes[0]['required']);
$this->assertEquals($stringResponse['body']['array'], $attributes[0]['array']);
$this->assertEquals($stringResponse['body']['size'], $attributes[0]['size']);
$this->assertEquals($stringResponse['body']['default'], $attributes[0]['default']);
$this->assertEquals($emailResponse['body']['key'], $attributes[1]['key']);
$this->assertEquals($emailResponse['body']['type'], $attributes[1]['type']);
$this->assertEquals($emailResponse['body']['status'], $attributes[1]['status']);
$this->assertEquals($emailResponse['body']['required'], $attributes[1]['required']);
$this->assertEquals($emailResponse['body']['array'], $attributes[1]['array']);
$this->assertEquals($emailResponse['body']['default'], $attributes[1]['default']);
$this->assertEquals($emailResponse['body']['format'], $attributes[1]['format']);
$this->assertEquals($ipResponse['body']['key'], $attributes[2]['key']);
$this->assertEquals($ipResponse['body']['type'], $attributes[2]['type']);
$this->assertEquals($ipResponse['body']['status'], $attributes[2]['status']);
$this->assertEquals($ipResponse['body']['required'], $attributes[2]['required']);
$this->assertEquals($ipResponse['body']['array'], $attributes[2]['array']);
$this->assertEquals($ipResponse['body']['default'], $attributes[2]['default']);
$this->assertEquals($ipResponse['body']['format'], $attributes[2]['format']);
$this->assertEquals($urlResponse['body']['key'], $attributes[3]['key']);
$this->assertEquals($urlResponse['body']['type'], $attributes[3]['type']);
$this->assertEquals($urlResponse['body']['status'], $attributes[3]['status']);
$this->assertEquals($urlResponse['body']['required'], $attributes[3]['required']);
$this->assertEquals($urlResponse['body']['array'], $attributes[3]['array']);
$this->assertEquals($urlResponse['body']['default'], $attributes[3]['default']);
$this->assertEquals($urlResponse['body']['format'], $attributes[3]['format']);
$this->assertEquals($integerResponse['body']['key'], $attributes[4]['key']);
$this->assertEquals($integerResponse['body']['type'], $attributes[4]['type']);
$this->assertEquals($integerResponse['body']['status'], $attributes[4]['status']);
$this->assertEquals($integerResponse['body']['required'], $attributes[4]['required']);
$this->assertEquals($integerResponse['body']['array'], $attributes[4]['array']);
$this->assertEquals($integerResponse['body']['default'], $attributes[4]['default']);
$this->assertEquals($integerResponse['body']['min'], $attributes[4]['min']);
$this->assertEquals($integerResponse['body']['max'], $attributes[4]['max']);
$this->assertEquals($floatResponse['body']['key'], $attributes[5]['key']);
$this->assertEquals($floatResponse['body']['type'], $attributes[5]['type']);
$this->assertEquals($floatResponse['body']['status'], $attributes[5]['status']);
$this->assertEquals($floatResponse['body']['required'], $attributes[5]['required']);
$this->assertEquals($floatResponse['body']['array'], $attributes[5]['array']);
$this->assertEquals($floatResponse['body']['default'], $attributes[5]['default']);
$this->assertEquals($floatResponse['body']['min'], $attributes[5]['min']);
$this->assertEquals($floatResponse['body']['max'], $attributes[5]['max']);
$this->assertEquals($booleanResponse['body']['key'], $attributes[6]['key']);
$this->assertEquals($booleanResponse['body']['type'], $attributes[6]['type']);
$this->assertEquals($booleanResponse['body']['status'], $attributes[6]['status']);
$this->assertEquals($booleanResponse['body']['required'], $attributes[6]['required']);
$this->assertEquals($booleanResponse['body']['array'], $attributes[6]['array']);
$this->assertEquals($booleanResponse['body']['default'], $attributes[6]['default']);
return $data;
}
/**
* @depends testCreateAttributes
*/
@ -754,7 +1167,7 @@ trait DatabaseBase
// $this->assertEquals('Minimum value must be lesser than maximum value', $invalidRange['body']['message']);
// wait for worker to add attributes
sleep(2);
sleep(3);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
@ -1090,6 +1503,144 @@ trait DatabaseBase
return $data;
}
public function testEnforceCollectionPermissions()
{
$user = 'user:' . $this->getUser()['$id'];
$collection = $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']
]), [
'collectionId' => 'unique()',
'name' => 'enforceCollectionPermissions',
'permission' => 'collection',
'read' => [$user],
'write' => [$user]
]);
$this->assertEquals($collection['headers']['status-code'], 201);
$this->assertEquals($collection['body']['name'], 'enforceCollectionPermissions');
$this->assertEquals($collection['body']['permission'], 'collection');
$collectionId = $collection['body']['$id'];
sleep(2);
$attribute = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute',
'size' => 64,
'required' => true,
]);
$this->assertEquals(201, $attribute['headers']['status-code'], 201);
$this->assertEquals('attribute', $attribute['body']['key']);
// wait for db to add attribute
sleep(2);
$index = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'key_attribute',
'type' => 'key',
'attributes' => [$attribute['body']['key']],
]);
$this->assertEquals(201, $index['headers']['status-code']);
$this->assertEquals('key_attribute', $index['body']['key']);
// wait for db to add attribute
sleep(2);
$document1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'attribute' => 'one',
],
'read' => [$user],
'write' => [$user],
]);
$this->assertEquals(201, $document1['headers']['status-code']);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(1, $documents['body']['sum']);
$this->assertCount(1, $documents['body']['documents']);
/*
* Test for Failure
*/
// Remove write permission
$collection = $this->client->call(Client::METHOD_PUT, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'enforceCollectionPermissions',
'permission' => 'collection',
'read' => [$user],
'write' => []
]);
$this->assertEquals(200, $collection['headers']['status-code']);
$badDocument = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => 'unique()',
'data' => [
'attribute' => 'bad',
],
'read' => [$user],
'write' => [$user],
]);
if($this->getSide() == 'client') {
$this->assertEquals(401, $badDocument['headers']['status-code']);
}
if($this->getSide() == 'server') {
$this->assertEquals(201, $badDocument['headers']['status-code']);
}
// Remove read permission
$collection = $this->client->call(Client::METHOD_PUT, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'enforceCollectionPermissions',
'permission' => 'collection',
'read' => [],
'write' => []
]);
$this->assertEquals(200, $collection['headers']['status-code']);
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(404, $documents['headers']['status-code']);
}
/**
* @depends testDefaultPermissions
*/

View file

@ -241,6 +241,214 @@ class DatabaseCustomServerTest extends Scope
/**
* @depends testDeleteIndex
*/
public function testDeleteIndexOnDeleteAttribute($data)
{
$attribute1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['collectionId'] . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute1',
'size' => 16,
'required' => true,
]);
$attribute2 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['collectionId'] . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute2',
'size' => 16,
'required' => true,
]);
$this->assertEquals(201, $attribute1['headers']['status-code']);
$this->assertEquals(201, $attribute2['headers']['status-code']);
$this->assertEquals('attribute1', $attribute1['body']['key']);
$this->assertEquals('attribute2', $attribute2['body']['key']);
sleep(2);
$index1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['collectionId'] . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'index1',
'type' => 'key',
'attributes' => ['attribute1', 'attribute2'],
'orders' => ['ASC', 'ASC'],
]);
$index2 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['collectionId'] . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'index2',
'type' => 'key',
'attributes' => ['attribute2'],
]);
$this->assertEquals(201, $index1['headers']['status-code']);
$this->assertEquals(201, $index2['headers']['status-code']);
$this->assertEquals('index1', $index1['body']['key']);
$this->assertEquals('index2', $index2['body']['key']);
sleep(2);
// Expected behavior: deleting attribute2 will cause index2 to be dropped, and index1 rebuilt with a single key
$deleted = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['collectionId'] . '/attributes/'. $attribute2['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
// wait for database worker to complete
sleep(2);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['collectionId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertIsArray($collection['body']['indexes']);
$this->assertCount(1, $collection['body']['indexes']);
$this->assertEquals($index1['body']['key'], $collection['body']['indexes'][0]['key']);
$this->assertIsArray($collection['body']['indexes'][0]['attributes']);
$this->assertCount(1, $collection['body']['indexes'][0]['attributes']);
$this->assertEquals($attribute1['body']['key'], $collection['body']['indexes'][0]['attributes'][0]);
// Delete attribute
$deleted = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['collectionId'] . '/attributes/' . $attribute1['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
return $data;
}
public function testCleanupDuplicateIndexOnDeleteAttribute()
{
$collection = $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']
]), [
'collectionId' => 'unique()',
'name' => 'TestCleanupDuplicateIndexOnDeleteAttribute',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertNotEmpty($collection['body']['$id']);
$collectionId = $collection['body']['$id'];
$attribute1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute1',
'size' => 16,
'required' => true,
]);
$attribute2 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'attribute2',
'size' => 16,
'required' => true,
]);
$this->assertEquals(201, $attribute1['headers']['status-code']);
$this->assertEquals(201, $attribute2['headers']['status-code']);
$this->assertEquals('attribute1', $attribute1['body']['key']);
$this->assertEquals('attribute2', $attribute2['body']['key']);
sleep(2);
$index1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'index1',
'type' => 'key',
'attributes' => ['attribute1', 'attribute2'],
'orders' => ['ASC', 'ASC'],
]);
$index2 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'indexId' => 'index2',
'type' => 'key',
'attributes' => ['attribute2'],
]);
$this->assertEquals(201, $index1['headers']['status-code']);
$this->assertEquals(201, $index2['headers']['status-code']);
$this->assertEquals('index1', $index1['body']['key']);
$this->assertEquals('index2', $index2['body']['key']);
sleep(2);
// Expected behavior: deleting attribute1 would cause index1 to be a duplicate of index2 and automatically removed
$deleted = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $collectionId . '/attributes/'. $attribute1['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
// wait for database worker to complete
sleep(2);
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $collection['headers']['status-code']);
$this->assertIsArray($collection['body']['indexes']);
$this->assertCount(1, $collection['body']['indexes']);
$this->assertEquals($index2['body']['key'], $collection['body']['indexes'][0]['key']);
$this->assertIsArray($collection['body']['indexes'][0]['attributes']);
$this->assertCount(1, $collection['body']['indexes'][0]['attributes']);
$this->assertEquals($attribute2['body']['key'], $collection['body']['indexes'][0]['attributes'][0]);
// Delete attribute
$deleted = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $collectionId . '/attributes/' . $attribute2['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals($deleted['headers']['status-code'], 204);
}
/**
* @depends testDeleteIndexOnDeleteAttribute
*/
public function testDeleteCollection($data)
{
$collectionId = $data['collectionId'];
@ -307,6 +515,101 @@ class DatabaseCustomServerTest extends Scope
$this->assertEquals($response['headers']['status-code'], 404);
}
public function testAttributeCountLimit()
{
$collection = $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']
]), [
'collectionId' => 'unique()',
'name' => 'attributeCountLimit',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
]);
$collectionId = $collection['body']['$id'];
// load the collection up to the limit
for ($i=0; $i < 1012; $i++) {
$attribute = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => "attribute{$i}",
'required' => false,
]);
$this->assertEquals(201, $attribute['headers']['status-code']);
}
sleep(5);
$tooMany = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => "tooMany",
'required' => false,
]);
$this->assertEquals(400, $tooMany['headers']['status-code']);
$this->assertEquals('Attribute limit exceeded', $tooMany['body']['message']);
}
public function testAttributeRowWidthLimit()
{
$collection = $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']
]), [
'collectionId' => 'attributeRowWidthLimit',
'name' => 'attributeRowWidthLimit',
'read' => ['role:all'],
'write' => ['role:all'],
'permission' => 'document',
]);
$this->assertEquals($collection['headers']['status-code'], 201);
$this->assertEquals($collection['body']['name'], 'attributeRowWidthLimit');
$collectionId = $collection['body']['$id'];
// Add wide string attributes to approach row width limit
for ($i=0; $i < 15; $i++) {
$attribute = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => "attribute{$i}",
'size' => 1024,
'required' => true,
]);
$this->assertEquals($attribute['headers']['status-code'], 201);
}
sleep(5);
$tooWide = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'attributeId' => 'tooWide',
'size' => 1024,
'required' => true,
]);
$this->assertEquals(400, $tooWide['headers']['status-code']);
$this->assertEquals('Attribute limit exceeded', $tooWide['body']['message']);
}
public function testIndexLimitException()
{
$collection = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([

View file

@ -78,6 +78,45 @@ class FunctionsCustomServerTest extends Scope
* Test for SUCCESS
*/
/**
* Test search queries
*/
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $data['functionId']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertCount(1, $response['body']['functions']);
$this->assertEquals($response['body']['functions'][0]['name'], 'Test');
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'Test'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertCount(1, $response['body']['functions']);
$this->assertEquals($response['body']['functions'][0]['$id'], $data['functionId']);
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'php-8.0'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertCount(1, $response['body']['functions']);
$this->assertEquals($response['body']['functions'][0]['$id'], $data['functionId']);
/**
* Test pagination
*/
$response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -122,7 +161,6 @@ class FunctionsCustomServerTest extends Scope
$this->assertCount(1, $response['body']['functions']);
$this->assertEquals($response['body']['functions'][0]['name'], 'Test 2');
return $data;
}
@ -283,6 +321,48 @@ class FunctionsCustomServerTest extends Scope
$this->assertIsArray($function['body']['tags']);
$this->assertCount(1, $function['body']['tags']);
/**
* Test search queries
*/
$function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/tags', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders(), [
'search' => $data['functionId']
]));
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertEquals($function['body']['sum'], 1);
$this->assertIsArray($function['body']['tags']);
$this->assertCount(1, $function['body']['tags']);
$this->assertEquals($function['body']['tags'][0]['$id'], $data['tagId']);
$function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/tags', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders(), [
'search' => 'Test'
]));
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertEquals($function['body']['sum'], 1);
$this->assertIsArray($function['body']['tags']);
$this->assertCount(1, $function['body']['tags']);
$this->assertEquals($function['body']['tags'][0]['$id'], $data['tagId']);
$function = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/tags', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders(), [
'search' => 'php-8.0'
]));
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertEquals($function['body']['sum'], 1);
$this->assertIsArray($function['body']['tags']);
$this->assertCount(1, $function['body']['tags']);
$this->assertEquals($function['body']['tags'][0]['$id'], $data['tagId']);
return $data;
}
@ -395,6 +475,36 @@ class FunctionsCustomServerTest extends Scope
$this->assertCount(1, $function['body']['executions']);
$this->assertEquals($function['body']['executions'][0]['$id'], $data['executionId']);
/**
* Test search queries
*/
$response = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $data['executionId'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['sum']);
$this->assertIsInt($response['body']['sum']);
$this->assertCount(1, $response['body']['executions']);
$this->assertEquals($data['functionId'], $response['body']['executions'][0]['functionId']);
$response = $this->client->call(Client::METHOD_GET, '/functions/'.$data['functionId'].'/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $data['functionId'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['sum']);
$this->assertIsInt($response['body']['sum']);
$this->assertCount(1, $response['body']['executions']);
$this->assertEquals($data['executionId'], $response['body']['executions'][0]['$id']);
return $data;
}

View file

@ -87,6 +87,7 @@ class ProjectsConsoleClientTest extends Scope
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -97,6 +98,35 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals($id, $response['body']['projects'][0]['$id']);
$this->assertEquals('Project Test', $response['body']['projects'][0]['name']);
/**
* Test search queries
*/
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders(), [
'search' => $id
]));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals($response['body']['sum'], 1);
$this->assertIsArray($response['body']['projects']);
$this->assertCount(1, $response['body']['projects']);
$this->assertEquals($response['body']['projects'][0]['name'], 'Project Test');
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders(), [
'search' => 'Project Test'
]));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals($response['body']['sum'], 1);
$this->assertIsArray($response['body']['projects']);
$this->assertCount(1, $response['body']['projects']);
$this->assertEquals($response['body']['projects'][0]['$id'], $data['projectId']);
/**
* Test after pagination
*/
@ -1422,6 +1452,17 @@ class ProjectsConsoleClientTest extends Scope
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/platforms/error', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Flutter App (Android) 2',
'key' => 'com.example.android2',
'store' => '',
'hostname' => '',
]);
$this->assertEquals(404, $response['headers']['status-code']);
return $data;
}

View file

@ -163,7 +163,7 @@ trait StorageBase
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => 'unique()',
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/file.png'), 'image/png', 'file.png'),
'read' => ['role:all'],
'write' => ['role:all'],
]);
@ -171,9 +171,9 @@ trait StorageBase
$this->assertEquals($file['headers']['status-code'], 201);
$this->assertNotEmpty($file['body']['$id']);
$this->assertIsInt($file['body']['dateCreated']);
$this->assertEquals('logo.png', $file['body']['name']);
$this->assertEquals('image/png', $file['body']['mimeType']);
$this->assertEquals(47218, $file['body']['sizeOriginal']);
$this->assertEquals('file.png', $file['body']['name']);
$this->assertEquals('image/jpeg', $file['body']['mimeType']);
$this->assertEquals(16804, $file['body']['sizeOriginal']);
$files = $this->client->call(Client::METHOD_GET, '/storage/files', array_merge([
'content-type' => 'application/json',
@ -197,6 +197,32 @@ trait StorageBase
$this->assertEquals($files['body']['files'][1]['$id'], $response['body']['files'][0]['$id']);
$this->assertCount(1, $response['body']['files']);
$response = $this->client->call(Client::METHOD_GET, '/storage/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $data['fileId'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['sum']);
$this->assertIsInt($response['body']['sum']);
$this->assertCount(1, $response['body']['files']);
$this->assertEquals('logo.png', $response['body']['files'][0]['name']);
$response = $this->client->call(Client::METHOD_GET, '/storage/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'logo',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['sum']);
$this->assertIsInt($response['body']['sum']);
$this->assertCount(1, $response['body']['files']);
$this->assertEquals($data['fileId'], $response['body']['files'][0]['$id']);
/**
* Test for FAILURE
*/

View file

@ -172,6 +172,19 @@ trait TeamsBase
$this->assertCount(1, $response['body']['teams']);
$this->assertEquals('Manchester United', $response['body']['teams'][0]['name']);
$response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $data['teamUid'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThan(0, $response['body']['sum']);
$this->assertIsInt($response['body']['sum']);
$this->assertCount(1, $response['body']['teams']);
$this->assertEquals('Arsenal', $response['body']['teams'][0]['name']);
$teams = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],

View file

@ -16,14 +16,14 @@ trait UsersBase
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'email' => 'users.service@example.com',
'email' => 'cristiano.ronaldo@manchester-united.co.uk',
'password' => 'password',
'name' => 'Project User',
'name' => 'Cristiano Ronaldo',
]);
$this->assertEquals($user['headers']['status-code'], 201);
$this->assertEquals($user['body']['name'], 'Project User');
$this->assertEquals($user['body']['email'], 'users.service@example.com');
$this->assertEquals($user['body']['name'], 'Cristiano Ronaldo');
$this->assertEquals($user['body']['email'], 'cristiano.ronaldo@manchester-united.co.uk');
$this->assertEquals($user['body']['status'], true);
$this->assertGreaterThan(0, $user['body']['registration']);
@ -35,15 +35,15 @@ trait UsersBase
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'user1',
'email' => 'users.service1@example.com',
'email' => 'lionel.messi@psg.fr',
'password' => 'password',
'name' => 'Project User',
'name' => 'Lionel Messi',
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['$id'], 'user1');
$this->assertEquals($res['body']['name'], 'Project User');
$this->assertEquals($res['body']['email'], 'users.service1@example.com');
$this->assertEquals($res['body']['name'], 'Lionel Messi');
$this->assertEquals($res['body']['email'], 'lionel.messi@psg.fr');
$this->assertEquals(true, $res['body']['status']);
$this->assertGreaterThan(0, $res['body']['registration']);
@ -56,7 +56,7 @@ trait UsersBase
public function testListUsers(array $data): void
{
/**
* Test for SUCCESS
* Test for SUCCESS listUsers
*/
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
@ -82,8 +82,72 @@ trait UsersBase
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], 'user1');
/**
* Test for SUCCESS searchUsers
*/
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'Ronaldo'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'cristiano.ronaldo'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'manchester'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'manchester-united.co.uk'
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertIsArray($response['body']);
$this->assertIsArray($response['body']['users']);
$this->assertIsInt($response['body']['sum']);
$this->assertEquals(1, $response['body']['sum']);
$this->assertCount(1, $response['body']['users']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $data['userId']
]);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
}
/**
@ -100,8 +164,8 @@ trait UsersBase
], $this->getHeaders()));
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertEquals($user['body']['name'], 'Project User');
$this->assertEquals($user['body']['email'], 'users.service@example.com');
$this->assertEquals($user['body']['name'], 'Cristiano Ronaldo');
$this->assertEquals($user['body']['email'], 'cristiano.ronaldo@manchester-united.co.uk');
$this->assertEquals($user['body']['status'], true);
$this->assertGreaterThan(0, $user['body']['registration']);
@ -132,21 +196,6 @@ trait UsersBase
$this->assertIsInt($users['body']['sum']);
$this->assertGreaterThan(0, $users['body']['sum']);
$users = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'example.com'
]);
$this->assertEquals($users['headers']['status-code'], 200);
$this->assertIsArray($users['body']);
$this->assertIsArray($users['body']['users']);
$this->assertIsInt($users['body']['sum']);
$this->assertEquals(2, $users['body']['sum']);
$this->assertGreaterThan(0, $users['body']['sum']);
$this->assertCount(2, $users['body']['users']);
return $data;
}

View file

@ -2,6 +2,7 @@
namespace Tests\E2E\Services\Users;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
@ -11,4 +12,49 @@ class UsersCustomServerTest extends Scope
use UsersBase;
use ProjectCustom;
use SideServer;
public function testDeprecatedUsers():array
{
/**
* Test for FAILURE (don't allow recreating account with same custom ID)
*/
// Create user with custom ID 'meldiron'
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'meldiron',
'email' => 'matej@appwrite.io',
'password' => 'my-superstr0ng-password',
'name' => 'Matej Bačo'
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Delete user with custom ID 'meldiron'
$response = $this->client->call(Client::METHOD_DELETE, '/users/meldiron', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
// Try to create user with custom ID 'meldiron' again, but now it should fail
$response1 = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'meldiron',
'email' => 'matej2@appwrite.io',
'password' => 'someones-superstr0ng-password',
'name' => 'Matej Bačo Second'
]);
$this->assertEquals(409, $response1['headers']['status-code']);
$this->assertEquals('Account already exists', $response1['body']['message']);
return [];
}
}