1
0
Fork 0
mirror of synced 2024-07-06 07:00:56 +12:00

Merge branch '1.3.x' into doc-auth-1.3

# Conflicts:
#	composer.lock
This commit is contained in:
Jake Barnby 2023-04-11 21:22:32 +12:00
commit 2157acd63f
No known key found for this signature in database
GPG key ID: C437A8CC85B96E9C
60 changed files with 2439 additions and 396 deletions

2
.gitmodules vendored
View file

@ -1,4 +1,4 @@
[submodule "app/console"]
path = app/console
url = https://github.com/appwrite/console
branch = 2.2.0
branch = feat-databases-v2

View file

@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT
RUN npm ci
RUN npm run build
FROM appwrite/base:0.2.0 as final
FROM appwrite/base:0.2.2 as final
LABEL maintainer="team@appwrite.io"

View file

@ -346,6 +346,16 @@ $collections = [
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('options'),
'type' => Database::VAR_STRING,
'size' => 16384,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['json'],
],
],
'indexes' => [
[

View file

@ -413,6 +413,11 @@ return [
'description' => 'Remote document is newer than local.',
'code' => 409,
],
Exception::DOCUMENT_DELETE_RESTRICTED => [
'name' => Exception::DOCUMENT_DELETE_RESTRICTED,
'description' => 'Document cannot be deleted because it is referenced by another document.',
'code' => 403,
],
/** Attributes */
Exception::ATTRIBUTE_NOT_FOUND => [
@ -422,12 +427,12 @@ return [
],
Exception::ATTRIBUTE_UNKNOWN => [
'name' => Exception::ATTRIBUTE_UNKNOWN,
'description' => 'The attribute required for the index could not be found. Please confirm all your attributes are in the <span class="tag">available</span> state.',
'description' => 'The attribute required for the index could not be found. Please confirm all your attributes are in the available state.',
'code' => 400,
],
Exception::ATTRIBUTE_NOT_AVAILABLE => [
'name' => Exception::ATTRIBUTE_NOT_AVAILABLE,
'description' => 'The requested attribute is not yet <span class="tag">available</span>. Please try again later.',
'description' => 'The requested attribute is not yet available. Please try again later.',
'code' => 400,
],
Exception::ATTRIBUTE_FORMAT_UNSUPPORTED => [
@ -437,7 +442,7 @@ return [
],
Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED => [
'name' => Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED,
'description' => 'Default values cannot be set for <span class="tag">array</span> and <span class="tag">required</span> attributes.',
'description' => 'Default values cannot be set for array or required attributes.',
'code' => 400,
],
Exception::ATTRIBUTE_ALREADY_EXISTS => [
@ -455,6 +460,11 @@ return [
'description' => 'The attribute value is invalid. Please check the type, range and value of the attribute.',
'code' => 400,
],
Exception::ATTRIBUTE_TYPE_INVALID => [
'name' => Exception::ATTRIBUTE_TYPE_INVALID,
'description' => 'The attribute type is invalid.',
'code' => 400,
],
/** Indexes */
Exception::INDEX_NOT_FOUND => [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 5e2a40c1e397bd341a432698c9d76a3f96315841
Subproject commit 8d6b58467fe13635ba75bb62f2d94280f8b7ceb8

View file

@ -1411,7 +1411,7 @@ App::get('/v1/account/logs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', true)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('user')
->inject('locale')

File diff suppressed because it is too large Load diff

View file

@ -123,7 +123,7 @@ App::get('/v1/functions')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FUNCTION_LIST)
->param('queries', [], new Functions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Functions::ALLOWED_ATTRIBUTES), true)
->param('queries', [], new Functions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Functions::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
@ -789,7 +789,7 @@ App::get('/v1/functions/:functionId/deployments')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DEPLOYMENT_LIST)
->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Deployments(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('queries', [], new Deployments(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
@ -1221,7 +1221,7 @@ App::get('/v1/functions/:functionId/executions')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_EXECUTION_LIST)
->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Executions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Executions::ALLOWED_ATTRIBUTES), true)
->param('queries', [], new Executions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Executions::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')

View file

@ -184,7 +184,7 @@ App::get('/v1/projects')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT_LIST)
->param('queries', [], new Projects(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true)
->param('queries', [], new Projects(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForConsole')

View file

@ -154,7 +154,7 @@ App::get('/v1/storage/buckets')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_BUCKET_LIST)
->param('queries', [], new Buckets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Buckets::ALLOWED_ATTRIBUTES), true)
->param('queries', [], new Buckets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Buckets::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
@ -680,7 +680,7 @@ App::get('/v1/storage/buckets/:bucketId/files')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE_LIST)
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('queries', [], new Files(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Files::ALLOWED_ATTRIBUTES), true)
->param('queries', [], new Files(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Files::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')

View file

@ -135,7 +135,7 @@ App::get('/v1/teams')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM_LIST)
->label('sdk.offline.model', '/teams')
->param('queries', [], new Teams(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Teams::ALLOWED_ATTRIBUTES), true)
->param('queries', [], new Teams(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Teams::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
@ -607,7 +607,7 @@ App::get('/v1/teams/:teamId/memberships')
->label('sdk.response.model', Response::MODEL_MEMBERSHIP_LIST)
->label('sdk.offline.model', '/teams/{teamId}/memberships')
->param('teamId', '', new UID(), 'Team ID.')
->param('queries', [], new Memberships(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Memberships::ALLOWED_ATTRIBUTES), true)
->param('queries', [], new Memberships(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Memberships::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
@ -1002,7 +1002,7 @@ App::get('/v1/teams/:teamId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('teamId', '', new UID(), 'Team ID.')
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', true)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')

View file

@ -373,7 +373,7 @@ App::get('/v1/users')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER_LIST)
->param('queries', [], new Users(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Users::ALLOWED_ATTRIBUTES), true)
->param('queries', [], new Users(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Users::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
@ -557,7 +557,7 @@ App::get('/v1/users/:userId/logs')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', true)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')

View file

@ -295,7 +295,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$log->addExtra('line', $th->getLine());
$log->addExtra('trace', $th->getTraceAsString());
$log->addExtra('detailedTrace', $th->getTrace());
$log->addExtra('roles', Authorization::$roles);
$log->addExtra('roles', Authorization::getRoles());
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$log->setAction($action);

View file

@ -285,12 +285,23 @@ Database::addFilter(
return null;
},
function (mixed $value, Document $document, Database $database) {
return $database
->find('attributes', [
Query::equal('collectionInternalId', [$document->getInternalId()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForAttributes()),
]);
$attributes = $database->find('attributes', [
Query::equal('collectionInternalId', [$document->getInternalId()]),
Query::equal('databaseInternalId', [$document->getAttribute('databaseInternalId')]),
Query::limit($database->getLimitForAttributes()),
]);
foreach ($attributes as $attribute) {
if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) {
$options = $attribute->getAttribute('options');
foreach ($options as $key => $value) {
$attribute->setAttribute($key, $value);
}
$attribute->removeAttribute('options');
}
}
return $attributes;
}
);

View file

@ -1,10 +1,13 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Resque\Worker;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
require_once __DIR__ . '/../init.php';
@ -89,16 +92,51 @@ class DatabaseV1 extends Worker
$format = $attribute->getAttribute('format', '');
$formatOptions = $attribute->getAttribute('formatOptions', []);
$filters = $attribute->getAttribute('filters', []);
$options = $attribute->getAttribute('options', []);
$project = $dbForConsole->getDocument('projects', $projectId);
try {
if (!$dbForProject->createAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters)) {
throw new Exception('Failed to create Attribute');
switch ($type) {
case Database::VAR_RELATIONSHIP:
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new Exception('Collection not found');
}
if (
!$dbForProject->createRelationship(
collection: 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(),
relatedCollection: 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(),
type: $options['relationType'],
twoWay: $options['twoWay'],
id: $key,
twoWayKey: $options['twoWayKey'],
onDelete: $options['onDelete'],
)
) {
throw new Exception('Failed to create Attribute');
}
if ($options['twoWay']) {
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'available'));
}
break;
default:
if (!$dbForProject->createAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters)) {
throw new Exception('Failed to create Attribute');
}
}
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available'));
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) {
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'failed'));
}
} finally {
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
@ -121,6 +159,10 @@ class DatabaseV1 extends Worker
);
}
if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) {
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId());
}
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId);
}
@ -129,6 +171,7 @@ class DatabaseV1 extends Worker
* @param Document $collection
* @param Document $attribute
* @param string $projectId
* @throws Throwable
*/
protected function deleteAttribute(Document $database, Document $collection, Document $attribute, string $projectId): void
{
@ -143,19 +186,43 @@ class DatabaseV1 extends Worker
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$status = $attribute->getAttribute('status', '');
$type = $attribute->getAttribute('type', '');
$project = $dbForConsole->getDocument('projects', $projectId);
$options = $attribute->getAttribute('options', []);
$relatedAttribute = new Document();
$relatedCollection = new Document();
// possible states at this point:
// - available: should not land in queue; controller flips these to 'deleting'
// - processing: hasn't finished creating
// - deleting: was available, in deletion queue for first time
// - failed: attribute was never created
// - stuck: attribute was available but cannot be removed
try {
if ($status !== 'failed' && !$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete Attribute');
if ($status !== 'failed') {
if ($type === Database::VAR_RELATIONSHIP) {
if ($options['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']);
}
if (!$dbForProject->deleteRelationship('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'stuck'));
throw new Exception('Failed to delete Relationship');
}
} elseif (!$dbForProject->deleteAttribute('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $key)) {
throw new Exception('Failed to delete Attribute');
}
}
$dbForProject->deleteDocument('attributes', $attribute->getId());
if (!$relatedAttribute->isEmpty()) {
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
}
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'stuck'));
@ -199,8 +266,8 @@ class DatabaseV1 extends Worker
// 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]]));
$lengths = \array_values(\array_diff($lengths, isset($lengths[$found]) ? [$lengths[$found]] : []));
$orders = \array_values(\array_diff($orders, isset($orders[$found]) ? [$orders[$found]] : []));
if (empty($attributes)) {
$dbForProject->deleteDocument('indexes', $index->getId());
@ -235,6 +302,11 @@ class DatabaseV1 extends Worker
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $collectionId);
$dbForProject->deleteCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId());
if (!$relatedCollection->isEmpty() && !$relatedAttribute->isEmpty()) {
$dbForProject->deleteCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId());
$dbForProject->deleteCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId());
}
}
/**

View file

@ -203,6 +203,21 @@ class DeletesV1 extends Worker
$dbForProject = $this->getProjectDB($projectId);
$relationships = \array_filter(
$document->getAttribute('attributes'),
fn ($attribute) => $attribute['type'] === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
if (!$relationship['twoWay']) {
continue;
}
$relatedCollection = $dbForProject->getDocument('database_' . $databaseInternalId, $relationship['relatedCollection']);
$dbForProject->deleteDocument('attributes', $databaseInternalId . '_' . $relatedCollection->getInternalId() . '_' . $relationship['twoWayKey']);
$dbForProject->deleteCachedDocument('database_' . $databaseInternalId, $relatedCollection->getId());
$dbForProject->deleteCachedCollection('database_' . $databaseInternalId . '_collection_' . $relatedCollection->getInternalId());
}
$dbForProject->deleteCollection('database_' . $databaseInternalId . '_collection_' . $document->getInternalId());
$this->deleteByGroup('attributes', [

View file

@ -43,13 +43,13 @@
"ext-sockets": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-runtimes": "0.11.*",
"utopia-php/abuse": "0.23.*",
"utopia-php/abuse": "0.24.*",
"utopia-php/analytics": "0.2.*",
"utopia-php/audit": "0.24.*",
"utopia-php/audit": "0.25.*",
"utopia-php/cache": "0.8.*",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.34.*",
"utopia-php/database": "0.35.*",
"utopia-php/domains": "1.1.*",
"utopia-php/framework": "0.28.*",
"utopia-php/image": "0.5.*",
@ -63,7 +63,7 @@
"utopia-php/swoole": "0.5.*",
"utopia-php/websocket": "0.1.*",
"resque/php-resque": "1.3.6",
"matomo/device-detector": "6.0.0",
"matomo/device-detector": "6.0.*",
"dragonmantank/cron-expression": "3.3.1",
"influxdb/influxdb-php": "1.15.2",
"phpmailer/phpmailer": "6.6.0",

108
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": "3b088b0f09093a6f094f65f73b96bf91",
"content-hash": "164a9bd37203b233d663d709394feb07",
"packages": [
{
"name": "adhocore/jwt",
@ -1000,16 +1000,16 @@
},
{
"name": "matomo/device-detector",
"version": "6.0.0",
"version": "6.0.6",
"source": {
"type": "git",
"url": "https://github.com/matomo-org/device-detector.git",
"reference": "7fc2af3af62bd69e6e3404d561e371a83c112be9"
"reference": "ce5ef5e6776c16af306d38e20674973f072e05ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/7fc2af3af62bd69e6e3404d561e371a83c112be9",
"reference": "7fc2af3af62bd69e6e3404d561e371a83c112be9",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/ce5ef5e6776c16af306d38e20674973f072e05ed",
"reference": "ce5ef5e6776c16af306d38e20674973f072e05ed",
"shasum": ""
},
"require": {
@ -1065,7 +1065,7 @@
"source": "https://github.com/matomo-org/matomo",
"wiki": "https://dev.matomo.org/"
},
"time": "2022-04-11T09:58:17+00:00"
"time": "2023-01-16T08:18:02+00:00"
},
{
"name": "mongodb/mongodb",
@ -1372,25 +1372,25 @@
},
{
"name": "psr/http-message",
"version": "1.0.1",
"version": "1.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "1.1.x-dev"
}
},
"autoload": {
@ -1419,9 +1419,9 @@
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/master"
"source": "https://github.com/php-fig/http-message/tree/1.1"
},
"time": "2016-08-06T14:39:51+00:00"
"time": "2023-04-04T09:50:52+00:00"
},
{
"name": "psr/log",
@ -1808,23 +1808,23 @@
},
{
"name": "utopia-php/abuse",
"version": "0.23.0",
"version": "0.24.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "4319461f4dde080b9dffaceea048f9e4f8949a52"
"reference": "403641f16a53b81ac40b91111a86e5672da49e8c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/4319461f4dde080b9dffaceea048f9e4f8949a52",
"reference": "4319461f4dde080b9dffaceea048f9e4f8949a52",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/403641f16a53b81ac40b91111a86e5672da49e8c",
"reference": "403641f16a53b81ac40b91111a86e5672da49e8c",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.34.*"
"utopia-php/database": "0.35.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@ -1851,9 +1851,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.23.0"
"source": "https://github.com/utopia-php/abuse/tree/0.24.0"
},
"time": "2023-03-31T07:54:08+00:00"
"time": "2023-04-11T05:31:55+00:00"
},
{
"name": "utopia-php/analytics",
@ -1912,21 +1912,21 @@
},
{
"name": "utopia-php/audit",
"version": "0.24.1",
"version": "0.25.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "237538b618506bbc0efc0e7884d49cdf52c20004"
"reference": "adc209f2e16878e5468f0b9cfd9f7f7ab497db31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/237538b618506bbc0efc0e7884d49cdf52c20004",
"reference": "237538b618506bbc0efc0e7884d49cdf52c20004",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/adc209f2e16878e5468f0b9cfd9f7f7ab497db31",
"reference": "adc209f2e16878e5468f0b9cfd9f7f7ab497db31",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/database": "0.34.*"
"utopia-php/database": "0.35.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@ -1953,9 +1953,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.24.1"
"source": "https://github.com/utopia-php/audit/tree/0.25.0"
},
"time": "2023-03-23T07:29:11+00:00"
"time": "2023-04-11T05:31:15+00:00"
},
{
"name": "utopia-php/cache",
@ -2112,16 +2112,16 @@
},
{
"name": "utopia-php/database",
"version": "0.34.1",
"version": "0.35.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "b592ab1bab9d7412cf5b756fdcdf850c03b1470d"
"reference": "f162c142fd61753c4b413b15c3c4041f3cd00bb2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/b592ab1bab9d7412cf5b756fdcdf850c03b1470d",
"reference": "b592ab1bab9d7412cf5b756fdcdf850c03b1470d",
"url": "https://api.github.com/repos/utopia-php/database/zipball/f162c142fd61753c4b413b15c3c4041f3cd00bb2",
"reference": "f162c142fd61753c4b413b15c3c4041f3cd00bb2",
"shasum": ""
},
"require": {
@ -2129,7 +2129,7 @@
"php": ">=8.0",
"utopia-php/cache": "0.8.*",
"utopia-php/framework": "0.*.*",
"utopia-php/mongo": "0.1.*"
"utopia-php/mongo": "0.2.*"
},
"require-dev": {
"ext-mongodb": "*",
@ -2138,7 +2138,7 @@
"laravel/pint": "1.4.*",
"mongodb/mongodb": "1.8.0",
"pcov/clobber": "^2.0",
"phpstan/phpstan": "1.9.*",
"phpstan/phpstan": "1.10.*",
"phpunit/phpunit": "^9.4",
"rregeer/phpunit-coverage-check": "^0.3.1",
"swoole/ide-helper": "4.8.0",
@ -2164,9 +2164,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.34.1"
"source": "https://github.com/utopia-php/database/tree/0.35.0"
},
"time": "2023-03-23T07:23:41+00:00"
"time": "2023-04-11T04:02:22+00:00"
},
{
"name": "utopia-php/domains",
@ -2472,16 +2472,16 @@
},
{
"name": "utopia-php/mongo",
"version": "0.1.0",
"version": "0.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/mongo.git",
"reference": "f4b6ec74c5323ca16c500dd19109518d2eeb1f8f"
"reference": "b6dfb31b93c07c59b8bbd62a3b52e3b97a407c09"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/f4b6ec74c5323ca16c500dd19109518d2eeb1f8f",
"reference": "f4b6ec74c5323ca16c500dd19109518d2eeb1f8f",
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/b6dfb31b93c07c59b8bbd62a3b52e3b97a407c09",
"reference": "b6dfb31b93c07c59b8bbd62a3b52e3b97a407c09",
"shasum": ""
},
"require": {
@ -2526,9 +2526,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/mongo/issues",
"source": "https://github.com/utopia-php/mongo/tree/0.1.0"
"source": "https://github.com/utopia-php/mongo/tree/0.2.0"
},
"time": "2023-01-12T14:02:08+00:00"
"time": "2023-03-22T10:44:29+00:00"
},
{
"name": "utopia-php/orchestration",
@ -3661,16 +3661,16 @@
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.7.0",
"version": "1.7.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "1534aea9bde19a5c85c5d1e1f834ab63f4c5dcf5"
"reference": "dfc078e8af9c99210337325ff5aa152872c98714"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/1534aea9bde19a5c85c5d1e1f834ab63f4c5dcf5",
"reference": "1534aea9bde19a5c85c5d1e1f834ab63f4c5dcf5",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/dfc078e8af9c99210337325ff5aa152872c98714",
"reference": "dfc078e8af9c99210337325ff5aa152872c98714",
"shasum": ""
},
"require": {
@ -3713,9 +3713,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.7.0"
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.1"
},
"time": "2023-03-12T10:13:29+00:00"
"time": "2023-03-27T19:02:04+00:00"
},
{
"name": "phpspec/prophecy",
@ -3787,16 +3787,16 @@
},
{
"name": "phpstan/phpdoc-parser",
"version": "1.16.1",
"version": "1.18.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571"
"reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e27e92d939e2e3636f0a1f0afaba59692c0bf571",
"reference": "e27e92d939e2e3636f0a1f0afaba59692c0bf571",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/22dcdfd725ddf99583bfe398fc624ad6c5004a0f",
"reference": "22dcdfd725ddf99583bfe398fc624ad6c5004a0f",
"shasum": ""
},
"require": {
@ -3826,9 +3826,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.16.1"
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.18.1"
},
"time": "2023-02-07T18:11:17+00:00"
"time": "2023-04-07T11:51:11+00:00"
},
{
"name": "phpunit/php-code-coverage",

View file

@ -343,7 +343,6 @@ services:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
#- ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
depends_on:
- redis
- mariadb

View file

@ -0,0 +1 @@
Create relationship attribute. [Learn more about relationship attributes](docs/databases-relationships#relationship-attributes).

View file

@ -0,0 +1 @@
Update relationship attribute. [Learn more about relationship attributes](docs/databases-relationships#relationship-attributes).

View file

@ -157,7 +157,7 @@ As the name implies, `param()` is used to define a request parameter.
```php
App::get('/v1/account/logs')
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Only supported methods are limit and offset', true)
->param('queries', [], new Queries(new Limit(), new Offset()), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
```
### 6. inject

View file

@ -136,6 +136,7 @@ class Exception extends \Exception
public const DOCUMENT_MISSING_PAYLOAD = 'document_missing_payload';
public const DOCUMENT_ALREADY_EXISTS = 'document_already_exists';
public const DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict';
public const DOCUMENT_DELETE_RESTRICTED = 'document_delete_restricted';
/** Attribute */
public const ATTRIBUTE_NOT_FOUND = 'attribute_not_found';

View file

@ -408,6 +408,8 @@ class Mapper
return static::model('AttributeBoolean');
case 'datetime':
return static::model('AttributeDatetime');
case 'relationship':
return static::model('AttributeRelationship');
}
throw new Exception('Unknown attribute implementation');

View file

@ -10,26 +10,26 @@ use Utopia\Database\Query;
class IndexedQueries extends Queries
{
/**
* @var Document[]
* @var array<Document>
*/
protected $attributes = [];
protected array $attributes = [];
/**
* @var Document[]
* @var array<Document>
*/
protected $indexes = [];
protected array $indexes = [];
/**
* Expression constructor
*
* This Queries Validator filters indexes for only available indexes
*
* @param Document[] $attributes
* @param Document[] $indexes
* @param array<Document> $attributes
* @param array<Document> $indexes
* @param Base ...$validators
* @throws \Exception
*/
public function __construct($attributes = [], $indexes = [], Base ...$validators)
public function __construct(array $attributes = [], array $indexes = [], Base ...$validators)
{
$this->attributes = $attributes;
@ -55,33 +55,6 @@ class IndexedQueries extends Queries
parent::__construct(...$validators);
}
/**
* Check if indexed array $indexes matches $queries
*
* @param array $indexes
* @param array $queries
*
* @return bool
*/
protected function arrayMatch(array $indexes, array $queries): bool
{
// Check the count of indexes first for performance
if (count($queries) !== count($indexes)) {
return false;
}
// Sort them for comparison, the order is not important here anymore.
sort($indexes, SORT_STRING);
sort($queries, SORT_STRING);
// Only matching arrays will have equal diffs in both directions
if (array_diff_assoc($indexes, $queries) !== array_diff_assoc($queries, $indexes)) {
return false;
}
return true;
}
/**
* Is valid.
*
@ -111,41 +84,26 @@ class IndexedQueries extends Queries
}
$grouped = Query::groupByType($queries);
/** @var Query[] */ $filters = $grouped['filters'];
/** @var string[] */ $orderAttributes = $grouped['orderAttributes'];
$filters = $grouped['filters'];
// Check filter queries for exact index match
if (count($filters) > 0) {
$filtersByAttribute = [];
foreach ($filters as $filter) {
$filtersByAttribute[$filter->getAttribute()] = $filter->getMethod();
}
foreach ($filters as $filter) {
if ($filter->getMethod() === Query::TYPE_SEARCH) {
$matched = false;
$found = null;
foreach ($this->indexes as $index) {
if (
$index->getAttribute('type') === Database::INDEX_FULLTEXT
&& $index->getAttribute('attributes') === [$filter->getAttribute()]
) {
$matched = true;
}
}
foreach ($this->indexes as $index) {
if ($this->arrayMatch($index->getAttribute('attributes'), array_keys($filtersByAttribute))) {
$found = $index;
if (!$matched) {
$this->message = "Searching by attribute \"{$filter->getAttribute()}\" requires a fulltext index.";
return false;
}
}
if (!$found) {
$this->message = 'Index not found: ' . implode(",", array_keys($filtersByAttribute));
return false;
}
// search method requires fulltext index
if (in_array(Query::TYPE_SEARCH, array_values($filtersByAttribute)) && $found['type'] !== Database::INDEX_FULLTEXT) {
$this->message = 'Search method requires fulltext index: ' . implode(",", array_keys($filtersByAttribute));
return false;
}
}
// Check order attributes for exact index match
$validator = new OrderAttributes($this->attributes, $this->indexes, true);
if (count($orderAttributes) > 0 && !$validator->isValid($orderAttributes)) {
$this->message = $validator->getDescription();
return false;
}
return true;

View file

@ -11,12 +11,12 @@ class Queries extends Validator
/**
* @var string
*/
protected $message = 'Invalid queries';
protected string $message = 'Invalid queries';
/**
* @var Base[]
* @var array<Base>
*/
protected $validators;
protected array $validators;
/**
* Queries constructor
@ -57,41 +57,35 @@ class Queries extends Validator
if (!$query instanceof Query) {
try {
$query = Query::parse($query);
} catch (\Throwable $th) {
$this->message = 'Invalid query: ${query}';
} catch (\Throwable) {
$this->message = "Invalid query: {$query}";
return false;
}
}
$method = $query->getMethod();
$methodType = '';
switch ($method) {
case Query::TYPE_LIMIT:
$methodType = Base::METHOD_TYPE_LIMIT;
break;
case Query::TYPE_OFFSET:
$methodType = Base::METHOD_TYPE_OFFSET;
break;
case Query::TYPE_CURSORAFTER:
case Query::TYPE_CURSORBEFORE:
$methodType = Base::METHOD_TYPE_CURSOR;
break;
case Query::TYPE_ORDERASC:
case Query::TYPE_ORDERDESC:
$methodType = Base::METHOD_TYPE_ORDER;
break;
case Query::TYPE_EQUAL:
case Query::TYPE_NOTEQUAL:
case Query::TYPE_LESSER:
case Query::TYPE_LESSEREQUAL:
case Query::TYPE_GREATER:
case Query::TYPE_GREATEREQUAL:
case Query::TYPE_SEARCH:
$methodType = Base::METHOD_TYPE_FILTER;
break;
default:
break;
}
$methodType = match ($method) {
Query::TYPE_SELECT => Base::METHOD_TYPE_SELECT,
Query::TYPE_LIMIT => Base::METHOD_TYPE_LIMIT,
Query::TYPE_OFFSET => Base::METHOD_TYPE_OFFSET,
Query::TYPE_CURSORAFTER,
Query::TYPE_CURSORBEFORE => Base::METHOD_TYPE_CURSOR,
Query::TYPE_ORDERASC,
Query::TYPE_ORDERDESC => Base::METHOD_TYPE_ORDER,
Query::TYPE_EQUAL,
Query::TYPE_NOTEQUAL,
Query::TYPE_LESSER,
Query::TYPE_LESSEREQUAL,
Query::TYPE_GREATER,
Query::TYPE_GREATEREQUAL,
Query::TYPE_SEARCH,
Query::TYPE_IS_NULL,
Query::TYPE_IS_NOT_NULL,
Query::TYPE_BETWEEN,
Query::TYPE_STARTS_WITH,
Query::TYPE_ENDS_WITH => Base::METHOD_TYPE_FILTER,
default => '',
};
$methodIsValid = false;
foreach ($this->validators as $validator) {

View file

@ -8,6 +8,7 @@ use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@ -19,6 +20,7 @@ class Base extends Queries
*
* @param string $collection
* @param string[] $allowedAttributes
* @throws \Exception
*/
public function __construct(string $collection, array $allowedAttributes)
{
@ -65,6 +67,7 @@ class Base extends Queries
new Cursor(),
new Filter($attributes),
new Order($attributes),
new Select($attributes),
];
parent::__construct(...$validators);

View file

@ -0,0 +1,46 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Utopia\Database\Database;
class Document extends Queries
{
/**
* Expression constructor
*
* @param array $attributes
* @throws \Exception
*/
public function __construct(array $attributes)
{
$attributes[] = new \Utopia\Database\Document([
'key' => '$id',
'type' => Database::VAR_STRING,
'array' => false,
]);
$attributes[] = new \Utopia\Database\Document([
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$attributes[] = new \Utopia\Database\Document([
'key' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'array' => false,
]);
$validators = [
new Select($attributes),
];
parent::__construct(...$validators);
}
}

View file

@ -2,16 +2,17 @@
namespace Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\IndexedQueries;
use Appwrite\Utopia\Database\Validator\Query\Cursor;
use Appwrite\Utopia\Database\Validator\Query\Filter;
use Appwrite\Utopia\Database\Validator\Query\Limit;
use Appwrite\Utopia\Database\Validator\Query\Offset;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Utopia\Database\Database;
use Utopia\Database\Document;
class Documents extends Queries
class Documents extends IndexedQueries
{
/**
* Expression constructor
@ -19,7 +20,7 @@ class Documents extends Queries
* @param Document[] $attributes
* @throws \Exception
*/
public function __construct(array $attributes)
public function __construct(array $attributes, array $indexes)
{
$attributes[] = new Document([
'key' => '$id',
@ -43,8 +44,9 @@ class Documents extends Queries
new Cursor(),
new Filter($attributes),
new Order($attributes),
new Select($attributes),
];
parent::__construct(...$validators);
parent::__construct($attributes, $indexes, ...$validators);
}
}

View file

@ -12,6 +12,7 @@ abstract class Base extends Validator
public const METHOD_TYPE_CURSOR = 'cursor';
public const METHOD_TYPE_ORDER = 'order';
public const METHOD_TYPE_FILTER = 'filter';
public const METHOD_TYPE_SELECT = 'select';
/**
* @var string

View file

@ -18,6 +18,8 @@ class Filter extends Base
*/
protected $schema = [];
private int $maxValuesCount;
/**
* Query constructor
*
@ -34,6 +36,18 @@ class Filter extends Base
protected function isValidAttribute($attribute): bool
{
if (\str_contains($attribute, '.')) {
// For relationships, just validate the top level.
// Utopia will validate each nested level during the recursive calls.
$attribute = \explode('.', $attribute)[0];
// TODO: Remove this when nested queries are supported
if (isset($this->schema[$attribute])) {
$this->message = 'Cannot query nested attribute on: ' . $attribute;
return false;
}
}
// Search for attribute in schema
if (!isset($this->schema[$attribute])) {
$this->message = 'Attribute not found in schema: ' . $attribute;
@ -49,6 +63,12 @@ class Filter extends Base
return false;
}
if (\str_contains($attribute, '.')) {
// For relationships, just validate the top level.
// Utopia will validate each nested level during the recursive calls.
$attribute = \explode('.', $attribute)[0];
}
$attributeSchema = $this->schema[$attribute];
if (count($values) > $this->maxValuesCount) {
@ -61,7 +81,9 @@ class Filter extends Base
foreach ($values as $value) {
$condition = match ($attributeType) {
Database::VAR_RELATIONSHIP => true,
Database::VAR_DATETIME => gettype($value) === Database::VAR_STRING,
Database::VAR_FLOAT => (gettype($value) === Database::VAR_FLOAT || gettype($value) === Database::VAR_INTEGER),
default => gettype($value) === $attributeType
};
@ -99,6 +121,11 @@ class Filter extends Base
case Query::TYPE_GREATER:
case Query::TYPE_GREATEREQUAL:
case Query::TYPE_SEARCH:
case Query::TYPE_STARTS_WITH:
case Query::TYPE_ENDS_WITH:
case Query::TYPE_BETWEEN:
case Query::TYPE_IS_NULL:
case Query::TYPE_IS_NOT_NULL:
$values = $query->getValues();
return $this->isValidAttributeAndValues($attribute, $values);

View file

@ -0,0 +1,60 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Query;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
class Select extends Base
{
protected array $schema = [];
/**
* Query constructor
*
*/
public function __construct(array $attributes = [])
{
foreach ($attributes as $attribute) {
$this->schema[$attribute->getAttribute('key')] = $attribute->getArrayCopy();
}
}
/**
* Is valid.
*
* Returns true if method is TYPE_SELECT selections are valid
*
* Otherwise, returns false
*
* @param $query
* @return bool
*/
public function isValid($query): bool
{
/* @var $query Query */
if ($query->getMethod() !== Query::TYPE_SELECT) {
return false;
}
foreach ($query->getValues() as $attribute) {
if (\str_contains($attribute, '.')) {
// For relationships, just validate the top level.
// Utopia will validate each nested level during the recursive calls.
$attribute = \explode('.', $attribute)[0];
}
if (!isset($this->schema[$attribute]) && $attribute !== '*') {
$this->message = 'Attribute not found in schema: ' . $attribute;
return false;
}
}
return true;
}
public function getMethodType(): string
{
return self::METHOD_TYPE_SELECT;
}
}

View file

@ -30,6 +30,7 @@ use Appwrite\Utopia\Response\Model\AttributeEnum;
use Appwrite\Utopia\Response\Model\AttributeIP;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Database;
@ -132,6 +133,7 @@ class Response extends SwooleResponse
public const MODEL_ATTRIBUTE_IP = 'attributeIp';
public const MODEL_ATTRIBUTE_URL = 'attributeUrl';
public const MODEL_ATTRIBUTE_DATETIME = 'attributeDatetime';
public const MODEL_ATTRIBUTE_RELATIONSHIP = 'attributeRelationship';
// Users
public const MODEL_ACCOUNT = 'account';
@ -288,6 +290,7 @@ class Response extends SwooleResponse
->setModel(new AttributeIP())
->setModel(new AttributeURL())
->setModel(new AttributeDatetime())
->setModel(new AttributeRelationship())
->setModel(new Index())
->setModel(new ModelDocument())
->setModel(new Log())

View file

@ -13,6 +13,7 @@ abstract class Model
public const TYPE_JSON = 'json';
public const TYPE_DATETIME = 'datetime';
public const TYPE_DATETIME_EXAMPLE = '2020-10-15T06:38:00.000+00:00';
public const TYPE_RELATIONSHIP = 'relationship';
/**
* @var bool
@ -80,6 +81,7 @@ abstract class Model
*
* @param string $key
* @param array $options
* @return Model
*/
protected function addRule(string $key, array $options): self
{
@ -98,7 +100,7 @@ abstract class Model
* If rule exists, it will be removed
*
* @param string $key
* @param array $options
* @return Model
*/
protected function removeRule(string $key): self
{
@ -109,7 +111,10 @@ abstract class Model
return $this;
}
public function getRequired()
/**
* @return array
*/
public function getRequired(): array
{
$list = [];

View file

@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeBoolean extends Attribute
{

View file

@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeEmail extends Attribute
{

View file

@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeEnum extends Attribute
{

View file

@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeFloat extends Attribute
{

View file

@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeIP extends Attribute
{

View file

@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeInteger extends Attribute
{

View file

@ -4,7 +4,6 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Document;
class AttributeList extends Model
{
@ -27,6 +26,7 @@ class AttributeList extends Model
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_DATETIME,
Response::MODEL_ATTRIBUTE_RELATIONSHIP,
Response::MODEL_ATTRIBUTE_STRING // needs to be last, since its condition would dominate any other string attribute
],
'description' => 'List of attributes.',

View file

@ -0,0 +1,76 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class AttributeRelationship extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('relatedCollection', [
'type' => self::TYPE_STRING,
'description' => 'The ID of the related collection.',
'default' => null,
'example' => 'collection',
])
->addRule('relationType', [
'type' => self::TYPE_STRING,
'description' => 'The type of the relationship.',
'default' => '',
'example' => 'oneToOne|oneToMany|manyToOne|manyToMany',
])
->addRule('twoWay', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is the relationship two-way?',
'default' => false,
'example' => false,
])
->addRule('twoWayKey', [
'type' => self::TYPE_STRING,
'description' => 'The key of the two-way relationship.',
'default' => '',
'example' => 'string',
])
->addRule('onDelete', [
'type' => self::TYPE_STRING,
'description' => 'How deleting the parent document will propagate to child documents.',
'default' => 'restrict',
'example' => 'restrict|cascade|setNull',
])
->addRule('side', [
'type' => self::TYPE_STRING,
'description' => 'Whether this is the parent or child side of the relationship',
'default' => '',
'example' => 'parent|child',
])
;
}
public array $conditions = [
'type' => self::TYPE_RELATIONSHIP,
];
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AttributeRelationship';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ATTRIBUTE_RELATIONSHIP;
}
}

View file

@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeString extends Attribute
{

View file

@ -3,7 +3,6 @@
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model\Attribute;
class AttributeURL extends Attribute
{

View file

@ -69,6 +69,7 @@ class Collection extends Model
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_DATETIME,
Response::MODEL_ATTRIBUTE_RELATIONSHIP,
Response::MODEL_ATTRIBUTE_STRING, // needs to be last, since its condition would dominate any other string attribute
],
'description' => 'Collection attributes.',

View file

@ -74,6 +74,18 @@ class Document extends Any
$document->removeAttribute('$internalId');
$document->removeAttribute('$collection'); // $collection is the internal collection ID
foreach ($document->getAttributes() as $attribute) {
if (\is_array($attribute)) {
foreach ($attribute as $subAttribute) {
if ($subAttribute instanceof DatabaseDocument) {
$this->filter($subAttribute);
}
}
} elseif ($attribute instanceof DatabaseDocument) {
$this->filter($attribute);
}
}
return $document;
}
}

File diff suppressed because it is too large Load diff

View file

@ -53,7 +53,7 @@ class DatabasesCustomClientTest extends Scope
$this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
$this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/attributes/string', array_merge([
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
@ -65,6 +65,8 @@ class DatabasesCustomClientTest extends Scope
sleep(1);
$this->assertEquals(202, $response['headers']['status-code']);
// Document aliases write to update, delete
$document1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
'content-type' => 'application/json',

View file

@ -1165,7 +1165,7 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(202, $attribute['headers']['status-code']);
}
sleep(20);
sleep(10);
$collection = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
@ -1426,6 +1426,17 @@ class DatabasesCustomServerTest extends Scope
$this->assertFalse($new['body']['required']);
$this->assertEquals('lorem', $new['body']['default']);
$new = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$attribute = array_values(array_filter($new['body']['attributes'], fn (array $a) => $a['key'] === $key))[0] ?? null;
$this->assertNotNull($attribute);
$this->assertFalse($attribute['required']);
$this->assertEquals('lorem', $attribute['default']);
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string/' . $key, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -1557,6 +1568,17 @@ class DatabasesCustomServerTest extends Scope
$this->assertFalse($new['body']['required']);
$this->assertEquals('torsten@appwrite.io', $new['body']['default']);
$new = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$attribute = array_values(array_filter($new['body']['attributes'], fn (array $a) => $a['key'] === $key))[0] ?? null;
$this->assertNotNull($attribute);
$this->assertFalse($attribute['required']);
$this->assertEquals('torsten@appwrite.io', $attribute['default']);
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/email/' . $key, array_merge([
'content-type' => 'application/json',
@ -1689,6 +1711,17 @@ class DatabasesCustomServerTest extends Scope
$this->assertFalse($new['body']['required']);
$this->assertEquals('127.0.0.1', $new['body']['default']);
$new = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$attribute = array_values(array_filter($new['body']['attributes'], fn (array $a) => $a['key'] === $key))[0] ?? null;
$this->assertNotNull($attribute);
$this->assertFalse($attribute['required']);
$this->assertEquals('127.0.0.1', $attribute['default']);
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/ip/' . $key, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -1820,6 +1853,17 @@ class DatabasesCustomServerTest extends Scope
$this->assertFalse($new['body']['required']);
$this->assertEquals('http://appwrite.io', $new['body']['default']);
$new = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$attribute = array_values(array_filter($new['body']['attributes'], fn (array $a) => $a['key'] === $key))[0] ?? null;
$this->assertNotNull($attribute);
$this->assertFalse($attribute['required']);
$this->assertEquals('http://appwrite.io', $attribute['default']);
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/url/' . $key, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -1955,6 +1999,19 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(0, $new['body']['min']);
$this->assertEquals(1000, $new['body']['max']);
$new = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$attribute = array_values(array_filter($new['body']['attributes'], fn (array $a) => $a['key'] === $key))[0] ?? null;
$this->assertNotNull($attribute);
$this->assertFalse($attribute['required']);
$this->assertEquals(123, $attribute['default']);
$this->assertEquals(0, $attribute['min']);
$this->assertEquals(1000, $attribute['max']);
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer/' . $key, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -2205,6 +2262,19 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(0, $new['body']['min']);
$this->assertEquals(1000, $new['body']['max']);
$new = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$attribute = array_values(array_filter($new['body']['attributes'], fn (array $a) => $a['key'] === $key))[0] ?? null;
$this->assertNotNull($attribute);
$this->assertFalse($attribute['required']);
$this->assertEquals(123.456, $attribute['default']);
$this->assertEquals(0, $attribute['min']);
$this->assertEquals(1000, $attribute['max']);
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/float/' . $key, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -2451,6 +2521,17 @@ class DatabasesCustomServerTest extends Scope
$this->assertFalse($new['body']['required']);
$this->assertEquals(true, $new['body']['default']);
$new = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$attribute = array_values(array_filter($new['body']['attributes'], fn (array $a) => $a['key'] === $key))[0] ?? null;
$this->assertNotNull($attribute);
$this->assertFalse($attribute['required']);
$this->assertEquals(true, $attribute['default']);
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/boolean/' . $key, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -2582,6 +2663,17 @@ class DatabasesCustomServerTest extends Scope
$this->assertFalse($new['body']['required']);
$this->assertEquals('1975-06-12 14:12:55+02:00', $new['body']['default']);
$new = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$attribute = array_values(array_filter($new['body']['attributes'], fn (array $a) => $a['key'] === $key))[0] ?? null;
$this->assertNotNull($attribute);
$this->assertFalse($attribute['required']);
$this->assertEquals('1975-06-12 14:12:55+02:00', $attribute['default']);
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/datetime/' . $key, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -2718,6 +2810,21 @@ class DatabasesCustomServerTest extends Scope
$this->assertContains('ipsum', $new['body']['elements']);
$this->assertContains('dolor', $new['body']['elements']);
$new = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$attribute = array_values(array_filter($new['body']['attributes'], fn (array $a) => $a['key'] === $key))[0] ?? null;
$this->assertNotNull($attribute);
$this->assertFalse($attribute['required']);
$this->assertEquals('lorem', $attribute['default']);
$this->assertCount(3, $attribute['elements']);
$this->assertContains('lorem', $attribute['elements']);
$this->assertContains('ipsum', $attribute['elements']);
$this->assertContains('dolor', $attribute['elements']);
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/enum/' . $key, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],

View file

@ -176,7 +176,7 @@ class DatabasesPermissionsTeamTest extends Scope
if ($success) {
$this->assertCount(1, $documents['body']['documents']);
} else {
$this->assertEquals(401, $documents['headers']['status-code']);
$this->assertCount(0, $documents['body']['documents']);
}
}

View file

@ -26,6 +26,8 @@ trait Base
public static string $CREATE_IP_ATTRIBUTE = 'create_ip_attribute';
public static string $CREATE_ENUM_ATTRIBUTE = 'create_enum_attribute';
public static string $CREATE_DATETIME_ATTRIBUTE = 'create_datetime_attribute';
public static string $CREATE_RELATIONSHIP_ATTRIBUTE = 'create_relationship_attribute';
public static string $UPDATE_STRING_ATTRIBUTE = 'update_string_attribute';
public static string $UPDATE_INTEGER_ATTRIBUTE = 'update_integer_attribute';
public static string $UPDATE_FLOAT_ATTRIBUTE = 'update_float_attribute';
@ -35,6 +37,8 @@ trait Base
public static string $UPDATE_IP_ATTRIBUTE = 'update_ip_attribute';
public static string $UPDATE_ENUM_ATTRIBUTE = 'update_enum_attribute';
public static string $UPDATE_DATETIME_ATTRIBUTE = 'update_datetime_attribute';
public static string $UPDATE_RELATIONSHIP_ATTRIBUTE = 'update_relationship_attribute';
public static string $GET_ATTRIBUTES = 'get_attributes';
public static string $GET_ATTRIBUTE = 'get_attribute';
public static string $DELETE_ATTRIBUTE = 'delete_attribute';
@ -463,6 +467,17 @@ trait Base
array
}
}';
case self::$CREATE_RELATIONSHIP_ATTRIBUTE:
return 'mutation createRelationshipAttribute($databaseId: String!, $collectionId: String!, $relatedCollectionId: String!, $type: String!, $twoWay: Boolean, $key: String, $twoWayKey: String, $onDelete: String){
databasesCreateRelationshipAttribute(databaseId: $databaseId, collectionId: $collectionId, relatedCollectionId: $relatedCollectionId, type: $type, twoWay: $twoWay, key: $key, twoWayKey: $twoWayKey, onDelete: $onDelete) {
relatedCollection
relationType
twoWay
key
twoWayKey
onDelete
}
}';
case self::$UPDATE_STRING_ATTRIBUTE:
return 'mutation updateStringAttribute($databaseId: String!, $collectionId: String!, $key: String!, $required: Boolean!, $default: String){
databasesUpdateStringAttribute(databaseId: $databaseId, collectionId: $collectionId, key: $key, required: $required, default: $default) {
@ -531,6 +546,17 @@ trait Base
default
}
}';
case self::$UPDATE_RELATIONSHIP_ATTRIBUTE:
return 'mutation updateRelationshipAttribute($databaseId: String!, $collectionId: String!, $key: String!, $onDelete: String){
databasesUpdateRelationshipAttribute(databaseId: $databaseId, collectionId: $collectionId, key: $key, onDelete: $onDelete) {
relatedCollection
relationType
twoWay
key
twoWayKey
onDelete
}
}';
case self::$CREATE_INDEX:
return 'mutation createIndex($databaseId: String!, $collectionId: String!, $key: String!, $type: String!, $attributes: [String!]!, $orders: [String!]){
databasesCreateIndex(databaseId: $databaseId, collectionId: $collectionId, key: $key, type: $type, attributes: $attributes, orders: $orders) {

View file

@ -7,6 +7,7 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@ -75,9 +76,36 @@ class DatabaseServerTest extends Scope
$collection = $collection['body']['data']['databasesCreateCollection'];
$this->assertEquals('Actors', $collection['name']);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $database['_id'],
'collectionId' => 'movies',
'name' => 'Movies',
'documentSecurity' => false,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]
];
$collection2 = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertIsArray($collection2['body']['data']);
$this->assertArrayNotHasKey('errors', $collection2['body']);
$collection2 = $collection2['body']['data']['databasesCreateCollection'];
$this->assertEquals('Movies', $collection2['name']);
return [
'database' => $database,
'collection' => $collection,
'collection2' => $collection2,
];
}
@ -119,7 +147,7 @@ class DatabaseServerTest extends Scope
public function testUpdateStringAttribute($data): array
{
// Wait for attributes to be available
sleep(3);
sleep(1);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_STRING_ATTRIBUTE);
@ -187,7 +215,7 @@ class DatabaseServerTest extends Scope
public function testUpdateIntegerAttribute($data): array
{
// Wait for attributes to be available
sleep(3);
sleep(1);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_INTEGER_ATTRIBUTE);
@ -257,7 +285,7 @@ class DatabaseServerTest extends Scope
public function testUpdateBooleanAttribute($data): array
{
// Wait for attributes to be available
sleep(3);
sleep(1);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_BOOLEAN_ATTRIBUTE);
@ -326,7 +354,7 @@ class DatabaseServerTest extends Scope
public function testUpdateFloatAttribute($data): array
{
// Wait for attributes to be available
sleep(3);
sleep(1);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_FLOAT_ATTRIBUTE);
@ -396,7 +424,7 @@ class DatabaseServerTest extends Scope
public function testUpdateEmailAttribute($data): array
{
// Wait for attributes to be available
sleep(3);
sleep(1);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_EMAIL_ATTRIBUTE);
@ -468,7 +496,7 @@ class DatabaseServerTest extends Scope
public function testUpdateEnumAttribute($data): array
{
// Wait for attributes to be available
sleep(3);
sleep(1);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_ENUM_ATTRIBUTE);
@ -541,7 +569,7 @@ class DatabaseServerTest extends Scope
public function testUpdateDatetimeAttribute($data): array
{
// Wait for attributes to be available
sleep(3);
sleep(1);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_DATETIME_ATTRIBUTE);
@ -570,6 +598,69 @@ class DatabaseServerTest extends Scope
return $data;
}
/**
* @depends testCreateCollection
*/
public function testCreateRelationshipAttribute(array $data): array
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_RELATIONSHIP_ATTRIBUTE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection2']['_id'], // Movies
'relatedCollectionId' => $data['collection']['_id'], // Actors
'type' => Database::RELATION_ONE_TO_MANY,
'twoWay' => true,
'key' => 'actors',
'twoWayKey' => 'movie'
]
];
$attribute = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $attribute['body']);
$this->assertIsArray($attribute['body']['data']);
$this->assertIsArray($attribute['body']['data']['databasesCreateRelationshipAttribute']);
return $data;
}
/**
* @depends testCreateRelationshipAttribute
*/
public function testUpdateRelationshipAttribute(array $data): array
{
sleep(1);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_RELATIONSHIP_ATTRIBUTE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'collectionId' => $data['collection2']['_id'],
'key' => 'actors',
'onDelete' => Database::RELATION_MUTATE_CASCADE,
]
];
$attribute = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $attribute['body']);
$this->assertIsArray($attribute['body']['data']);
$this->assertIsArray($attribute['body']['data']['databasesUpdateRelationshipAttribute']);
return $data;
}
/**
* @depends testCreateCollection
* @throws Exception

View file

@ -36,9 +36,14 @@ class FilterTest extends TestCase
public function testValue(): void
{
// Test for Success
$this->assertEquals($this->validator->isValid(Query::equal('attr', ['v'])), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::between('attr', '1975-12-06', '2050-12-06')), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::isNotNull('attr')), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::isNull('attr')), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::startsWith('attr', 'super')), true, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::endsWith('attr', 'man')), true, $this->validator->getDescription());
// Test for Failure
$this->assertEquals($this->validator->isValid(Query::select(['attr'])), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(1)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(0)), false, $this->validator->getDescription());
$this->assertEquals($this->validator->isValid(Query::limit(100)), false, $this->validator->getDescription());

View file

@ -0,0 +1,45 @@
<?php
namespace Tests\Unit\Utopia\Database\Validator\Query;
use Appwrite\Utopia\Database\Validator\Query\Base;
use Appwrite\Utopia\Database\Validator\Query\Order;
use Appwrite\Utopia\Database\Validator\Query\Select;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use PHPUnit\Framework\TestCase;
class SelectTest extends TestCase
{
/**
* @var Base
*/
protected $validator = null;
public function setUp(): void
{
$this->validator = new Select(
attributes: [
new Document([
'key' => 'attr',
'type' => Database::VAR_STRING,
'array' => false,
]),
],
);
}
public function tearDown(): void
{
}
public function testValue(): void
{
// Test for Success
$this->assertEquals($this->validator->isValid(Query::select(['*', 'attr'])), true, $this->validator->getDescription());
// Test for Failure
$this->assertEquals($this->validator->isValid(Query::limit(1)), false, $this->validator->getDescription());
}
}