Merge branch 'feat-database-indexing' of https://github.com/appwrite/appwrite into feat-db-refactor-ui-fixes
This commit is contained in:
commit
df1398328c
|
@ -76,13 +76,17 @@ function createAttribute($collectionId, $attribute, $response, $dbForInternal, $
|
|||
throw new Exception('Cannot set default value for required attribute', 400);
|
||||
}
|
||||
|
||||
if ($array && $default) {
|
||||
throw new Exception('Cannot set default value for array attributes', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$attribute = new Document([
|
||||
'$id' => $collectionId.'_'.$attributeId,
|
||||
'key' => $attributeId,
|
||||
'collectionId' => $collectionId,
|
||||
'type' => $type,
|
||||
'status' => 'processing', // processing, available, failed, deleting
|
||||
'status' => 'processing', // processing, available, failed, deleting, stuck
|
||||
'size' => $size,
|
||||
'required' => $required,
|
||||
'signed' => $signed,
|
||||
|
@ -667,13 +671,13 @@ App::post('/v1/database/collections/:collectionId/attributes/string')
|
|||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.namespace', 'database')
|
||||
->label('sdk.method', 'createStringAttribute')
|
||||
->label('sdk.description', '/docs/references/database/create-attribute-string.md')
|
||||
->label('sdk.description', '/docs/references/database/create-string-attribute.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->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.')
|
||||
->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Range::TYPE_INTEGER), 'Attribute size for text attributes, in number of characters.')
|
||||
->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('array', false, new Boolean(), 'Is attribute an array?', true)
|
||||
|
@ -715,7 +719,7 @@ App::post('/v1/database/collections/:collectionId/attributes/email')
|
|||
->label('sdk.namespace', 'database')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createEmailAttribute')
|
||||
->label('sdk.description', '/docs/references/database/create-attribute-email.md')
|
||||
->label('sdk.description', '/docs/references/database/create-email-attribute.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_EMAIL)
|
||||
|
@ -812,7 +816,7 @@ App::post('/v1/database/collections/:collectionId/attributes/ip')
|
|||
->label('sdk.namespace', 'database')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createIpAttribute')
|
||||
->label('sdk.description', '/docs/references/database/create-attribute-ip.md')
|
||||
->label('sdk.description', '/docs/references/database/create-ip-attribute.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_IP)
|
||||
|
@ -854,7 +858,7 @@ App::post('/v1/database/collections/:collectionId/attributes/url')
|
|||
->label('sdk.namespace', 'database')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createUrlAttribute')
|
||||
->label('sdk.description', '/docs/references/database/create-attribute-url.md')
|
||||
->label('sdk.description', '/docs/references/database/create-url-attribute.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_URL)
|
||||
|
@ -896,7 +900,7 @@ App::post('/v1/database/collections/:collectionId/attributes/integer')
|
|||
->label('sdk.namespace', 'database')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createIntegerAttribute')
|
||||
->label('sdk.description', '/docs/references/database/create-attribute-integer.md')
|
||||
->label('sdk.description', '/docs/references/database/create-integer-attribute.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_INTEGER)
|
||||
|
@ -922,6 +926,11 @@ App::post('/v1/database/collections/:collectionId/attributes/integer')
|
|||
// Ensure attribute default is within range
|
||||
$min = (is_null($min)) ? PHP_INT_MIN : \intval($min);
|
||||
$max = (is_null($max)) ? PHP_INT_MAX : \intval($max);
|
||||
|
||||
if ($min > $max) {
|
||||
throw new Exception('Minimum value must be lesser than maximum value', 400);
|
||||
}
|
||||
|
||||
$validator = new Range($min, $max, Database::VAR_INTEGER);
|
||||
|
||||
if (!is_null($default) && !$validator->isValid($default)) {
|
||||
|
@ -960,7 +969,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float')
|
|||
->label('sdk.namespace', 'database')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createFloatAttribute')
|
||||
->label('sdk.description', '/docs/references/database/create-attribute-float.md')
|
||||
->label('sdk.description', '/docs/references/database/create-float-attribute.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_FLOAT)
|
||||
|
@ -986,6 +995,11 @@ App::post('/v1/database/collections/:collectionId/attributes/float')
|
|||
// Ensure attribute default is within range
|
||||
$min = (is_null($min)) ? PHP_FLOAT_MIN : \floatval($min);
|
||||
$max = (is_null($max)) ? PHP_FLOAT_MAX : \floatval($max);
|
||||
|
||||
if ($min > $max) {
|
||||
throw new Exception('Minimum value must be lesser than maximum value', 400);
|
||||
}
|
||||
|
||||
$validator = new Range($min, $max, Database::VAR_FLOAT);
|
||||
|
||||
if (!is_null($default) && !$validator->isValid($default)) {
|
||||
|
@ -1024,7 +1038,7 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean')
|
|||
->label('sdk.namespace', 'database')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
|
||||
->label('sdk.method', 'createBooleanAttribute')
|
||||
->label('sdk.description', '/docs/references/database/create-attribute-boolean.md')
|
||||
->label('sdk.description', '/docs/references/database/create-boolean-attribute.md')
|
||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_BOOLEAN)
|
||||
|
@ -1194,7 +1208,11 @@ App::delete('/v1/database/collections/:collectionId/attributes/:attributeId')
|
|||
throw new Exception('Attribute not found', 404);
|
||||
}
|
||||
|
||||
$attribute = $dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
|
||||
// Only update status if removing available attribute
|
||||
if ($attribute->getAttribute('status' === 'available')) {
|
||||
$attribute = $dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
|
||||
}
|
||||
|
||||
$dbForInternal->deleteCachedDocument('collections', $collectionId);
|
||||
|
||||
$database
|
||||
|
@ -1205,8 +1223,26 @@ App::delete('/v1/database/collections/:collectionId/attributes/:attributeId')
|
|||
|
||||
$usage->setParam('database.collections.update', 1);
|
||||
|
||||
// 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_ENUM => Response::MODEL_ATTRIBUTE_ENUM,
|
||||
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,
|
||||
};
|
||||
|
||||
$events
|
||||
->setParam('payload', $response->output($attribute, Response::MODEL_ATTRIBUTE))
|
||||
->setParam('payload', $response->output($attribute, $model))
|
||||
;
|
||||
|
||||
$audits
|
||||
|
@ -1271,7 +1307,6 @@ App::post('/v1/database/collections/:collectionId/indexes')
|
|||
// lengths hidden by default
|
||||
$lengths = [];
|
||||
|
||||
// set attribute size as length for strings, null otherwise
|
||||
foreach ($attributes as $key => $attribute) {
|
||||
// find attribute metadata in collection document
|
||||
$attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key'));
|
||||
|
@ -1280,10 +1315,16 @@ App::post('/v1/database/collections/:collectionId/indexes')
|
|||
throw new Exception('Unknown attribute: ' . $attribute, 400);
|
||||
}
|
||||
|
||||
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
|
||||
$attributeType = $oldAttributes[$attributeIndex]['type'];
|
||||
$attributeSize = $oldAttributes[$attributeIndex]['size'];
|
||||
|
||||
// Only set length for indexes on strings
|
||||
// ensure attribute is available
|
||||
if ($attributeStatus !== 'available') {
|
||||
throw new Exception ('Attribute not available: ' . $oldAttributes[$attributeIndex]['key'], 400);
|
||||
}
|
||||
|
||||
// set attribute size as index length only for strings
|
||||
$lengths[$key] = ($attributeType === Database::VAR_STRING) ? $attributeSize : null;
|
||||
}
|
||||
|
||||
|
@ -1291,7 +1332,7 @@ App::post('/v1/database/collections/:collectionId/indexes')
|
|||
$index = $dbForInternal->createDocument('indexes', new Document([
|
||||
'$id' => $collectionId.'_'.$indexId,
|
||||
'key' => $indexId,
|
||||
'status' => 'processing', // processing, available, failed, deleting
|
||||
'status' => 'processing', // processing, available, failed, deleting, stuck
|
||||
'collectionId' => $collectionId,
|
||||
'type' => $type,
|
||||
'attributes' => $attributes,
|
||||
|
@ -1446,7 +1487,11 @@ App::delete('/v1/database/collections/:collectionId/indexes/:indexId')
|
|||
throw new Exception('Index not found', 404);
|
||||
}
|
||||
|
||||
$index = $dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting'));
|
||||
// Only update status if removing available index
|
||||
if ($index->getAttribute('status') === 'available') {
|
||||
$index = $dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting'));
|
||||
}
|
||||
|
||||
$dbForInternal->deleteCachedDocument('collections', $collectionId);
|
||||
|
||||
$database
|
||||
|
@ -1550,7 +1595,7 @@ App::post('/v1/database/collections/:collectionId/documents')
|
|||
$usage
|
||||
->setParam('database.documents.create', 1)
|
||||
->setParam('collectionId', $collectionId)
|
||||
;
|
||||
;
|
||||
|
||||
$audits
|
||||
->setParam('event', 'database.documents.create')
|
||||
|
|
|
@ -70,6 +70,7 @@ 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_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH = 1073741824; // 2^32 bits / 4 bits per char
|
||||
const APP_STORAGE_UPLOADS = '/storage/uploads';
|
||||
const APP_STORAGE_FUNCTIONS = '/storage/functions';
|
||||
const APP_STORAGE_CACHE = '/storage/cache';
|
||||
|
@ -93,6 +94,11 @@ const DATABASE_TYPE_DELETE_ATTRIBUTE = 'deleteAttribute';
|
|||
const DATABASE_TYPE_DELETE_INDEX = 'deleteIndex';
|
||||
// Deletion Types
|
||||
const DELETE_TYPE_DOCUMENT = 'document';
|
||||
const DELETE_TYPE_COLLECTIONS = 'collections';
|
||||
const DELETE_TYPE_PROJECTS = 'projects';
|
||||
const DELETE_TYPE_FUNCTIONS = 'functions';
|
||||
const DELETE_TYPE_USERS = 'users';
|
||||
const DELETE_TYPE_TEAMS= 'teams';
|
||||
const DELETE_TYPE_EXECUTIONS = 'executions';
|
||||
const DELETE_TYPE_AUDIT = 'audit';
|
||||
const DELETE_TYPE_ABUSE = 'abuse';
|
||||
|
|
|
@ -108,14 +108,14 @@ class DatabaseV1 extends Worker
|
|||
$key = $attribute->getAttribute('key', '');
|
||||
|
||||
try {
|
||||
if(!$dbForExternal->deleteAttribute($collectionId, $key)) {
|
||||
if(!$dbForExternal->deleteAttribute($collectionId, $key) && $attribute->getAttribute('status') !== 'failed') {
|
||||
throw new Exception('Failed to delete Attribute');
|
||||
}
|
||||
|
||||
$dbForInternal->deleteDocument('attributes', $attribute->getId());
|
||||
} catch (\Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
$dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
|
||||
$dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'stuck'));
|
||||
}
|
||||
|
||||
// The underlying database removes/rebuilds indexes when attribute is removed
|
||||
|
@ -216,14 +216,14 @@ class DatabaseV1 extends Worker
|
|||
$key = $index->getAttribute('key');
|
||||
|
||||
try {
|
||||
if(!$dbForExternal->deleteIndex($collectionId, $key)) {
|
||||
if(!$dbForExternal->deleteIndex($collectionId, $key) && $index->getAttribute('status') !== 'failed') {
|
||||
throw new Exception('Failed to delete index');
|
||||
}
|
||||
|
||||
$dbForInternal->deleteDocument('indexes', $index->getId());
|
||||
} catch (\Throwable $th) {
|
||||
Console::error($th->getMessage());
|
||||
$dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
|
||||
$dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'stuck'));
|
||||
}
|
||||
|
||||
$dbForInternal->deleteCachedDocument('collections', $collectionId);
|
||||
|
|
|
@ -38,20 +38,19 @@ class DeletesV1 extends Worker
|
|||
$document = new Document($document);
|
||||
|
||||
switch ($document->getCollection()) {
|
||||
// TODO@kodumbeats define these as constants somewhere
|
||||
case 'collections':
|
||||
case DELETE_TYPE_COLLECTIONS:
|
||||
$this->deleteCollection($document, $projectId);
|
||||
break;
|
||||
case 'projects':
|
||||
case DELETE_TYPE_PROJECTS:
|
||||
$this->deleteProject($document);
|
||||
break;
|
||||
case 'functions':
|
||||
case DELETE_TYPE_FUNCTIONS:
|
||||
$this->deleteFunction($document, $projectId);
|
||||
break;
|
||||
case 'users':
|
||||
case DELETE_TYPE_USERS:
|
||||
$this->deleteUser($document, $projectId);
|
||||
break;
|
||||
case 'teams':
|
||||
case DELETE_TYPE_TEAMS:
|
||||
$this->deleteMemberships($document, $projectId);
|
||||
break;
|
||||
default:
|
||||
|
@ -291,7 +290,6 @@ class DeletesV1 extends Worker
|
|||
{
|
||||
Authorization::disable();
|
||||
|
||||
// TODO@kodumbeats is it better to pass objects or ID strings?
|
||||
if($database->deleteDocument($document->getCollection(), $document->getId())) {
|
||||
Console::success('Deleted document "'.$document->getId().'" successfully');
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"appwrite/php-clamav": "1.1.*",
|
||||
"appwrite/php-runtimes": "0.6.*",
|
||||
|
||||
"utopia-php/framework": "0.18.*",
|
||||
"utopia-php/framework": "0.19.*",
|
||||
"utopia-php/abuse": "0.6.*",
|
||||
"utopia-php/analytics": "0.2.*",
|
||||
"utopia-php/audit": "0.6.*",
|
||||
|
|
39
composer.lock
generated
39
composer.lock
generated
|
@ -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": "e55db0e0bb8929027e77cb9a9164a215",
|
||||
"content-hash": "fa378feaffc446f557a140035a1c77b6",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
|
@ -613,16 +613,16 @@
|
|||
},
|
||||
{
|
||||
"name": "guzzlehttp/promises",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/promises.git",
|
||||
"reference": "136a635e2b4a49b9d79e9c8fee267ffb257fdba0"
|
||||
"reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/136a635e2b4a49b9d79e9c8fee267ffb257fdba0",
|
||||
"reference": "136a635e2b4a49b9d79e9c8fee267ffb257fdba0",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
|
||||
"reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -677,7 +677,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/guzzle/promises/issues",
|
||||
"source": "https://github.com/guzzle/promises/tree/1.5.0"
|
||||
"source": "https://github.com/guzzle/promises/tree/1.5.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -693,7 +693,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2021-10-07T13:05:22+00:00"
|
||||
"time": "2021-10-22T20:56:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/psr7",
|
||||
|
@ -2255,16 +2255,16 @@
|
|||
},
|
||||
{
|
||||
"name": "utopia-php/framework",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/framework.git",
|
||||
"reference": "f577522a5eb8009967b893fb7ad4ee70d3f7c0db"
|
||||
"reference": "c86fc078ef258f3c88d3a25233202267314df3a9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/framework/zipball/f577522a5eb8009967b893fb7ad4ee70d3f7c0db",
|
||||
"reference": "f577522a5eb8009967b893fb7ad4ee70d3f7c0db",
|
||||
"url": "https://api.github.com/repos/utopia-php/framework/zipball/c86fc078ef258f3c88d3a25233202267314df3a9",
|
||||
"reference": "c86fc078ef258f3c88d3a25233202267314df3a9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2298,9 +2298,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/framework/issues",
|
||||
"source": "https://github.com/utopia-php/framework/tree/0.18.0"
|
||||
"source": "https://github.com/utopia-php/framework/tree/0.19.0"
|
||||
},
|
||||
"time": "2021-08-19T04:58:47+00:00"
|
||||
"time": "2021-10-08T11:46:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/image",
|
||||
|
@ -3064,16 +3064,16 @@
|
|||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
"version": "3.2.5",
|
||||
"version": "3.2.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/semver.git",
|
||||
"reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9"
|
||||
"reference": "83e511e247de329283478496f7a1e114c9517506"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/31f3ea725711245195f62e54ffa402d8ef2fdba9",
|
||||
"reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/83e511e247de329283478496f7a1e114c9517506",
|
||||
"reference": "83e511e247de329283478496f7a1e114c9517506",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -3125,7 +3125,7 @@
|
|||
"support": {
|
||||
"irc": "irc://irc.freenode.org/composer",
|
||||
"issues": "https://github.com/composer/semver/issues",
|
||||
"source": "https://github.com/composer/semver/tree/3.2.5"
|
||||
"source": "https://github.com/composer/semver/tree/3.2.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -3141,7 +3141,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2021-05-24T12:41:47+00:00"
|
||||
"time": "2021-10-25T11:34:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/xdebug-handler",
|
||||
|
@ -5423,7 +5423,6 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"abandoned": true,
|
||||
"time": "2020-09-28T06:45:17+00:00"
|
||||
},
|
||||
{
|
||||
|
|
1
docs/references/database/create-boolean-attribute.md
Normal file
1
docs/references/database/create-boolean-attribute.md
Normal file
|
@ -0,0 +1 @@
|
|||
Create a boolean attribute.
|
1
docs/references/database/create-email-attribute.md
Normal file
1
docs/references/database/create-email-attribute.md
Normal file
|
@ -0,0 +1 @@
|
|||
Create an email attribute.
|
1
docs/references/database/create-float-attribute.md
Normal file
1
docs/references/database/create-float-attribute.md
Normal file
|
@ -0,0 +1 @@
|
|||
Create a float attribute. Optionally, minimum and maximum values can be provided.
|
1
docs/references/database/create-integer-attribute.md
Normal file
1
docs/references/database/create-integer-attribute.md
Normal file
|
@ -0,0 +1 @@
|
|||
Create an integer attribute. Optionally, minimum and maximum values can be provided.
|
1
docs/references/database/create-ip-attribute.md
Normal file
1
docs/references/database/create-ip-attribute.md
Normal file
|
@ -0,0 +1 @@
|
|||
Create IP address attribute.
|
1
docs/references/database/create-string-attribute.md
Normal file
1
docs/references/database/create-string-attribute.md
Normal file
|
@ -0,0 +1 @@
|
|||
Create a new string attribute.
|
1
docs/references/database/create-url-attribute.md
Normal file
1
docs/references/database/create-url-attribute.md
Normal file
|
@ -0,0 +1 @@
|
|||
Create a URL attribute.
|
|
@ -24,7 +24,7 @@ class Attribute extends Model
|
|||
])
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Attribute status. Possible values: `available`, `processing`, `deleting`, or `failed`',
|
||||
'description' => 'Attribute status. Possible values: `available`, `processing`, `deleting`, `stuck`, or `failed`',
|
||||
'default' => '',
|
||||
'example' => 'available',
|
||||
])
|
||||
|
|
|
@ -24,7 +24,7 @@ class Index extends Model
|
|||
])
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Index status. Possible values: `available`, `processing`, `deleting`, or `failed`',
|
||||
'description' => 'Index status. Possible values: `available`, `processing`, `deleting`, `stuck`, or `failed`',
|
||||
'default' => '',
|
||||
'example' => 'available',
|
||||
])
|
||||
|
|
|
@ -1293,17 +1293,16 @@ trait DatabaseBase
|
|||
'max' => 1.4,
|
||||
]);
|
||||
|
||||
// TODO@kodumbeats float validator rejects 0.0 and 1.0 as floats
|
||||
// $probability = $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' => 'probability',
|
||||
// 'required' => false,
|
||||
// 'min' => \floatval(0.0),
|
||||
// 'max' => \floatval(1.0),
|
||||
// ]);
|
||||
$probability = $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' => 'probability',
|
||||
'required' => false,
|
||||
'min' => 0,
|
||||
'max' => 1,
|
||||
]);
|
||||
|
||||
$upperBound = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
|
@ -1329,26 +1328,38 @@ trait DatabaseBase
|
|||
* Test for failure
|
||||
*/
|
||||
|
||||
// TODO@kodumbeats troubleshoot
|
||||
// $invalidRange = $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' => 'invalidRange',
|
||||
// 'required' => false,
|
||||
// 'min' => 4,
|
||||
// 'max' => 3,
|
||||
// ]);
|
||||
$invalidRange = $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' => 'invalidRange',
|
||||
'required' => false,
|
||||
'min' => 4,
|
||||
'max' => 3,
|
||||
]);
|
||||
|
||||
$defaultArray = $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' => 'defaultArray',
|
||||
'required' => false,
|
||||
'default' => 42,
|
||||
'array' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $email['headers']['status-code']);
|
||||
$this->assertEquals(201, $ip['headers']['status-code']);
|
||||
$this->assertEquals(201, $url['headers']['status-code']);
|
||||
$this->assertEquals(201, $range['headers']['status-code']);
|
||||
$this->assertEquals(201, $floatRange['headers']['status-code']);
|
||||
$this->assertEquals(201, $probability['headers']['status-code']);
|
||||
$this->assertEquals(201, $upperBound['headers']['status-code']);
|
||||
$this->assertEquals(201, $lowerBound['headers']['status-code']);
|
||||
// $this->assertEquals(400, $invalidRange['headers']['status-code']);
|
||||
// $this->assertEquals('Minimum value must be lesser than maximum value', $invalidRange['body']['message']);
|
||||
$this->assertEquals(400, $invalidRange['headers']['status-code']);
|
||||
$this->assertEquals(400, $defaultArray['headers']['status-code']);
|
||||
$this->assertEquals('Minimum value must be lesser than maximum value', $invalidRange['body']['message']);
|
||||
$this->assertEquals('Cannot set default value for array attributes', $defaultArray['body']['message']);
|
||||
|
||||
// wait for worker to add attributes
|
||||
sleep(3);
|
||||
|
@ -1359,7 +1370,7 @@ trait DatabaseBase
|
|||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), []);
|
||||
|
||||
$this->assertCount(8, $collection['body']['attributes']);
|
||||
$this->assertCount(9, $collection['body']['attributes']);
|
||||
|
||||
/**
|
||||
* Test for successful validation
|
||||
|
@ -1437,6 +1448,18 @@ trait DatabaseBase
|
|||
'write' => ['user:'.$this->getUser()['$id']],
|
||||
]);
|
||||
|
||||
$goodProbability = $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' => [
|
||||
'probability' => 0.99999,
|
||||
],
|
||||
'read' => ['user:'.$this->getUser()['$id']],
|
||||
'write' => ['user:'.$this->getUser()['$id']],
|
||||
]);
|
||||
|
||||
$notTooHigh = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -1467,6 +1490,7 @@ trait DatabaseBase
|
|||
$this->assertEquals(201, $goodUrl['headers']['status-code']);
|
||||
$this->assertEquals(201, $goodRange['headers']['status-code']);
|
||||
$this->assertEquals(201, $goodFloatRange['headers']['status-code']);
|
||||
$this->assertEquals(201, $goodProbability['headers']['status-code']);
|
||||
$this->assertEquals(201, $notTooHigh['headers']['status-code']);
|
||||
$this->assertEquals(201, $notTooLow['headers']['status-code']);
|
||||
|
||||
|
@ -1546,6 +1570,18 @@ trait DatabaseBase
|
|||
'write' => ['user:'.$this->getUser()['$id']],
|
||||
]);
|
||||
|
||||
$badProbability = $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' => [
|
||||
'probability' => 1.1,
|
||||
],
|
||||
'read' => ['user:'.$this->getUser()['$id']],
|
||||
'write' => ['user:'.$this->getUser()['$id']],
|
||||
]);
|
||||
|
||||
$tooHigh = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
|
@ -1576,6 +1612,7 @@ trait DatabaseBase
|
|||
$this->assertEquals(400, $badUrl['headers']['status-code']);
|
||||
$this->assertEquals(400, $badRange['headers']['status-code']);
|
||||
$this->assertEquals(400, $badFloatRange['headers']['status-code']);
|
||||
$this->assertEquals(400, $badProbability['headers']['status-code']);
|
||||
$this->assertEquals(400, $tooHigh['headers']['status-code']);
|
||||
$this->assertEquals(400, $tooLow['headers']['status-code']);
|
||||
$this->assertEquals('Invalid document structure: Attribute "email" has invalid format. Value must be a valid email address', $badEmail['body']['message']);
|
||||
|
@ -1584,6 +1621,7 @@ trait DatabaseBase
|
|||
$this->assertEquals('Invalid document structure: Attribute "url" has invalid format. Value must be a valid URL', $badUrl['body']['message']);
|
||||
$this->assertEquals('Invalid document structure: Attribute "range" has invalid format. Value must be a valid range between 1 and 10', $badRange['body']['message']);
|
||||
$this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 1', $badFloatRange['body']['message']);
|
||||
$this->assertEquals('Invalid document structure: Attribute "probability" has invalid format. Value must be a valid range between 0 and 1', $badProbability['body']['message']);
|
||||
$this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between -9,223,372,036,854,775,808 and 10', $tooHigh['body']['message']);
|
||||
$this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,808', $tooLow['body']['message']);
|
||||
}
|
||||
|
|
|
@ -558,50 +558,59 @@ 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',
|
||||
]);
|
||||
// Adds several minutes to test to replicate coverage in Utopia\Database unit tests
|
||||
// and messes with subsequent tests as DatabaseV1 queue gets overwhelmed
|
||||
// TODO@kodumbeats either fix or remove testAttributeCountLimit
|
||||
// Options to fix:
|
||||
// - Enable attribute creation in batches
|
||||
// - Use additional database workers
|
||||
// - Wait for worker to complete before moving onto next test
|
||||
// - Remove since this is unit tested in Utopia\Database
|
||||
//
|
||||
// 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'];
|
||||
// $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,
|
||||
]);
|
||||
// // 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']);
|
||||
}
|
||||
// $this->assertEquals(201, $attribute['headers']['status-code']);
|
||||
// }
|
||||
|
||||
sleep(30);
|
||||
// sleep(30);
|
||||
|
||||
$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,
|
||||
]);
|
||||
// $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']);
|
||||
}
|
||||
// $this->assertEquals(400, $tooMany['headers']['status-code']);
|
||||
// $this->assertEquals('Attribute limit exceeded', $tooMany['body']['message']);
|
||||
// }
|
||||
|
||||
public function testAttributeRowWidthLimit()
|
||||
{
|
||||
|
@ -688,7 +697,7 @@ class DatabaseCustomServerTest extends Scope
|
|||
$this->assertEquals($attribute['headers']['status-code'], 201);
|
||||
}
|
||||
|
||||
sleep(5);
|
||||
sleep(20);
|
||||
|
||||
$collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
|
@ -703,6 +712,10 @@ class DatabaseCustomServerTest extends Scope
|
|||
$this->assertCount(64, $collection['body']['attributes']);
|
||||
$this->assertCount(0, $collection['body']['indexes']);
|
||||
|
||||
foreach ($collection['body']['attributes'] as $attribute) {
|
||||
$this->assertEquals('available', $attribute['status'], 'attribute: ' . $attribute['key']);
|
||||
}
|
||||
|
||||
// testing for indexLimit = 64
|
||||
// MariaDB, MySQL, and MongoDB create 3 indexes per new collection
|
||||
// Add up to the limit, then check if the next index throws IndexLimitException
|
||||
|
|
80
tests/e2e/Services/Database/DatabasePermissionsGuestTest.php
Normal file
80
tests/e2e/Services/Database/DatabasePermissionsGuestTest.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Database;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
|
||||
class DatabasePermissionsGuestTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
use DatabasePermissionsScope;
|
||||
|
||||
public function createCollection(): array
|
||||
{
|
||||
$movies = $this->client->call(Client::METHOD_POST, '/database/collections', $this->getServerHeader(), [
|
||||
'collectionId' => 'unique()',
|
||||
'name' => 'Movies',
|
||||
'read' => ['role:all'],
|
||||
'write' => ['role:all'],
|
||||
'permission' => 'document',
|
||||
]);
|
||||
|
||||
$collection = ['id' => $movies['body']['$id']];
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/database/collections/' . $collection['id'] . '/attributes/string', $this->getServerHeader(), [
|
||||
'attributeId' => 'title',
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* [string[] $read, string[] $write]
|
||||
*/
|
||||
public function readDocumentsProvider()
|
||||
{
|
||||
return [
|
||||
[['role:all'], []],
|
||||
[['role:member'], []],
|
||||
[[] ,['role:all']],
|
||||
[['role:all'], ['role:all']],
|
||||
[['role:member'], ['role:member']],
|
||||
[['role:all'], ['role:member']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider readDocumentsProvider
|
||||
*/
|
||||
public function testReadDocuments($read, $write)
|
||||
{
|
||||
$collection = $this->createCollection();
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collection['id'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => 'unique()',
|
||||
'data' => [
|
||||
'title' => 'Lorem',
|
||||
],
|
||||
'read' => $read,
|
||||
'write' => $write,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collection['id'] . '/documents', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]);
|
||||
|
||||
foreach ($documents['body']['documents'] as $document) {
|
||||
$this->assertContains('role:all', $document['$read']);
|
||||
}
|
||||
}
|
||||
}
|
165
tests/e2e/Services/Database/DatabasePermissionsMemberTest.php
Normal file
165
tests/e2e/Services/Database/DatabasePermissionsMemberTest.php
Normal file
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Database;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
|
||||
class DatabasePermissionsMemberTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
use DatabasePermissionsScope;
|
||||
|
||||
public array $collections = [];
|
||||
|
||||
public function createUsers(): array
|
||||
{
|
||||
return [
|
||||
'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
|
||||
'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* [string[] $read, string[] $write]
|
||||
*/
|
||||
public function readDocumentsProvider()
|
||||
{
|
||||
return [
|
||||
[['role:all'], []],
|
||||
[['role:member'], []],
|
||||
[['user:random'], []],
|
||||
[['user:lorem'] ,['user:lorem']],
|
||||
[['user:dolor'] ,['user:dolor']],
|
||||
[['user:dolor', 'user:lorem'] ,['user:dolor']],
|
||||
[[], ['role:all']],
|
||||
[['role:all'], ['role:all']],
|
||||
[['role:member'], ['role:member']],
|
||||
[['role:all'], ['role:member']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup database
|
||||
*
|
||||
* Data providers lose object state
|
||||
* so explicitly pass [$users, $collections] to each iteration
|
||||
* @return array
|
||||
*/
|
||||
public function testSetupDatabase(): array
|
||||
{
|
||||
$this->createUsers();
|
||||
|
||||
$public = $this->client->call(Client::METHOD_POST, '/database/collections', $this->getServerHeader(), [
|
||||
'collectionId' => 'unique()',
|
||||
'name' => 'Movies',
|
||||
'read' => ['role:all'],
|
||||
'write' => ['role:all'],
|
||||
'permission' => 'document',
|
||||
]);
|
||||
$this->assertEquals(201, $public['headers']['status-code']);
|
||||
|
||||
$this->collections = ['public' => $public['body']['$id']];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/database/collections/' . $this->collections['public'] . '/attributes/string', $this->getServerHeader(), [
|
||||
'attributeId' => 'title',
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$private = $this->client->call(Client::METHOD_POST, '/database/collections', $this->getServerHeader(), [
|
||||
'collectionId' => 'unique()',
|
||||
'name' => 'Private Movies',
|
||||
'read' => ['role:member'],
|
||||
'write' => ['role:member'],
|
||||
'permission' => 'document',
|
||||
]);
|
||||
$this->assertEquals(201, $private['headers']['status-code']);
|
||||
|
||||
$this->collections['private'] = $private['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/database/collections/' . $this->collections['private'] . '/attributes/string', $this->getServerHeader(), [
|
||||
'attributeId' => 'title',
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
sleep(2);
|
||||
|
||||
return [
|
||||
'users' => $this->users,
|
||||
'collections' => $this->collections
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider params are passed before test dependencies
|
||||
* @dataProvider readDocumentsProvider
|
||||
* @depends testSetupDatabase
|
||||
*/
|
||||
public function testReadDocuments($read, $write, $data)
|
||||
{
|
||||
$users = $data['users'];
|
||||
$collections = $data['collections'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collections['public'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => 'unique()',
|
||||
'data' => [
|
||||
'title' => 'Lorem',
|
||||
],
|
||||
'read' => $read,
|
||||
'write' => $write,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collections['private'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => 'unique()',
|
||||
'data' => [
|
||||
'title' => 'Lorem',
|
||||
],
|
||||
'read' => $read,
|
||||
'write' => $write,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Check role:all collection
|
||||
*/
|
||||
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collections['public'] . '/documents', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
|
||||
]);
|
||||
|
||||
foreach ($documents['body']['documents'] as $document) {
|
||||
$hasPermissions = \array_reduce(['role:all', 'role:member', 'user:' . $users['user1']['$id']], function ($carry, $item) use ($document) {
|
||||
return $carry ? true : \in_array($item, $document['$read']);
|
||||
}, false);
|
||||
$this->assertTrue($hasPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check role:member collection
|
||||
*/
|
||||
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collections['private'] . '/documents', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
|
||||
]);
|
||||
|
||||
foreach ($documents['body']['documents'] as $document) {
|
||||
$hasPermissions = \array_reduce(['role:all', 'role:member', 'user:' . $users['user1']['$id']], function ($carry, $item) use ($document) {
|
||||
return $carry ? true : \in_array($item, $document['$read']);
|
||||
}, false);
|
||||
$this->assertTrue($hasPermissions);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
86
tests/e2e/Services/Database/DatabasePermissionsScope.php
Normal file
86
tests/e2e/Services/Database/DatabasePermissionsScope.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Database;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
|
||||
trait DatabasePermissionsScope
|
||||
{
|
||||
public array $users = [];
|
||||
public array $teams = [];
|
||||
|
||||
public function createUser(string $id, string $email, string $password = 'test123'): array
|
||||
{
|
||||
$user = $this->client->call(Client::METHOD_POST, '/account', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'userId' => $id,
|
||||
'email' => $email,
|
||||
'password' => $password
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $user['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
|
||||
|
||||
$user = [
|
||||
'$id' => $user['body']['$id'],
|
||||
'email' => $user['body']['email'],
|
||||
'session' => $session,
|
||||
];
|
||||
$this->users[$id] = $user;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function getCreatedUser(string $id): array
|
||||
{
|
||||
return $this->users[$id] ?? [];
|
||||
}
|
||||
|
||||
public function createTeam(string $id, string $name): array
|
||||
{
|
||||
$team = $this->client->call(Client::METHOD_POST, '/teams', $this->getServerHeader(), [
|
||||
'teamId' => $id,
|
||||
'name' => $name
|
||||
]);
|
||||
$this->teams[$id] = $team['body'];
|
||||
|
||||
return $team['body'];
|
||||
}
|
||||
|
||||
public function addToTeam(string $user, string $team, array $roles = []): array
|
||||
{
|
||||
$membership = $this->client->call(Client::METHOD_POST, '/teams/' . $team . '/memberships', $this->getServerHeader(), [
|
||||
'teamId' => $team,
|
||||
'email' => $this->getCreatedUser($user)['email'],
|
||||
'roles' => $roles,
|
||||
'url' => 'http://localhost:5000/join-us#title'
|
||||
]);
|
||||
|
||||
return [
|
||||
'user' => $membership['body']['userId'],
|
||||
'membership' => $membership['body']['$id']
|
||||
];
|
||||
}
|
||||
|
||||
public function getServerHeader(): array
|
||||
{
|
||||
return [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
];
|
||||
}
|
||||
}
|
192
tests/e2e/Services/Database/DatabasePermissionsTeamTest.php
Normal file
192
tests/e2e/Services/Database/DatabasePermissionsTeamTest.php
Normal file
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Database;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
|
||||
class DatabasePermissionsTeamTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
use DatabasePermissionsScope;
|
||||
|
||||
public array $collections = [];
|
||||
|
||||
public function createTeams(): array
|
||||
{
|
||||
return [
|
||||
'team1' => $this->createTeam('team1', 'Team 1'),
|
||||
'team2' => $this->createTeam('team2', 'Team 2'),
|
||||
];
|
||||
}
|
||||
|
||||
public function createUsers(): array
|
||||
{
|
||||
return [
|
||||
'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
|
||||
'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
|
||||
'user3' => $this->createUser('user3', 'sit@ipsum.com'),
|
||||
];
|
||||
}
|
||||
|
||||
public function createCollections($teams)
|
||||
{
|
||||
$collection1 = $this->client->call(Client::METHOD_POST, '/database/collections', $this->getServerHeader(), [
|
||||
'collectionId' => 'collection1',
|
||||
'name' => 'Collection 1',
|
||||
'read' => ['team:' . $teams['team1']['$id']],
|
||||
'write' => ['team:' . $teams['team1']['$id'] . '/admin'],
|
||||
'permission' => 'collection',
|
||||
]);
|
||||
|
||||
$this->collections['collection1'] = $collection1['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/database/collections/' . $this->collections['collection1'] . '/attributes/string', $this->getServerHeader(), [
|
||||
'attributeId' => 'title',
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$collection2 = $this->client->call(Client::METHOD_POST, '/database/collections', $this->getServerHeader(), [
|
||||
'collectionId' => 'collection2',
|
||||
'name' => 'Collection 2',
|
||||
'read' => ['team:' . $teams['team2']['$id']],
|
||||
'write' => ['team:' . $teams['team2']['$id'] . '/owner'],
|
||||
'permission' => 'collection',
|
||||
]);
|
||||
|
||||
$this->collections['collection2'] = $collection2['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/database/collections/' . $this->collections['collection2'] . '/attributes/string', $this->getServerHeader(), [
|
||||
'attributeId' => 'title',
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
return $this->collections;
|
||||
}
|
||||
|
||||
/*
|
||||
* $success = can $user read from $collection
|
||||
* [$user, $collection, $success]
|
||||
*/
|
||||
public function readDocumentsProvider(): array
|
||||
{
|
||||
return [
|
||||
['user1', 'collection1', true],
|
||||
['user2', 'collection1', false],
|
||||
['user3', 'collection1', true],
|
||||
['user1', 'collection2', false],
|
||||
['user2', 'collection2', true],
|
||||
['user3', 'collection2', true],
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* $success = can $user write to $collection
|
||||
* [$user, $collection, $success]
|
||||
*/
|
||||
public function writeDocumentsProvider(): array
|
||||
{
|
||||
return [
|
||||
['user1', 'collection1', true],
|
||||
['user2', 'collection1', false],
|
||||
['user3', 'collection1', false],
|
||||
['user1', 'collection2', false],
|
||||
['user2', 'collection2', true],
|
||||
['user3', 'collection2', false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup database
|
||||
*
|
||||
* Data providers lose object state
|
||||
* so explicitly pass $users to each iteration
|
||||
* @return array $users
|
||||
*/
|
||||
public function testSetupDatabase(): array
|
||||
{
|
||||
$this->createUsers();
|
||||
$this->createTeams();
|
||||
|
||||
$this->addToTeam('user1', 'team1', ['admin']);
|
||||
$this->addToTeam('user2', 'team2', ['owner']);
|
||||
|
||||
// user3 in both teams but with no roles
|
||||
$this->addToTeam('user3', 'team1');
|
||||
$this->addToTeam('user3', 'team2');
|
||||
|
||||
$this->createCollections($this->teams);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/database/collections/' . $this->collections['collection1'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => 'unique()',
|
||||
'data' => [
|
||||
'title' => 'Lorem',
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/database/collections/' . $this->collections['collection2'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => 'unique()',
|
||||
'data' => [
|
||||
'title' => 'Ipsum',
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider params are passed before test dependencies
|
||||
* @depends testSetupDatabase
|
||||
* @dataProvider readDocumentsProvider
|
||||
*/
|
||||
public function testReadDocuments($user, $collection, $success, $users)
|
||||
{
|
||||
$documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collection . '/documents', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
|
||||
]);
|
||||
|
||||
if ($success) {
|
||||
$this->assertCount(1, $documents['body']['documents']);
|
||||
} else {
|
||||
$this->assertEquals(404, $documents['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testSetupDatabase
|
||||
* @dataProvider writeDocumentsProvider
|
||||
*/
|
||||
public function testWriteDocuments($user, $collection, $success, $users)
|
||||
{
|
||||
$documents = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collection . '/documents', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
|
||||
], [
|
||||
'documentId' => 'unique()',
|
||||
'data' => [
|
||||
'title' => 'Ipsum',
|
||||
],
|
||||
]);
|
||||
|
||||
if ($success) {
|
||||
$this->assertEquals(201, $documents['headers']['status-code']);
|
||||
} else {
|
||||
// 401 if user is a part of team, 404 otherwise
|
||||
$this->assertContains($documents['headers']['status-code'], [401, 404]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -72,10 +72,22 @@ trait WebhooksBase
|
|||
'required' => true,
|
||||
]);
|
||||
|
||||
$extra = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'attributeId' => 'extra',
|
||||
'size' => 64,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals($firstName['headers']['status-code'], 201);
|
||||
$this->assertEquals($firstName['body']['key'], 'firstName');
|
||||
$this->assertEquals($lastName['headers']['status-code'], 201);
|
||||
$this->assertEquals($lastName['body']['key'], 'lastName');
|
||||
$this->assertEquals($extra['headers']['status-code'], 201);
|
||||
$this->assertEquals($extra['body']['key'], 'extra');
|
||||
|
||||
// wait for database worker to kick in
|
||||
sleep(10);
|
||||
|
@ -90,9 +102,27 @@ trait WebhooksBase
|
|||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
|
||||
$this->assertNotEmpty($webhook['data']['key']);
|
||||
$this->assertEquals($webhook['data']['key'], 'lastName');
|
||||
$this->assertEquals($webhook['data']['key'], 'extra');
|
||||
|
||||
// TODO@kodumbeats test webhook for removing attribute
|
||||
$removed = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/attributes/' . $extra['body']['key'], array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]));
|
||||
|
||||
$this->assertEquals(204, $removed['headers']['status-code']);
|
||||
|
||||
$webhook = $this->getLastRequest();
|
||||
|
||||
// $this->assertEquals($webhook['method'], 'DELETE');
|
||||
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
|
||||
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'database.attributes.delete');
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
|
||||
$this->assertNotEmpty($webhook['data']['key']);
|
||||
$this->assertEquals($webhook['data']['key'], 'extra');
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
|
|
@ -87,27 +87,24 @@ class WebhooksCustomServerTest extends Scope
|
|||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
|
||||
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
|
||||
|
||||
// TODO@kodumbeats test for indexes.delete
|
||||
// Remove index
|
||||
// $index = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/indexes/' . $index['body']['$id'], array_merge([
|
||||
// 'content-type' => 'application/json',
|
||||
// 'x-appwrite-project' => $this->getProject()['$id'],
|
||||
// 'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
// ]));
|
||||
$this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/indexes/' . $index['body']['key'], array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]));
|
||||
|
||||
// // wait for database worker to remove index
|
||||
// sleep(5);
|
||||
$webhook = $this->getLastRequest();
|
||||
|
||||
// $webhook = $this->getLastRequest();
|
||||
|
||||
// // $this->assertEquals($webhook['method'], 'DELETE');
|
||||
// $this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
|
||||
// $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
|
||||
// $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'database.indexes.delete');
|
||||
// $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
|
||||
// $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
|
||||
// $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
|
||||
// $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
|
||||
// $this->assertEquals($webhook['method'], 'DELETE');
|
||||
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
|
||||
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'database.indexes.delete');
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented');
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
|
||||
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
|
||||
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue