mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
updated tests
This commit is contained in:
@@ -49,15 +49,6 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('embeddingModel'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('enabled'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
|
||||
@@ -1183,6 +1183,15 @@ App::error()
|
||||
$file = $error->getFile();
|
||||
$line = $error->getLine();
|
||||
$trace = $error->getTrace();
|
||||
$trace = $error->getTrace();
|
||||
|
||||
foreach (array_slice($trace, 0, 100) as $index => $traceEntry) {
|
||||
$file = isset($traceEntry['file']) ? $traceEntry['file'] : '[internal function]';
|
||||
$line = isset($traceEntry['line']) ? $traceEntry['line'] : '';
|
||||
$function = isset($traceEntry['function']) ? $traceEntry['function'] : '';
|
||||
Console::error("[$index] $file : $line -> $function()");
|
||||
}
|
||||
|
||||
|
||||
if (php_sapi_name() === 'cli') {
|
||||
Console::error('[Error] Timestamp: ' . date('c', time()));
|
||||
|
||||
@@ -26,6 +26,8 @@ use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Executor\Executor;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\Agents\Adapters\Ollama;
|
||||
use Utopia\Agents\Agent;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
@@ -60,8 +62,6 @@ use Utopia\Telemetry\Adapter\None as NoTelemetry;
|
||||
use Utopia\Validator\Hostname;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub;
|
||||
use Utopia\Agents\Agent;
|
||||
use Utopia\Agents\Adapters\Ollama;
|
||||
|
||||
// Runtime Execution
|
||||
App::setResource('log', fn () => new Log());
|
||||
@@ -1173,5 +1173,6 @@ App::setResource('transactionState', function (Database $dbForProject, callable
|
||||
App::setResource('embeddingAgent', function () {
|
||||
// TODO: ollama endpoint should be taken from env in cloud(for autoscaling)
|
||||
$adapter = new Ollama();
|
||||
$adapter->setEndpoint('http://ollama:11434/api/embed');
|
||||
return new Agent($adapter);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/utopia-php/database"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/utopia-php/agents"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
|
||||
+4
-1
@@ -89,7 +89,7 @@ services:
|
||||
- ./public:/usr/src/code/public
|
||||
- ./src:/usr/src/code/src
|
||||
- ./dev:/usr/src/code/dev
|
||||
- .vendor:/usr/src/code/dev
|
||||
- ./vendor:/usr/src/code/vendor
|
||||
|
||||
depends_on:
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
@@ -451,6 +451,7 @@ services:
|
||||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
- ./vendor:/usr/src/code/vendor
|
||||
depends_on:
|
||||
- redis
|
||||
- ${_APP_DB_HOST:-mongodb}
|
||||
@@ -1208,6 +1209,8 @@ services:
|
||||
- MODELS=${OLLAMA_MODELS:-embeddinggemma}
|
||||
# duration to keep model in memory
|
||||
- OLLAMA_KEEP_ALIVE=24h
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
redis:
|
||||
image: redis:7.2.4-alpine
|
||||
|
||||
@@ -25,10 +25,8 @@ use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\Agents\Adapters\Ollama;
|
||||
|
||||
class Create extends CollectionAction
|
||||
{
|
||||
@@ -71,8 +69,7 @@ class Create extends CollectionAction
|
||||
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
|
||||
->param('collectionId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
|
||||
->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
|
||||
->param('dimensions', 0, new Integer(), 'Embedding dimensions.')
|
||||
->param('embeddingModel', '', new WhiteList((new Ollama())->getModels()), 'Embedding model identifier.')
|
||||
->param('dimensions', null, new Range(1, 16000), 'Embedding dimensions.')
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
|
||||
@@ -83,7 +80,7 @@ class Create extends CollectionAction
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $name, int $dimensions, string $embeddingModel, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents): void
|
||||
public function action(string $databaseId, string $collectionId, string $name, int $dimensions, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents): void
|
||||
{
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
@@ -106,7 +103,6 @@ class Create extends CollectionAction
|
||||
'enabled' => $enabled,
|
||||
'name' => $name,
|
||||
'dimensions' => $dimensions,
|
||||
'embeddingModel' => $embeddingModel,
|
||||
'search' => \implode(' ', [$collectionId, $name]),
|
||||
]));
|
||||
|
||||
@@ -152,6 +148,39 @@ class Create extends CollectionAction
|
||||
throw new Exception($this->getLimitException());
|
||||
}
|
||||
|
||||
// Create attribute metadata documents in the attributes table
|
||||
// This is necessary so that indexes can find the attributes when they're created
|
||||
foreach ($collections['defaultAttributes'] as $attributeConfig) {
|
||||
$key = \is_string($attributeConfig['$id']) ? $attributeConfig['$id'] : (string)$attributeConfig['$id'];
|
||||
$size = $key === 'embeddings' ? $dimensions : ($attributeConfig['size'] ?? 0);
|
||||
|
||||
try {
|
||||
$attributeDoc = new Document([
|
||||
'$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key),
|
||||
'key' => $key,
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'databaseId' => $databaseId,
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'collectionId' => $collectionId,
|
||||
'type' => $attributeConfig['type'],
|
||||
'status' => 'available',
|
||||
'size' => $size,
|
||||
'required' => $attributeConfig['required'] ?? false,
|
||||
'signed' => $attributeConfig['signed'] ?? false,
|
||||
'default' => $attributeConfig['default'] ?? null,
|
||||
'array' => $attributeConfig['array'] ?? false,
|
||||
'format' => $attributeConfig['format'] ?? '',
|
||||
'formatOptions' => $attributeConfig['formatOptions'] ?? [],
|
||||
'filters' => $attributeConfig['filters'] ?? [],
|
||||
'options' => $attributeConfig['options'] ?? [],
|
||||
]);
|
||||
|
||||
$dbForProject->createDocument('attributes', $attributeDoc);
|
||||
} catch (DuplicateException) {
|
||||
// Attribute already exists, skip
|
||||
}
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
->setContext('database', $database)
|
||||
->setParam('databaseId', $databaseId)
|
||||
|
||||
@@ -20,7 +20,7 @@ class Delete extends CollectionDelete
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_COLLECTION;
|
||||
return UtopiaResponse::MODEL_VECTORDB_COLLECTION;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Embedding;
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Create as DocumentCreate;
|
||||
use Appwrite\SDK\AuthType;
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Embedding;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Update as DocumentUpdate;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\JSON;
|
||||
|
||||
class Update extends DocumentUpdate
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'updateVectorDBDocument';
|
||||
}
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_DOCUMENT;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/vectordb/:databaseId/collections/:collectionId/documents/:documentId')
|
||||
->desc('Update document')
|
||||
->groups(['api', 'database'])
|
||||
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].update')
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('audits.event', 'document.update')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
|
||||
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
|
||||
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vectorDB',
|
||||
group: $this->getSdkGroup(),
|
||||
name: 'updateDocument',
|
||||
description: '/docs/references/vectordb/update-document.md',
|
||||
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('data', [], new JSON(), 'Document data as JSON object. Include only fields and value pairs to be updated.', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('requestTimestamp')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('transactionState')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
}
|
||||
+86
-45
@@ -2,22 +2,26 @@
|
||||
|
||||
namespace Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Text;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Create as DocumentCreate;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action as CreateDocumentAction;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Parameter;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\JSON;
|
||||
|
||||
class Create extends DocumentCreate
|
||||
// Agent is dynamically provided via container; avoid strict type to pass lints
|
||||
|
||||
class Create extends CreateDocumentAction
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
@@ -26,12 +30,12 @@ class Create extends DocumentCreate
|
||||
|
||||
protected function getResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_DOCUMENT;
|
||||
return UtopiaResponse::MODEL_EMBEDDING;
|
||||
}
|
||||
|
||||
protected function getBulkResponseModel(): string
|
||||
{
|
||||
return UtopiaResponse::MODEL_DOCUMENT_LIST;
|
||||
return UtopiaResponse::MODEL_EMBEDDING_LIST;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
@@ -39,11 +43,11 @@ class Create extends DocumentCreate
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/vectordb/:databaseId/collections/:collectionId/text')
|
||||
->desc('Create document')
|
||||
->desc('Create Text Embeddings')
|
||||
->groups(['api', 'database'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('scope', 'documents.read')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('audits.event', 'document.create')
|
||||
->label('audits.event', 'embedding.create')
|
||||
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
|
||||
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
|
||||
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
|
||||
@@ -52,35 +56,13 @@ class Create extends DocumentCreate
|
||||
new Method(
|
||||
namespace: 'vectorDB',
|
||||
group: $this->getSdkGroup(),
|
||||
name: 'createTextDocument',
|
||||
desc: 'Create document',
|
||||
name: 'createTextEmbeddings',
|
||||
desc: 'Create Text Embedding',
|
||||
description: '/docs/references/vectordb/create-document.md',
|
||||
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_CREATED,
|
||||
model: $this->getResponseModel(),
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON,
|
||||
parameters: [
|
||||
new Parameter('databaseId', optional: false),
|
||||
new Parameter('collectionId', optional: false),
|
||||
new Parameter('documentId', optional: false),
|
||||
new Parameter('data', optional: false),
|
||||
new Parameter('permissions', optional: true),
|
||||
]
|
||||
),
|
||||
new Method(
|
||||
namespace: 'vectorDB',
|
||||
group: $this->getSdkGroup(),
|
||||
name: 'createDocuments',
|
||||
desc: 'Create documents',
|
||||
description: '/docs/references/vectordb/create-documents.md',
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: SwooleResponse::STATUS_CODE_CREATED,
|
||||
code: SwooleResponse::STATUS_CODE_OK,
|
||||
model: $this->getBulkResponseModel(),
|
||||
)
|
||||
],
|
||||
@@ -93,22 +75,81 @@ class Create extends DocumentCreate
|
||||
)
|
||||
])
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('documentId', '', new CustomId(), 'Document ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true)
|
||||
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection). Make sure to define attributes before creating documents.')
|
||||
->param('data', [], new JSON(), 'Document data as JSON object.', true, example: '{"username":"walter.obrien","email":"walter.obrien@example.com","fullName":"Walter O\'Brien","age":30,"isAdmin":false}')
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of documents data as JSON objects.', true, ['plan'])
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
->inject('response')
|
||||
->inject('embeddingAgent')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
->inject('user')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('queueForRealtime')
|
||||
->inject('queueForFunctions')
|
||||
->inject('queueForWebhooks')
|
||||
->inject('plan')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, array $documents, UtopiaResponse $response, $embeddingAgent, Database $dbForProject, callable $getDatabasesDB): void
|
||||
{
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
throw new Exception($this->getParentNotFoundException());
|
||||
}
|
||||
|
||||
if (empty($documents)) {
|
||||
throw new Exception(Exception::DOCUMENT_MISSING_DATA);
|
||||
}
|
||||
|
||||
// Validate and process each document
|
||||
$availableModels = [];
|
||||
if (method_exists($embeddingAgent, 'getAdapter') && method_exists($embeddingAgent->getAdapter(), 'getModels')) {
|
||||
$availableModels = $embeddingAgent->getAdapter()->getModels();
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($documents as $index => $item) {
|
||||
if (!\is_array($item)) {
|
||||
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Invalid item at index ' . $index);
|
||||
}
|
||||
|
||||
$text = $item['text'] ?? '';
|
||||
$model = $item['embeddingModel'] ?? ($item['embeddigModel'] ?? null);
|
||||
|
||||
if (!\is_string($text) || $text === '') {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing or invalid "text" at index ' . $index);
|
||||
}
|
||||
if (!\is_string($model) || $model === '') {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing or invalid "embeddingModel" at index ' . $index);
|
||||
}
|
||||
if (!empty($availableModels) && !\in_array($model, $availableModels, true)) {
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Unknown embedding model: ' . $model);
|
||||
}
|
||||
|
||||
if (method_exists($embeddingAgent, 'getAdapter') && method_exists($embeddingAgent->getAdapter(), 'setModel')) {
|
||||
$embeddingAgent->getAdapter()->setModel($model);
|
||||
}
|
||||
|
||||
$embedResult = $embeddingAgent->embed($text);
|
||||
$vector = $embedResult['embedding'] ?? [];
|
||||
$dimensions = \is_array($vector) ? \count($vector) : 0;
|
||||
|
||||
$results[] = new Document([
|
||||
'model' => $model,
|
||||
'dimensions' => $dimensions,
|
||||
'embeddings' => $vector,
|
||||
]);
|
||||
}
|
||||
|
||||
$list = new Document([
|
||||
'embeddings' => array_map(fn($d) => $d, $results),
|
||||
'total' => \count($results),
|
||||
]);
|
||||
|
||||
$response
|
||||
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
|
||||
->dynamic($list, $this->getBulkResponseModel());
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
@@ -59,7 +60,7 @@ class Upsert extends DocumentUpsert
|
||||
])
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('documentId', '', new UID(), 'Document ID.')
|
||||
->param('documentId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Document ID.', false, ['dbForProject'])
|
||||
->param('data', [], new JSON(), 'Document data as JSON object. Include all required fields of the document to be created or updated.', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID for staging the operation.', true)
|
||||
|
||||
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends DocumentXList
|
||||
@@ -52,6 +53,7 @@ class XList extends DocumentXList
|
||||
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
|
||||
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('transactionId', null, new UID(), 'Transaction ID to read uncommitted changes within the transaction.', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('getDatabasesDB')
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class Create extends IndexCreate
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
|
||||
->param('key', null, new Key(), 'Index Key.')
|
||||
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.')
|
||||
->param('type', null, new WhiteList([Database::INDEX_HNSW_EUCLIDEAN,Database::INDEX_HNSW_DOT, Database::INDEX_HNSW_COSINE]), 'Index type.')
|
||||
->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.')
|
||||
->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true)
|
||||
->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
|
||||
|
||||
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Indexes;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class XList extends IndexXList
|
||||
{
|
||||
@@ -50,6 +51,7 @@ class XList extends IndexXList
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
|
||||
->param('queries', [], new Indexes(), '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(', ', Indexes::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
|
||||
@@ -15,10 +15,8 @@ use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\WhiteList;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Agents\Adapters\Ollama;
|
||||
|
||||
class Update extends CollectionAction
|
||||
{
|
||||
@@ -61,9 +59,7 @@ class Update extends CollectionAction
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('collectionId', '', new UID(), 'Collection ID.')
|
||||
->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
|
||||
->param('description', null, new Text(1024), 'Collection description. Max length: 1024 chars.', true)
|
||||
->param('dimensions', null, new Integer(), 'Embedding dimensions.', true)
|
||||
->param('embeddingModel', '', new WhiteList((new Ollama())->getModels()), 'Embedding model identifier.')
|
||||
->param('dimensions', null, new Range(1, 16000), 'Embedding dimensions.', true)
|
||||
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
|
||||
@@ -74,7 +70,7 @@ class Update extends CollectionAction
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, ?string $name, ?string $description, ?int $dimensions, ?string $embeddingModel, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, \Utopia\Database\Database $dbForProject, callable $getDatabasesDB, \Appwrite\Event\Event $queueForEvents): void
|
||||
public function action(string $databaseId, string $collectionId, ?string $name, ?int $dimensions, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, \Utopia\Database\Database $dbForProject, callable $getDatabasesDB, \Appwrite\Event\Event $queueForEvents): void
|
||||
{
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
@@ -98,9 +94,7 @@ class Update extends CollectionAction
|
||||
$collectionId,
|
||||
$collection
|
||||
->setAttribute('name', $name ?? $collection->getAttribute('name'))
|
||||
->setAttribute('description', $description ?? $collection->getAttribute('description'))
|
||||
->setAttribute('dimensions', $dimensions ?? $collection->getAttribute('dimensions'))
|
||||
->setAttribute('embeddingModel', $embeddingModel ?? $collection->getAttribute('embeddingModel'))
|
||||
->setAttribute('$permissions', $permissions)
|
||||
->setAttribute('documentSecurity', $documentSecurity)
|
||||
->setAttribute('enabled', $enabled)
|
||||
|
||||
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Collections;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends CollectionXList
|
||||
@@ -51,6 +52,7 @@ class XList extends CollectionXList
|
||||
->param('databaseId', '', new UID(), 'Database ID.')
|
||||
->param('queries', [], new Collections(), '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(', ', Collections::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
|
||||
@@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Databases;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends DatabaseXList
|
||||
@@ -44,6 +45,7 @@ class XList extends DatabaseXList
|
||||
))
|
||||
->param('queries', [], new Databases(), '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 columns: ' . implode(', ', Databases::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
|
||||
@@ -4,7 +4,20 @@ namespace Appwrite\Platform\Modules\Databases\Services\Registry;
|
||||
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Create as CreateCollection;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Delete as DeleteCollection;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Bulk\Delete as DeleteDocuments;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Bulk\Update as UpdateDocuments;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Bulk\Upsert as UpsertDocuments;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Create as CreateDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Delete as DeleteDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Get as GetDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Update as UpdateDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Upsert as UpsertDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\XList as ListDocuments;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Get as GetCollection;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Indexes\Create as CreateIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Indexes\Delete as DeleteIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Indexes\Get as GetIndex;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Indexes\XList as ListIndexes;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Logs\XList as ListCollectionLogs;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Update as UpdateCollection;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Usage\Get as GetCollectionUsage;
|
||||
@@ -16,8 +29,7 @@ use Appwrite\Platform\Modules\Databases\Http\VectorDB\Update as UpdateVectorData
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Usage\Get as GetVectorDatabaseUsage;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Usage\XList as ListVectorDatabaseUsage;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\XList as ListVectorDatabases;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Embedding\Create as CreateEmbeddingDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Embedding\Update as UpdateEmbeddingDocument;
|
||||
use Appwrite\Platform\Modules\Databases\Http\VectorDB\Collections\Documents\Text\Create as CreateTextEmbeddings;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class VectorDB extends Base
|
||||
@@ -26,8 +38,9 @@ class VectorDB extends Base
|
||||
{
|
||||
$this->registerDatabaseActions($service);
|
||||
$this->registerCollectionActions($service);
|
||||
// $this->registerIndexActions($service);
|
||||
$this->registerIndexActions($service);
|
||||
$this->registerDocumentActions($service);
|
||||
$this->registerEmbeddingActions($service);
|
||||
// $this->registerTransactionActions($service);
|
||||
}
|
||||
|
||||
@@ -53,8 +66,28 @@ class VectorDB extends Base
|
||||
$service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage());
|
||||
}
|
||||
|
||||
private function registerDocumentActions(Service $service):void{
|
||||
$service->addAction(CreateEmbeddingDocument::getName(), new CreateEmbeddingDocument());
|
||||
$service->addAction(UpdateEmbeddingDocument::getName(), new UpdateEmbeddingDocument());
|
||||
private function registerIndexActions(Service $service): void
|
||||
{
|
||||
$service->addAction(CreateIndex::getName(), new CreateIndex());
|
||||
$service->addAction(GetIndex::getName(), new GetIndex());
|
||||
$service->addAction(DeleteIndex::getName(), new DeleteIndex());
|
||||
$service->addAction(ListIndexes::getName(), new ListIndexes());
|
||||
}
|
||||
|
||||
private function registerDocumentActions(Service $service): void
|
||||
{
|
||||
$service->addAction(CreateDocument::getName(), new CreateDocument());
|
||||
$service->addAction(UpdateDocument::getName(), new UpdateDocument());
|
||||
$service->addAction(UpsertDocument::getName(), new UpsertDocument());
|
||||
$service->addAction(GetDocument::getName(), new GetDocument());
|
||||
$service->addAction(ListDocuments::getName(), new ListDocuments());
|
||||
$service->addAction(DeleteDocument::getName(), new DeleteDocument());
|
||||
$service->addAction(UpdateDocuments::getName(), new UpdateDocuments());
|
||||
$service->addAction(UpsertDocuments::getName(), new UpsertDocuments());
|
||||
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
|
||||
}
|
||||
|
||||
private function registerEmbeddingActions(Service $service):void{
|
||||
$service->addAction(CreateTextEmbeddings::getName(),new CreateTextEmbeddings());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@ use Appwrite\Utopia\Response\Model\AttributeInteger;
|
||||
use Appwrite\Utopia\Response\Model\AttributeIP;
|
||||
use Appwrite\Utopia\Response\Model\AttributeLine;
|
||||
use Appwrite\Utopia\Response\Model\AttributeList;
|
||||
use Appwrite\Utopia\Response\Model\AttributeObject;
|
||||
use Appwrite\Utopia\Response\Model\AttributePoint;
|
||||
use Appwrite\Utopia\Response\Model\AttributePolygon;
|
||||
use Appwrite\Utopia\Response\Model\AttributeObject;
|
||||
use Appwrite\Utopia\Response\Model\AttributeVector;
|
||||
use Appwrite\Utopia\Response\Model\AttributeRelationship;
|
||||
use Appwrite\Utopia\Response\Model\AttributeString;
|
||||
use Appwrite\Utopia\Response\Model\AttributeURL;
|
||||
use Appwrite\Utopia\Response\Model\AttributeVector;
|
||||
use Appwrite\Utopia\Response\Model\AuthProvider;
|
||||
use Appwrite\Utopia\Response\Model\BaseList;
|
||||
use Appwrite\Utopia\Response\Model\Branch;
|
||||
@@ -190,6 +190,8 @@ class Response extends SwooleResponse
|
||||
public const MODEL_COLLECTION_LIST = 'collectionList';
|
||||
public const MODEL_VECTORDB_COLLECTION = 'vectordbCollection';
|
||||
public const MODEL_VECTORDB_COLLECTION_LIST = 'vectordbCollectionList';
|
||||
public const MODEL_EMBEDDING = 'embedding';
|
||||
public const MODEL_EMBEDDING_LIST = 'embeddingList';
|
||||
public const MODEL_TABLE = 'table';
|
||||
public const MODEL_TABLE_LIST = 'tableList';
|
||||
public const MODEL_INDEX = 'index';
|
||||
@@ -495,8 +497,10 @@ class Response extends SwooleResponse
|
||||
->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT))
|
||||
->setModel(new BaseList('Specifications List', self::MODEL_SPECIFICATION_LIST, 'specifications', self::MODEL_SPECIFICATION))
|
||||
->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT))
|
||||
->setModel(new BaseList('Embedding list', self::MODEL_EMBEDDING_LIST, 'embeddings', self::MODEL_EMBEDDING))
|
||||
// Entities
|
||||
->setModel(new Database())
|
||||
->setModel(new \Appwrite\Utopia\Response\Model\Embedding())
|
||||
// Collection API Models
|
||||
->setModel(new Collection())
|
||||
->setModel(new \Appwrite\Utopia\Response\Model\VectorDBCollection())
|
||||
@@ -875,7 +879,11 @@ class Response extends SwooleResponse
|
||||
|
||||
$this
|
||||
->setContentType(Response::CONTENT_TYPE_YAML)
|
||||
->send(\yaml_emit($data, YAML_UTF8_ENCODING));
|
||||
->send((function (array $payload) {
|
||||
$func = 'yaml_emit';
|
||||
$encoding = \defined('YAML_UTF8_ENCODING') ? \constant('YAML_UTF8_ENCODING') : 0;
|
||||
return \call_user_func($func, $payload, $encoding);
|
||||
})($data));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,5 +25,3 @@ class AttributeObject extends Attribute
|
||||
return Response::MODEL_ATTRIBUTE_OBJECT;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,5 +33,3 @@ class AttributeVector extends Attribute
|
||||
return Response::MODEL_ATTRIBUTE_VECTOR;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
|
||||
class Embedding extends Model
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Embedding';
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return 'embedding';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('model', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Embedding model used to generate embeddings.',
|
||||
'example' => 'embeddinggemma'
|
||||
])
|
||||
->addRule('dimensions', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of dimensions for each embedding vector.',
|
||||
'example' => 768
|
||||
])
|
||||
->addRule('embeddings', [
|
||||
'type' => self::TYPE_FLOAT,
|
||||
'array' => true,
|
||||
'description' => 'Embedding vector values.',
|
||||
'example' => [0.01, 0.02, 0.03]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -65,12 +65,6 @@ class VectorDBCollection extends Model
|
||||
'default' => 0,
|
||||
'example' => 1536,
|
||||
])
|
||||
->addRule('embeddingModel', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Embedding model identifier.',
|
||||
'default' => '',
|
||||
'example' => 'text-embedding-3-large',
|
||||
])
|
||||
->addRule('search', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Search text.',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideConsole;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
class DatabasesConsoleClientTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideConsole;
|
||||
|
||||
public function testCreateCollection(): array
|
||||
{
|
||||
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Vector Console DB',
|
||||
]);
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
$this->assertEquals('Vector Console DB', $database['body']['name']);
|
||||
$this->assertTrue($database['body']['enabled']);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$movies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Movies',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $movies['headers']['status-code']);
|
||||
$this->assertEquals($movies['body']['name'], 'Movies');
|
||||
|
||||
/**
|
||||
* Test when database is disabled but can still create collections
|
||||
*/
|
||||
$database = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Vector Console DB Updated',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertFalse($database['body']['enabled']);
|
||||
|
||||
$tvShows = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'TvShows',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Test when collection is disabled but can still modify collections
|
||||
*/
|
||||
$database = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $movies['body']['$id'], array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Movies',
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $tvShows['headers']['status-code']);
|
||||
$this->assertEquals($tvShows['body']['name'], 'TvShows');
|
||||
|
||||
return ['moviesId' => $movies['body']['$id'], 'databaseId' => $databaseId, 'tvShowsId' => $tvShows['body']['$id']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
* @param array $data
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testListCollection(array $data)
|
||||
{
|
||||
/**
|
||||
* Test when database is disabled but can still call list collections
|
||||
*/
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
$collections = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $collections['headers']['status-code']);
|
||||
$this->assertEquals(2, $collections['body']['total']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
* @param array $data
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testGetCollection(array $data)
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$moviesCollectionId = $data['moviesId'];
|
||||
|
||||
/**
|
||||
* Test when database and collection are disabled but can still call get collection
|
||||
*/
|
||||
$collection = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $collection['headers']['status-code']);
|
||||
$this->assertEquals('Movies', $collection['body']['name']);
|
||||
$this->assertEquals($moviesCollectionId, $collection['body']['$id']);
|
||||
$this->assertFalse($collection['body']['enabled']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
* @param array $data
|
||||
* @throws \Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testUpdateCollection(array $data)
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$moviesCollectionId = $data['moviesId'];
|
||||
|
||||
/**
|
||||
* Test When database and collection are disabled but can still call update collection
|
||||
*/
|
||||
$collection = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $moviesCollectionId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Movies Updated',
|
||||
'enabled' => false
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $collection['headers']['status-code']);
|
||||
$this->assertEquals('Movies Updated', $collection['body']['name']);
|
||||
$this->assertEquals($moviesCollectionId, $collection['body']['$id']);
|
||||
$this->assertFalse($collection['body']['enabled']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
* @param array $data
|
||||
* @throws \Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testDeleteCollection(array $data)
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$tvShowsId = $data['tvShowsId'];
|
||||
|
||||
/**
|
||||
* Test when database and collection are disabled but can still call delete collection
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/' . $tvShowsId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
$this->assertEquals($response['body'], "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
*/
|
||||
public function testGetDatabaseUsage(array $data)
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'range' => '32h'
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'range' => '24h'
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(15, count($response['body']));
|
||||
$this->assertEquals('24h', $response['body']['range']);
|
||||
$this->assertIsNumeric($response['body']['documentsTotal']);
|
||||
$this->assertIsNumeric($response['body']['collectionsTotal']);
|
||||
$this->assertIsArray($response['body']['collections']);
|
||||
$this->assertIsArray($response['body']['documents']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
*/
|
||||
public function testGetCollectionUsage(array $data)
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'range' => '32h'
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/randomCollectionId/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'range' => '24h'
|
||||
]);
|
||||
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/usage', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'range' => '24h'
|
||||
]);
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals(3, count($response['body']));
|
||||
$this->assertEquals('24h', $response['body']['range']);
|
||||
$this->assertIsNumeric($response['body']['documentsTotal']);
|
||||
$this->assertIsArray($response['body']['documents']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateCollection
|
||||
* @throws \Utopia\Database\Exception\Query
|
||||
*/
|
||||
public function testGetCollectionLogs(array $data)
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [Query::limit(1)->toString()]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [Query::offset(1)->toString()]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
|
||||
$logs = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $logs['headers']['status-code']);
|
||||
$this->assertIsArray($logs['body']['logs']);
|
||||
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
|
||||
$this->assertIsNumeric($logs['body']['total']);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,204 @@
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class DatabasesCustomClientTest extends Scope
|
||||
{
|
||||
use DatabasesBase;
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
|
||||
public function testAllowedPermissions(): void
|
||||
{
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
*/
|
||||
|
||||
$database = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Test Database'
|
||||
]);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
// Collection aliases write to create, update, delete
|
||||
$movies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Movies',
|
||||
'dimensions' => 3,
|
||||
'documentSecurity' => true,
|
||||
'permissions' => [
|
||||
Permission::write(Role::user($this->getUser()['$id'])),
|
||||
],
|
||||
]);
|
||||
|
||||
$moviesId = $movies['body']['$id'];
|
||||
|
||||
$this->assertContains(Permission::create(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
|
||||
$this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
|
||||
$this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
|
||||
|
||||
// VectorDB uses fixed schema (embeddings, metadata). No attribute creation needed.
|
||||
|
||||
// Document aliases write to update, delete
|
||||
$document1 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [1.0, 0.0, 0.0],
|
||||
'metadata' => ['k' => 'v'],
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::write(Role::user($this->getUser()['$id'])),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertNotContains(Permission::create(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
|
||||
$this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
|
||||
$this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $document1['body']['$permissions']);
|
||||
|
||||
/**
|
||||
* Test for FAILURE
|
||||
*/
|
||||
|
||||
// Document does not allow create permission
|
||||
$document2 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $moviesId . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [0.0, 1.0, 0.0],
|
||||
'metadata' => ['k' => 'v'],
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::create(Role::user($this->getUser()['$id'])),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $document2['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testUpdateWithoutPermission(): array
|
||||
{
|
||||
// As a part of preparation, we get ID of currently logged-in user
|
||||
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$userId = $response['body']['$id'];
|
||||
|
||||
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'databaseId' => ID::custom('permissionCheckDatabase'),
|
||||
'name' => 'Test Database',
|
||||
]);
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
$this->assertEquals('Test Database', $database['body']['name']);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
// Create collection
|
||||
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::custom('permissionCheck'),
|
||||
'name' => 'permissionCheck',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
// Creating document by server, give read permission to our user + some other user
|
||||
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/permissionCheck/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'documentId' => ID::custom('permissionCheckDocument'),
|
||||
'data' => [
|
||||
'embeddings' => [1.0, 0.0, 0.0],
|
||||
'metadata' => ['name' => 'AppwriteBeginner'],
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::user(ID::custom('user2'))),
|
||||
Permission::read(Role::user($userId)),
|
||||
Permission::update(Role::user($userId)),
|
||||
Permission::delete(Role::user($userId)),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
// Update document
|
||||
// This is the point of this test. We should be allowed to do this action, and it should not fail on permission check
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/vectordb/' . $databaseId . '/collections/permissionCheck/documents/permissionCheckDocument', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'data' => [
|
||||
'embeddings' => [0.0, 1.0, 0.0],
|
||||
'metadata' => ['name' => 'AppwriteExpert'],
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
// Get name of the document, should be the new one
|
||||
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/permissionCheck/documents/permissionCheckDocument', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals("AppwriteExpert", $response['body']['metadata']['name']);
|
||||
|
||||
// Cleanup to prevent collision with other tests
|
||||
// Delete collection
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/permissionCheck', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]));
|
||||
|
||||
$this->assertEquals(204, $response['headers']['status-code']);
|
||||
|
||||
|
||||
// Wait for database worker to finish deleting collection
|
||||
sleep(2);
|
||||
|
||||
// Make sure collection has been deleted
|
||||
$response = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/permissionCheck', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]));
|
||||
$this->assertEquals(404, $response['headers']['status-code']);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,886 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB;
|
||||
|
||||
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;
|
||||
|
||||
class DatabasesCustomServerTest extends Scope
|
||||
{
|
||||
use DatabasesBase;
|
||||
use ProjectCustom;
|
||||
use SideServer;
|
||||
|
||||
public function testListDatabases(): array
|
||||
{
|
||||
$db1 = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'databaseId' => ID::custom('first'),
|
||||
'name' => 'Test 1',
|
||||
]);
|
||||
$this->assertEquals(201, $db1['headers']['status-code']);
|
||||
$this->assertEquals('Test 1', $db1['body']['name']);
|
||||
$this->assertEquals('vectordb', $db1['body']['type']);
|
||||
// Validate database response model fields on create
|
||||
$this->assertArrayHasKey('$id', $db1['body']);
|
||||
$this->assertArrayHasKey('$createdAt', $db1['body']);
|
||||
$this->assertArrayHasKey('$updatedAt', $db1['body']);
|
||||
$this->assertArrayHasKey('enabled', $db1['body']);
|
||||
|
||||
$db2 = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'databaseId' => ID::custom('second'),
|
||||
'name' => 'Test 2',
|
||||
]);
|
||||
$this->assertEquals(201, $db2['headers']['status-code']);
|
||||
$this->assertEquals('Test 2', $db2['body']['name']);
|
||||
$this->assertEquals('vectordb', $db2['body']['type']);
|
||||
|
||||
$list = $this->client->call(Client::METHOD_GET, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $list['headers']['status-code']);
|
||||
$this->assertIsInt($list['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(2, $list['body']['total']);
|
||||
$this->assertIsArray($list['body']['databases']);
|
||||
$this->assertArrayHasKey('$id', $list['body']['databases'][0]);
|
||||
$this->assertArrayHasKey('name', $list['body']['databases'][0]);
|
||||
$this->assertArrayHasKey('type', $list['body']['databases'][0]);
|
||||
|
||||
return ['databaseId' => $db1['body']['$id']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testListDatabases
|
||||
*/
|
||||
public function testGetDatabase(array $data): array
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$res = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $res['headers']['status-code']);
|
||||
$this->assertEquals($databaseId, $res['body']['$id']);
|
||||
$this->assertEquals('Test 1', $res['body']['name']);
|
||||
$this->assertEquals('vectordb', $res['body']['type']);
|
||||
return ['databaseId' => $databaseId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testListDatabases
|
||||
*/
|
||||
public function testUpdateDatabase(array $data): array
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$res = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Test 1 Updated',
|
||||
]);
|
||||
$this->assertEquals(200, $res['headers']['status-code']);
|
||||
$this->assertEquals('Test 1 Updated', $res['body']['name']);
|
||||
$this->assertEquals('vectordb', $res['body']['type']);
|
||||
return ['databaseId' => $databaseId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testListDatabases
|
||||
*/
|
||||
public function testDeleteDatabase(array $data): void
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$del = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(204, $del['headers']['status-code']);
|
||||
$this->assertEquals("", $del['body']);
|
||||
|
||||
$get = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(404, $get['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCollectionsCRUD(): array
|
||||
{
|
||||
// Create database for collections tests
|
||||
$database = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Collections DB',
|
||||
]);
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
// Create two collections
|
||||
$col1 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Test 1',
|
||||
'collectionId' => ID::custom('first'),
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
'dimensions' => 3,
|
||||
]);
|
||||
$this->assertEquals(201, $col1['headers']['status-code']);
|
||||
// Validate collection response model on create
|
||||
$this->assertArrayHasKey('$id', $col1['body']);
|
||||
$this->assertArrayHasKey('$createdAt', $col1['body']);
|
||||
$this->assertArrayHasKey('$updatedAt', $col1['body']);
|
||||
$this->assertArrayHasKey('enabled', $col1['body']);
|
||||
$this->assertArrayHasKey('documentSecurity', $col1['body']);
|
||||
$this->assertArrayHasKey('dimensions', $col1['body']);
|
||||
|
||||
$col2 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Test 2',
|
||||
'collectionId' => ID::custom('second'),
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
'dimensions' => 3,
|
||||
]);
|
||||
$this->assertEquals(201, $col2['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$id', $col2['body']);
|
||||
$this->assertArrayHasKey('$createdAt', $col2['body']);
|
||||
$this->assertArrayHasKey('$updatedAt', $col2['body']);
|
||||
|
||||
// List collections
|
||||
$list = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $list['headers']['status-code']);
|
||||
$this->assertIsInt($list['body']['total']);
|
||||
$this->assertGreaterThanOrEqual(2, $list['body']['total']);
|
||||
$this->assertIsArray($list['body']['collections']);
|
||||
$this->assertArrayHasKey('$id', $list['body']['collections'][0]);
|
||||
$this->assertArrayHasKey('name', $list['body']['collections'][0]);
|
||||
$this->assertArrayHasKey('dimensions', $list['body']['collections'][0]);
|
||||
|
||||
// Get collection
|
||||
$get = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertEquals($col1['body']['$id'], $get['body']['$id']);
|
||||
$this->assertEquals('Test 1', $get['body']['name']);
|
||||
$this->assertEquals(3, $get['body']['dimensions']);
|
||||
|
||||
// Update collection (name only)
|
||||
$upd = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $col1['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Test 1 Updated',
|
||||
]);
|
||||
$this->assertEquals(200, $upd['headers']['status-code']);
|
||||
$this->assertEquals('Test 1 Updated', $upd['body']['name']);
|
||||
$this->assertArrayHasKey('$updatedAt', $upd['body']);
|
||||
|
||||
// Delete collection
|
||||
$del = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/' . $col2['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(204, $del['headers']['status-code']);
|
||||
$this->assertEquals("", $del['body']);
|
||||
|
||||
return [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $col1['body']['$id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCollectionsCRUD
|
||||
*/
|
||||
public function testUpdateCollectionMore(array $data): array
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$collectionId = $data['collectionId'];
|
||||
|
||||
// Update collection name and dimensions
|
||||
$upd = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Test 1 Renamed',
|
||||
'dimensions' => 4,
|
||||
]);
|
||||
$this->assertEquals(200, $upd['headers']['status-code']);
|
||||
$this->assertEquals('Test 1 Renamed', $upd['body']['name']);
|
||||
$this->assertEquals(4, $upd['body']['dimensions']);
|
||||
|
||||
// Read back to confirm
|
||||
$get = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertEquals('Test 1 Renamed', $get['body']['name']);
|
||||
$this->assertEquals(4, $get['body']['dimensions']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCollectionsCRUD
|
||||
*/
|
||||
public function testUpdateCollectionEnabledFlag(array $data): array
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$collectionId = $data['collectionId'];
|
||||
|
||||
// Disable collection
|
||||
$disable = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Updated',
|
||||
'enabled' => false,
|
||||
]);
|
||||
$this->assertEquals(200, $disable['headers']['status-code']);
|
||||
$this->assertFalse($disable['body']['enabled']);
|
||||
|
||||
// Re-enable collection
|
||||
$enable = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $collectionId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Updated',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertEquals(200, $enable['headers']['status-code']);
|
||||
$this->assertTrue($enable['body']['enabled']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function testUpdateDatabaseNameAndEnabled(): void
|
||||
{
|
||||
// Create isolated database for this test to avoid ordering conflicts
|
||||
$create = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Update DB',
|
||||
]);
|
||||
$this->assertEquals(201, $create['headers']['status-code']);
|
||||
$databaseId = $create['body']['$id'];
|
||||
|
||||
// Update name
|
||||
$rename = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Test DB Renamed',
|
||||
]);
|
||||
$this->assertEquals(200, $rename['headers']['status-code']);
|
||||
$this->assertEquals('Test DB Renamed', $rename['body']['name']);
|
||||
|
||||
// Toggle enabled off then on
|
||||
$disable = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Test DB Renamed',
|
||||
'enabled' => false,
|
||||
]);
|
||||
$this->assertEquals(200, $disable['headers']['status-code']);
|
||||
$this->assertFalse($disable['body']['enabled']);
|
||||
|
||||
$enable = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'name' => 'Test DB Renamed',
|
||||
'enabled' => true,
|
||||
]);
|
||||
$this->assertEquals(200, $enable['headers']['status-code']);
|
||||
$this->assertTrue($enable['body']['enabled']);
|
||||
|
||||
// Cleanup
|
||||
$del = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(204, $del['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCollectionsCRUD
|
||||
*/
|
||||
public function testRecreateIndex(array $data): void
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$collectionId = $data['collectionId'];
|
||||
|
||||
// Create a new index variant
|
||||
$create = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'embedding_euclidean_v2',
|
||||
'type' => Database::INDEX_HNSW_EUCLIDEAN,
|
||||
'attributes' => ['embeddings']
|
||||
]);
|
||||
$this->assertEquals(202, $create['headers']['status-code']);
|
||||
|
||||
// Ensure it exists
|
||||
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertEquals('embedding_euclidean_v2', $get['body']['key']);
|
||||
|
||||
// Delete it
|
||||
$del = $this->client->call(Client::METHOD_DELETE, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean_v2", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(204, $del['headers']['status-code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCollectionsCRUD
|
||||
*/
|
||||
public function testIndexesCRUD(array $data): void
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
$collectionId = $data['collectionId'];
|
||||
|
||||
// Create indexes
|
||||
$eu = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'embedding_euclidean',
|
||||
'type' => Database::INDEX_HNSW_EUCLIDEAN,
|
||||
'attributes' => ['embeddings']
|
||||
]);
|
||||
$this->assertEquals(202, $eu['headers']['status-code']);
|
||||
|
||||
$dot = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'embedding_dot',
|
||||
'type' => Database::INDEX_HNSW_DOT,
|
||||
'attributes' => ['embeddings']
|
||||
]);
|
||||
$this->assertEquals(202, $dot['headers']['status-code']);
|
||||
|
||||
$cos = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'key' => 'embedding_cosine',
|
||||
'type' => Database::INDEX_HNSW_COSINE,
|
||||
'attributes' => ['embeddings']
|
||||
]);
|
||||
$this->assertEquals(202, $cos['headers']['status-code']);
|
||||
|
||||
// List indexes
|
||||
$list = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $list['headers']['status-code']);
|
||||
$this->assertIsArray($list['body']['indexes']);
|
||||
$keys = array_map(fn ($i) => $i['key'], $list['body']['indexes']);
|
||||
$this->assertContains('embedding_euclidean', $keys);
|
||||
$this->assertContains('embedding_dot', $keys);
|
||||
$this->assertContains('embedding_cosine', $keys);
|
||||
|
||||
// Get index by key
|
||||
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_euclidean", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertEquals('embedding_euclidean', $get['body']['key']);
|
||||
$this->assertEquals(Database::INDEX_HNSW_EUCLIDEAN, $get['body']['type']);
|
||||
|
||||
// Delete index
|
||||
$del = $this->client->call(Client::METHOD_DELETE, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(204, $del['headers']['status-code']);
|
||||
sleep(4);
|
||||
// Ensure it's gone
|
||||
$getMissing = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/indexes/embedding_dot", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(404, $getMissing['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testBulkCreate(): array
|
||||
{
|
||||
// Setup: create isolated database and collection
|
||||
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'BulkDBCreate'
|
||||
]);
|
||||
$this->assertEquals(201, $db['headers']['status-code']);
|
||||
$databaseId = $db['body']['$id'];
|
||||
|
||||
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'BulkColCreate',
|
||||
'documentSecurity' => true,
|
||||
'dimensions' => 3,
|
||||
'permissions' => [Permission::read(Role::any())]
|
||||
]);
|
||||
$this->assertEquals(201, $col['headers']['status-code']);
|
||||
$collectionId = $col['body']['$id'];
|
||||
|
||||
$docs = [
|
||||
[
|
||||
'embeddings' => [1.0, 0.0, 0.0],
|
||||
'metadata' => ['group' => 'bulkA'],
|
||||
'$permissions' => [Permission::read(Role::any())]
|
||||
],
|
||||
[
|
||||
'embeddings' => [0.0, 1.0, 0.0],
|
||||
'metadata' => ['group' => 'bulkB'],
|
||||
'$permissions' => [Permission::read(Role::any())]
|
||||
],
|
||||
];
|
||||
|
||||
$res = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => $docs
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $res['headers']['status-code']);
|
||||
$this->assertIsInt($res['body']['total'] ?? 0);
|
||||
$this->assertGreaterThanOrEqual(2, $res['body']['total']);
|
||||
$this->assertIsArray($res['body']['documents']);
|
||||
$this->assertCount(2, $res['body']['documents']);
|
||||
|
||||
$ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
|
||||
$this->assertNotEmpty($ids[0]);
|
||||
$this->assertNotEmpty($ids[1]);
|
||||
|
||||
// Fetch and validate persisted data via GET
|
||||
foreach ($ids as $i => $id) {
|
||||
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertEquals($id, $get['body']['$id']);
|
||||
$this->assertIsArray($get['body']['embeddings']);
|
||||
$this->assertCount(3, $get['body']['embeddings']);
|
||||
$this->assertArrayHasKey('group', $get['body']['metadata']);
|
||||
}
|
||||
|
||||
return [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'bulkIds' => $ids ];
|
||||
}
|
||||
|
||||
public function testCreateTextEmbeddingsSuccessAndErrors(): void
|
||||
{
|
||||
// Setup new database and collection
|
||||
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'EmbedDB',
|
||||
]);
|
||||
$this->assertEquals(201, $db['headers']['status-code']);
|
||||
$databaseId = $db['body']['$id'];
|
||||
|
||||
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'EmbedCol',
|
||||
'documentSecurity' => true,
|
||||
'dimensions' => 3,
|
||||
'permissions' => [Permission::read(Role::any())]
|
||||
]);
|
||||
$this->assertEquals(201, $col['headers']['status-code']);
|
||||
$collectionId = $col['body']['$id'];
|
||||
|
||||
// Success: two embeddings
|
||||
$ok = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/text", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => [
|
||||
['text' => 'hello world', 'embeddingModel' => 'embeddinggemma'],
|
||||
['text' => 'second sentence', 'embeddingModel' => 'embeddinggemma'],
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $ok['headers']['status-code']);
|
||||
$this->assertIsInt($ok['body']['total'] ?? 0);
|
||||
$this->assertEquals(2, $ok['body']['total']);
|
||||
$this->assertIsArray($ok['body']['embeddings']);
|
||||
$this->assertCount(2, $ok['body']['embeddings']);
|
||||
foreach ($ok['body']['embeddings'] as $embed) {
|
||||
$this->assertIsString($embed['model']);
|
||||
$this->assertIsInt($embed['dimensions']);
|
||||
$this->assertIsArray($embed['embeddings']);
|
||||
$this->assertGreaterThan(0, count($embed['embeddings']));
|
||||
}
|
||||
|
||||
// Error: missing documents
|
||||
$missingDocs = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/text", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], []);
|
||||
$this->assertEquals(400, $missingDocs['headers']['status-code']);
|
||||
|
||||
// Error: invalid item structure (not an object)
|
||||
$invalidItem = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/text", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => [ 'oops' ]
|
||||
]);
|
||||
$this->assertEquals(400, $invalidItem['headers']['status-code']);
|
||||
|
||||
// Error: missing text
|
||||
$missingText = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/text", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => [ ['embeddingModel' => 'embeddinggemma'] ]
|
||||
]);
|
||||
$this->assertEquals(400, $missingText['headers']['status-code']);
|
||||
|
||||
// Error: missing embeddingModel
|
||||
$missingModel = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/text", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => [ ['text' => 'no model'] ]
|
||||
]);
|
||||
$this->assertEquals(400, $missingModel['headers']['status-code']);
|
||||
|
||||
// Error: unknown embedding model
|
||||
$unknownModel = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections/{$collectionId}/text", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => [ ['text' => 'hello', 'embeddingModel' => 'nonexistent-model'] ]
|
||||
]);
|
||||
$this->assertEquals(400, $unknownModel['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testBulkUpsert(): void
|
||||
{
|
||||
// Setup fresh db/collection
|
||||
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpsert' ]);
|
||||
$this->assertEquals(201, $db['headers']['status-code']);
|
||||
$databaseId = $db['body']['$id'];
|
||||
|
||||
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'BulkColUpsert',
|
||||
'documentSecurity' => true,
|
||||
'dimensions' => 3,
|
||||
'permissions' => [Permission::read(Role::any())]
|
||||
]);
|
||||
$this->assertEquals(201, $col['headers']['status-code']);
|
||||
$collectionId = $col['body']['$id'];
|
||||
|
||||
$docs = [
|
||||
[
|
||||
'embeddings' => [0.5, 0.5, 0.0],
|
||||
'metadata' => ['group' => 'bulkA', 'updated' => true],
|
||||
'$permissions' => [Permission::read(Role::any())]
|
||||
],
|
||||
[
|
||||
'embeddings' => [0.2, 0.8, 0.0],
|
||||
'metadata' => ['group' => 'bulkB', 'updated' => true],
|
||||
'$permissions' => [Permission::read(Role::any())]
|
||||
],
|
||||
];
|
||||
|
||||
$res = $this->client->call(Client::METHOD_PUT, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => $docs
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $res['headers']['status-code']);
|
||||
$this->assertIsArray($res['body']['documents']);
|
||||
$this->assertCount(2, $res['body']['documents']);
|
||||
$this->assertTrue($res['body']['documents'][0]['metadata']['updated']);
|
||||
$this->assertTrue($res['body']['documents'][1]['metadata']['updated']);
|
||||
|
||||
// Fetch and validate updated content
|
||||
$ids = array_map(fn ($d) => $d['$id'], $res['body']['documents']);
|
||||
foreach ($ids as $id) {
|
||||
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertTrue($get['body']['metadata']['updated']);
|
||||
}
|
||||
|
||||
// Perform another bulk upsert to mutate the same documents
|
||||
$docs2 = [
|
||||
[ 'embeddings' => [0.6, 0.4, 0.0], 'metadata' => ['updatedAgain' => true] ],
|
||||
[ 'embeddings' => [0.3, 0.7, 0.0], 'metadata' => ['updatedAgain' => true] ],
|
||||
];
|
||||
$res2 = $this->client->call(Client::METHOD_PUT, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => $docs2
|
||||
]);
|
||||
$this->assertEquals(200, $res2['headers']['status-code']);
|
||||
$this->assertIsArray($res2['body']['documents']);
|
||||
$this->assertCount(2, $res2['body']['documents']);
|
||||
|
||||
// Fetch again and assert second update persisted
|
||||
$ids2 = array_map(fn ($d) => $d['$id'], $res2['body']['documents']);
|
||||
foreach ($ids2 as $id) {
|
||||
$get2 = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $get2['headers']['status-code']);
|
||||
$this->assertTrue($get2['body']['metadata']['updatedAgain']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testBulkUpdate(): void
|
||||
{
|
||||
// Setup: create db/collection and two docs
|
||||
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBUpdate' ]);
|
||||
$this->assertEquals(201, $db['headers']['status-code']);
|
||||
$databaseId = $db['body']['$id'];
|
||||
|
||||
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'BulkColUpdate',
|
||||
'documentSecurity' => true,
|
||||
'dimensions' => 3,
|
||||
'permissions' => [Permission::read(Role::any())]
|
||||
]);
|
||||
$this->assertEquals(201, $col['headers']['status-code']);
|
||||
$collectionId = $col['body']['$id'];
|
||||
|
||||
$seed = $this->client->call(Client::METHOD_PUT, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => [
|
||||
['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
|
||||
['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $seed['headers']['status-code']);
|
||||
$ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
|
||||
|
||||
$res = $this->client->call(Client::METHOD_PATCH, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'data' => [ 'metadata' => ['bulkUpdated' => true] ],
|
||||
'queries' => [
|
||||
\Utopia\Database\Query::equal('$id', $ids)->toString()
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $res['headers']['status-code']);
|
||||
$this->assertIsArray($res['body']['documents']);
|
||||
$this->assertCount(2, $res['body']['documents']);
|
||||
foreach ($res['body']['documents'] as $doc) {
|
||||
$this->assertTrue($doc['metadata']['bulkUpdated']);
|
||||
}
|
||||
|
||||
// Fetch by IDs and assert update persisted
|
||||
foreach ($ids as $id) {
|
||||
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(200, $get['headers']['status-code']);
|
||||
$this->assertTrue($get['body']['metadata']['bulkUpdated']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testBulkDelete(): void
|
||||
{
|
||||
// Setup: create db/collection and two docs
|
||||
$db = $this->client->call(Client::METHOD_POST, '/vectordb', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [ 'databaseId' => ID::unique(), 'name' => 'BulkDBDelete' ]);
|
||||
$this->assertEquals(201, $db['headers']['status-code']);
|
||||
$databaseId = $db['body']['$id'];
|
||||
|
||||
$col = $this->client->call(Client::METHOD_POST, "/vectordb/{$databaseId}/collections", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'BulkColDelete',
|
||||
'documentSecurity' => true,
|
||||
'dimensions' => 3,
|
||||
'permissions' => [Permission::read(Role::any())]
|
||||
]);
|
||||
$this->assertEquals(201, $col['headers']['status-code']);
|
||||
$collectionId = $col['body']['$id'];
|
||||
|
||||
$seed = $this->client->call(Client::METHOD_PUT, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'documents' => [
|
||||
['embeddings' => [1.0,0.0,0.0], 'metadata' => ['seed' => 1], '$permissions' => [Permission::read(Role::any())]],
|
||||
['embeddings' => [0.0,1.0,0.0], 'metadata' => ['seed' => 2], '$permissions' => [Permission::read(Role::any())]]
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $seed['headers']['status-code']);
|
||||
$ids = array_map(fn ($d) => $d['$id'], $seed['body']['documents']);
|
||||
|
||||
$res = $this->client->call(Client::METHOD_DELETE, "/vectordb/{$databaseId}/collections/{$collectionId}/documents", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
], [
|
||||
'queries' => [
|
||||
\Utopia\Database\Query::equal('$id', $ids)->toString()
|
||||
]
|
||||
]);
|
||||
$this->assertEquals(200, $res['headers']['status-code']);
|
||||
$this->assertIsInt($res['body']['total'] ?? 0);
|
||||
$this->assertGreaterThanOrEqual(2, $res['body']['total']);
|
||||
|
||||
// Ensure they are deleted
|
||||
foreach ($ids as $id) {
|
||||
$get = $this->client->call(Client::METHOD_GET, "/vectordb/{$databaseId}/collections/{$collectionId}/documents/{$id}", [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]);
|
||||
$this->assertEquals(404, $get['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB\Permissions;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
|
||||
class DatabasesPermissionsGuestTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
use DatabasesPermissionsScope;
|
||||
|
||||
public function createCollection(): array
|
||||
{
|
||||
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'VectorGuestDB',
|
||||
]);
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
$this->assertEquals('VectorGuestDB', $database['body']['name']);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
$publicMovies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Movies',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
]);
|
||||
$privateMovies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Movies',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
|
||||
$publicCollection = ['id' => $publicMovies['body']['$id']];
|
||||
$privateCollection = ['id' => $privateMovies['body']['$id']];
|
||||
|
||||
return [
|
||||
'databaseId' => $databaseId,
|
||||
'publicCollectionId' => $publicCollection['id'],
|
||||
'privateCollectionId' => $privateCollection['id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function permissionsProvider(): array
|
||||
{
|
||||
return [
|
||||
[[Permission::read(Role::any())]],
|
||||
[[Permission::read(Role::users())]],
|
||||
[[Permission::update(Role::any()), Permission::delete(Role::any())]],
|
||||
[[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]],
|
||||
[[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())]],
|
||||
[[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider permissionsProvider
|
||||
*/
|
||||
public function testReadDocuments($permissions)
|
||||
{
|
||||
$data = $this->createCollection();
|
||||
$publicCollectionId = $data['publicCollectionId'];
|
||||
$privateCollectionId = $data['privateCollectionId'];
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
$publicResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [1.0, 0.0, 0.0],
|
||||
'metadata' => ['title' => 'Lorem'],
|
||||
],
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
$privateResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [0.0, 1.0, 0.0],
|
||||
'metadata' => ['title' => 'Lorem'],
|
||||
],
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $publicResponse['headers']['status-code']);
|
||||
$this->assertEquals(201, $privateResponse['headers']['status-code']);
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
Authorization::cleanRoles();
|
||||
|
||||
$publicDocuments = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]);
|
||||
$privateDocuments = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(1, $publicDocuments['body']['total']);
|
||||
$this->assertEquals($permissions, $publicDocuments['body']['documents'][0]['$permissions']);
|
||||
|
||||
if (\in_array(Permission::read(Role::any()), $permissions)) {
|
||||
$this->assertEquals(1, $privateDocuments['body']['total']);
|
||||
$this->assertEquals($permissions, $privateDocuments['body']['documents'][0]['$permissions']);
|
||||
} else {
|
||||
$this->assertEquals(0, $privateDocuments['body']['total']);
|
||||
}
|
||||
|
||||
foreach ($roles as $role) {
|
||||
Authorization::setRole($role);
|
||||
}
|
||||
}
|
||||
|
||||
public function testWriteDocument()
|
||||
{
|
||||
$data = $this->createCollection();
|
||||
$publicCollectionId = $data['publicCollectionId'];
|
||||
$privateCollectionId = $data['privateCollectionId'];
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
Authorization::cleanRoles();
|
||||
|
||||
$publicResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [1.0, 0.0, 0.0],
|
||||
'metadata' => ['title' => 'Lorem'],
|
||||
]
|
||||
]);
|
||||
|
||||
$publicDocumentId = $publicResponse['body']['$id'];
|
||||
$this->assertEquals(201, $publicResponse['headers']['status-code']);
|
||||
|
||||
$privateResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [0.0, 1.0, 0.0],
|
||||
'metadata' => ['title' => 'Lorem'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(401, $privateResponse['headers']['status-code']);
|
||||
|
||||
// Create a document in private collection with API key so we can test that update and delete are also not allowed
|
||||
$privateResponse = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [0.0, 0.0, 1.0],
|
||||
'metadata' => ['title' => 'Lorem'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $privateResponse['headers']['status-code']);
|
||||
$privateDocumentId = $privateResponse['body']['$id'];
|
||||
|
||||
$publicDocument = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents/' . $publicDocumentId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'data' => [
|
||||
'embeddings' => [0.5, 0.5, 0.0],
|
||||
'metadata' => ['title' => 'Thor: Ragnarok'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $publicDocument['headers']['status-code']);
|
||||
$this->assertEquals('Thor: Ragnarok', $publicDocument['body']['metadata']['title']);
|
||||
|
||||
$privateDocument = $this->client->call(Client::METHOD_PUT, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents/' . $privateDocumentId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'data' => [
|
||||
'embeddings' => [0.2, 0.3, 0.5],
|
||||
'metadata' => ['title' => 'Thor: Ragnarok'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(401, $privateDocument['headers']['status-code']);
|
||||
|
||||
$publicDocument = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/' . $publicCollectionId . '/documents/' . $publicDocumentId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(204, $publicDocument['headers']['status-code']);
|
||||
|
||||
$privateDocument = $this->client->call(Client::METHOD_DELETE, '/vectordb/' . $databaseId . '/collections/' . $privateCollectionId . '/documents/' . $privateDocumentId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(401, $privateDocument['headers']['status-code']);
|
||||
|
||||
foreach ($roles as $role) {
|
||||
Authorization::setRole($role);
|
||||
}
|
||||
}
|
||||
|
||||
public function testWriteDocumentWithPermissions()
|
||||
{
|
||||
$database = $this->client->call(Client::METHOD_POST, '/vectordb', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'VectorGuestPermsWrite',
|
||||
]);
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
$this->assertEquals('VectorGuestPermsWrite', $database['body']['name']);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
$movies = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Movies',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
],
|
||||
'documentSecurity' => true
|
||||
]);
|
||||
|
||||
$moviesId = $movies['body']['$id'];
|
||||
|
||||
$document = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $moviesId . '/documents', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [1.0, 0.0, 0.0],
|
||||
'metadata' => ['title' => 'Thor: Ragnarok'],
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $document['headers']['status-code']);
|
||||
$this->assertEquals('Thor: Ragnarok', $document['body']['metadata']['title']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB\Permissions;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class DatabasesPermissionsMemberTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
use DatabasesPermissionsScope;
|
||||
|
||||
public array $collections = [];
|
||||
|
||||
public function createUsers(): array
|
||||
{
|
||||
return [
|
||||
'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
|
||||
'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
|
||||
];
|
||||
}
|
||||
|
||||
public function permissionsProvider(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'permissions' => [Permission::read(Role::any())],
|
||||
'any' => 1,
|
||||
'users' => 1,
|
||||
'doconly' => 1,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::users())],
|
||||
'any' => 2,
|
||||
'users' => 2,
|
||||
'doconly' => 2,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::user(ID::custom('random')))],
|
||||
'any' => 3,
|
||||
'users' => 3,
|
||||
'doconly' => 2,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))],
|
||||
'any' => 4,
|
||||
'users' => 4,
|
||||
'doconly' => 2,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))],
|
||||
'any' => 5,
|
||||
'users' => 5,
|
||||
'doconly' => 2,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))],
|
||||
'any' => 6,
|
||||
'users' => 6,
|
||||
'doconly' => 2,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::update(Role::any()), Permission::delete(Role::any())],
|
||||
'any' => 7,
|
||||
'users' => 7,
|
||||
'doconly' => 2,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())],
|
||||
'any' => 8,
|
||||
'users' => 8,
|
||||
'doconly' => 3,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())],
|
||||
'any' => 9,
|
||||
'users' => 9,
|
||||
'doconly' => 4,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::user(ID::custom('user1')))],
|
||||
'any' => 10,
|
||||
'users' => 10,
|
||||
'doconly' => 5,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::user(ID::custom('user1'))), Permission::read(Role::user(ID::custom('user1')))],
|
||||
'any' => 11,
|
||||
'users' => 11,
|
||||
'doconly' => 6,
|
||||
],
|
||||
[
|
||||
'permissions' => [Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())],
|
||||
'any' => 12,
|
||||
'users' => 12,
|
||||
'doconly' => 7,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup database
|
||||
*
|
||||
* Data providers lose object state so explicitly pass [$users, $collections] to each iteration
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSetupDatabase(): array
|
||||
{
|
||||
$this->createUsers();
|
||||
|
||||
$db = $this->client->call(Client::METHOD_POST, '/vectordb', $this->getServerHeader(), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Test Database',
|
||||
]);
|
||||
$this->assertEquals(201, $db['headers']['status-code']);
|
||||
|
||||
$databaseId = $db['body']['$id'];
|
||||
|
||||
$public = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Movies',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
$this->assertEquals(201, $public['headers']['status-code']);
|
||||
$this->collections = ['public' => $public['body']['$id']];
|
||||
|
||||
$private = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Private Movies',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [
|
||||
Permission::read(Role::users()),
|
||||
Permission::create(Role::users()),
|
||||
Permission::update(Role::users()),
|
||||
Permission::delete(Role::users()),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
$this->assertEquals(201, $private['headers']['status-code']);
|
||||
$this->collections['private'] = $private['body']['$id'];
|
||||
|
||||
$doconly = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections', $this->getServerHeader(), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Document Only Movies',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
$this->assertEquals(201, $private['headers']['status-code']);
|
||||
$this->collections['doconly'] = $doconly['body']['$id'];
|
||||
|
||||
return [
|
||||
'users' => $this->users,
|
||||
'collections' => $this->collections,
|
||||
'databaseId' => $databaseId
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider params are passed before test dependencies
|
||||
* @dataProvider permissionsProvider
|
||||
* @depends testSetupDatabase
|
||||
*/
|
||||
public function testReadDocuments($permissions, $anyCount, $usersCount, $docOnlyCount, $data)
|
||||
{
|
||||
$users = $data['users'];
|
||||
$collections = $data['collections'];
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $collections['public'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [1.0, 0.0, 0.0],
|
||||
'metadata' => ['title' => 'Lorem'],
|
||||
],
|
||||
'permissions' => $permissions
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $collections['private'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [0.0, 1.0, 0.0],
|
||||
'metadata' => ['title' => 'Lorem'],
|
||||
],
|
||||
'permissions' => $permissions
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [0.0, 0.0, 1.0],
|
||||
'metadata' => ['title' => 'Lorem'],
|
||||
],
|
||||
'permissions' => $permissions
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Check "any" permission collection
|
||||
*/
|
||||
$documents = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/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'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $documents['headers']['status-code']);
|
||||
$this->assertEquals($anyCount, $documents['body']['total']);
|
||||
|
||||
/**
|
||||
* Check "users" permission collection
|
||||
*/
|
||||
$documents = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/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'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $documents['headers']['status-code']);
|
||||
$this->assertEquals($usersCount, $documents['body']['total']);
|
||||
|
||||
/**
|
||||
* Check "user:user1" document only permission collection
|
||||
*/
|
||||
$documents = $this->client->call(Client::METHOD_GET, '/vectordb/' . $databaseId . '/collections/' . $collections['doconly'] . '/documents', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $documents['headers']['status-code']);
|
||||
$this->assertEquals($docOnlyCount, $documents['body']['total']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB\Permissions;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
|
||||
trait DatabasesPermissionsScope
|
||||
{
|
||||
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'],
|
||||
'x-appwrite-dev-key' => $this->getProject()['devKey'] ?? '',
|
||||
], [
|
||||
'userId' => $id,
|
||||
'email' => $email,
|
||||
'password' => $password
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $user['headers']['status-code']);
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$session = $session['cookies']['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']
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\E2E\Services\Databases\VectorDB\Permissions;
|
||||
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
use Tests\E2E\Scopes\SideClient;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
|
||||
class DatabasesPermissionsTeamTest extends Scope
|
||||
{
|
||||
use ProjectCustom;
|
||||
use SideClient;
|
||||
use DatabasesPermissionsScope;
|
||||
|
||||
public array $collections = [];
|
||||
public string $databaseId = 'testpermissiondb';
|
||||
|
||||
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)
|
||||
{
|
||||
$db = $this->client->call(Client::METHOD_POST, '/vectordb', $this->getServerHeader(), [
|
||||
'databaseId' => $this->databaseId,
|
||||
'name' => 'Test Database',
|
||||
]);
|
||||
$this->assertEquals(201, $db['headers']['status-code']);
|
||||
|
||||
$collection1 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/collections', $this->getServerHeader(), [
|
||||
'collectionId' => ID::custom('collection1'),
|
||||
'name' => 'Collection 1',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [
|
||||
Permission::read(Role::team($teams['team1']['$id'])),
|
||||
Permission::create(Role::team($teams['team1']['$id'], 'admin')),
|
||||
Permission::update(Role::team($teams['team1']['$id'], 'admin')),
|
||||
Permission::delete(Role::team($teams['team1']['$id'], 'admin')),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->collections['collection1'] = $collection1['body']['$id'];
|
||||
|
||||
$collection2 = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/collections', $this->getServerHeader(), [
|
||||
'collectionId' => ID::custom('collection2'),
|
||||
'name' => 'Collection 2',
|
||||
'dimensions' => 3,
|
||||
'permissions' => [
|
||||
Permission::read(Role::team($teams['team2']['$id'])),
|
||||
Permission::create(Role::team($teams['team2']['$id'], 'owner')),
|
||||
Permission::update(Role::team($teams['team2']['$id'], 'owner')),
|
||||
Permission::delete(Role::team($teams['team2']['$id'], 'owner')),
|
||||
]
|
||||
]);
|
||||
|
||||
$this->collections['collection2'] = $collection2['body']['$id'];
|
||||
|
||||
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, '/vectordb/' . $this->databaseId . '/collections/' . $this->collections['collection1'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [1.0, 0.0, 0.0],
|
||||
'metadata' => ['title' => 'Lorem'],
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/collections/' . $this->collections['collection2'] . '/documents', $this->getServerHeader(), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [0.0, 1.0, 0.0],
|
||||
'metadata' => ['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, '/vectordb/' . $this->databaseId . '/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(401, $documents['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testSetupDatabase
|
||||
* @dataProvider writeDocumentsProvider
|
||||
*/
|
||||
public function testWriteDocuments($user, $collection, $success, $users)
|
||||
{
|
||||
$documents = $this->client->call(Client::METHOD_POST, '/vectordb/' . $this->databaseId . '/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' => ID::unique(),
|
||||
'data' => [
|
||||
'embeddings' => [0.2, 0.3, 0.5],
|
||||
'metadata' => ['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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user