mirror of
https://github.com/appwrite/appwrite
synced 2026-05-06 06:48:22 +00:00
Add attributes + indexes params
This commit is contained in:
parent
2bd308c236
commit
2a88341edc
5 changed files with 694 additions and 3 deletions
|
|
@ -105,4 +105,64 @@ abstract class Action extends UtopiaAction
|
|||
? Exception::COLLECTION_LIMIT_EXCEEDED
|
||||
: Exception::TABLE_LIMIT_EXCEEDED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate format unsupported exception.
|
||||
*/
|
||||
protected function getFormatUnsupportedException(): string
|
||||
{
|
||||
return $this->isCollectionsAPI()
|
||||
? Exception::ATTRIBUTE_FORMAT_UNSUPPORTED
|
||||
: Exception::COLUMN_FORMAT_UNSUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct default unsupported message.
|
||||
*/
|
||||
protected function getDefaultUnsupportedException(): string
|
||||
{
|
||||
return $this->isCollectionsAPI()
|
||||
? Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED
|
||||
: Exception::COLUMN_DEFAULT_UNSUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate parent level not found exception.
|
||||
*/
|
||||
protected function getParentNotFoundException(): string
|
||||
{
|
||||
return $this->isCollectionsAPI()
|
||||
? Exception::COLLECTION_NOT_FOUND
|
||||
: Exception::TABLE_NOT_FOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct invalid structure message.
|
||||
*/
|
||||
protected function getStructureException(): string
|
||||
{
|
||||
return $this->isCollectionsAPI()
|
||||
? Exception::DOCUMENT_INVALID_STRUCTURE
|
||||
: Exception::ROW_INVALID_STRUCTURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the exception for unknown attribute/column in index.
|
||||
*/
|
||||
protected function getParentUnknownException(): string
|
||||
{
|
||||
return $this->isCollectionsAPI()
|
||||
? Exception::ATTRIBUTE_UNKNOWN
|
||||
: Exception::COLUMN_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the exception for invalid attribute/column type in index.
|
||||
*/
|
||||
protected function getParentInvalidTypeException(): string
|
||||
{
|
||||
return $this->isCollectionsAPI()
|
||||
? Exception::ATTRIBUTE_TYPE_INVALID
|
||||
: Exception::COLUMN_TYPE_INVALID;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ use Appwrite\SDK\ContentType;
|
|||
use Appwrite\SDK\Deprecated;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Attributes as AttributesValidator;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Indexes as IndexesValidator;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
|
|
@ -17,10 +19,12 @@ use Utopia\Database\Exception\Duplicate as DuplicateException;
|
|||
use Utopia\Database\Exception\Index as IndexException;
|
||||
use Utopia\Database\Exception\Limit as LimitException;
|
||||
use Utopia\Database\Exception\NotFound as NotFoundException;
|
||||
use Utopia\Database\Exception\Structure as StructureException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\Structure;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
|
@ -75,13 +79,15 @@ class Create extends Action
|
|||
->param('permissions', null, new Nullable(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)
|
||||
->param('attributes', [], new AttributesValidator(), 'Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.', true)
|
||||
->param('indexes', [], new IndexesValidator(), 'Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of attribute keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
|
||||
public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, array $attributes, array $indexes, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
|
||||
{
|
||||
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
|
||||
|
|
@ -113,20 +119,53 @@ class Create extends Action
|
|||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$collectionAttributes = [];
|
||||
$attributeDocuments = [];
|
||||
foreach ($attributes as $attributeDef) {
|
||||
$attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef, $dbForProject);
|
||||
$collectionAttributes[] = $attrDoc['collection'];
|
||||
$attributeDocuments[] = $attrDoc['document'];
|
||||
}
|
||||
|
||||
$collectionIndexes = [];
|
||||
$indexDocuments = [];
|
||||
foreach ($indexes as $indexDef) {
|
||||
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes);
|
||||
$collectionIndexes[] = $idxDoc['collection'];
|
||||
$indexDocuments[] = $idxDoc['document'];
|
||||
}
|
||||
|
||||
try {
|
||||
$dbForProject->createCollection(
|
||||
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
|
||||
attributes: $collectionAttributes,
|
||||
indexes: $collectionIndexes,
|
||||
permissions: $permissions,
|
||||
documentSecurity: $documentSecurity
|
||||
);
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
} catch (IndexException) {
|
||||
throw new Exception($this->getInvalidIndexException());
|
||||
} catch (IndexException $e) {
|
||||
throw new Exception($this->getInvalidIndexException(), $e->getMessage());
|
||||
} catch (LimitException) {
|
||||
throw new Exception($this->getLimitException());
|
||||
}
|
||||
|
||||
// Create documents in attributes and indexes collections
|
||||
try {
|
||||
if (!empty($attributeDocuments)) {
|
||||
$dbForProject->createDocuments('attributes', $attributeDocuments);
|
||||
}
|
||||
if (!empty($indexDocuments)) {
|
||||
$dbForProject->createDocuments('indexes', $indexDocuments);
|
||||
}
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception($this->getDuplicateException());
|
||||
}
|
||||
|
||||
$dbForProject->purgeCachedDocument('database_' . $database->getSequence(), $collection->getId());
|
||||
$dbForProject->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
|
||||
|
||||
$queueForEvents
|
||||
->setContext('database', $database)
|
||||
->setParam('databaseId', $databaseId)
|
||||
|
|
@ -136,4 +175,191 @@ class Create extends Action
|
|||
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
|
||||
->dynamic($collection, $this->getResponseModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build attribute Document objects from a definition array
|
||||
*
|
||||
* @return array{collection: Document, document: Document}
|
||||
*/
|
||||
protected function buildAttributeDocument(Document $database, Document $collection, array $attributeDef, Database $dbForProject): array
|
||||
{
|
||||
$key = $attributeDef['key'];
|
||||
$type = $attributeDef['type'];
|
||||
$size = $attributeDef['size'] ?? 0;
|
||||
$required = $attributeDef['required'] ?? false;
|
||||
$signed = $attributeDef['signed'] ?? true;
|
||||
$array = $attributeDef['array'] ?? false;
|
||||
$format = $attributeDef['format'] ?? '';
|
||||
$formatOptions = [];
|
||||
$filters = $attributeDef['filters'] ?? [];
|
||||
$default = $attributeDef['default'] ?? null;
|
||||
$options = [];
|
||||
|
||||
if ($type === Database::VAR_STRING) {
|
||||
if ($size === 0) {
|
||||
$size = 256; // Default size for strings
|
||||
}
|
||||
}
|
||||
|
||||
if ($format === APP_DATABASE_ATTRIBUTE_ENUM && isset($attributeDef['elements'])) {
|
||||
$formatOptions = ['elements' => $attributeDef['elements']];
|
||||
}
|
||||
|
||||
if (isset($attributeDef['min']) && isset($attributeDef['max'])) {
|
||||
$format = $type === Database::VAR_INTEGER ? APP_DATABASE_ATTRIBUTE_INT_RANGE : APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
|
||||
$formatOptions = [
|
||||
'min' => $attributeDef['min'],
|
||||
'max' => $attributeDef['max'],
|
||||
];
|
||||
}
|
||||
|
||||
if ($type === Database::VAR_RELATIONSHIP) {
|
||||
$options = [
|
||||
'relatedCollection' => $attributeDef['relatedCollection'] ?? '',
|
||||
'relationType' => $attributeDef['relationType'] ?? Database::RELATION_ONE_TO_ONE,
|
||||
'twoWay' => $attributeDef['twoWay'] ?? false,
|
||||
'twoWayKey' => $attributeDef['twoWayKey'] ?? '',
|
||||
'onDelete' => $attributeDef['onDelete'] ?? Database::RELATION_MUTATE_RESTRICT,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($format)) {
|
||||
if (!Structure::hasFormat($format, $type)) {
|
||||
throw new Exception($this->getFormatUnsupportedException(), "Format $format not available for $type attributes.");
|
||||
}
|
||||
}
|
||||
|
||||
if ($required && isset($default) && $default !== null) {
|
||||
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for required ' . $this->getContext());
|
||||
}
|
||||
|
||||
if ($array && isset($default) && $default !== null) {
|
||||
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for array ' . $this->getContext() . 's');
|
||||
}
|
||||
|
||||
if ($type === Database::VAR_RELATIONSHIP) {
|
||||
$options['side'] = Database::RELATION_SIDE_PARENT;
|
||||
$relatedCollection = $dbForProject->getDocument('database_' . $database->getSequence(), $options['relatedCollection'] ?? '');
|
||||
if ($relatedCollection->isEmpty()) {
|
||||
$parent = $this->isCollectionsAPI() ? 'collection' : 'table';
|
||||
throw new Exception($this->getParentNotFoundException(), "The related $parent was not found.");
|
||||
}
|
||||
}
|
||||
|
||||
$collectionDoc = new Document([
|
||||
'$id' => $key,
|
||||
'key' => $key,
|
||||
'type' => $type,
|
||||
'size' => $size,
|
||||
'required' => $required,
|
||||
'signed' => $signed,
|
||||
'default' => $default,
|
||||
'array' => $array,
|
||||
'format' => $format,
|
||||
'formatOptions' => $formatOptions,
|
||||
'filters' => $filters,
|
||||
'options' => $options,
|
||||
]);
|
||||
|
||||
$document = new Document([
|
||||
'$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key),
|
||||
'key' => $key,
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'databaseId' => $database->getId(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'collectionId' => $collection->getId(),
|
||||
'type' => $type,
|
||||
'status' => 'available',
|
||||
'size' => $size,
|
||||
'required' => $required,
|
||||
'signed' => $signed,
|
||||
'default' => $default,
|
||||
'array' => $array,
|
||||
'format' => $format,
|
||||
'formatOptions' => $formatOptions,
|
||||
'filters' => $filters,
|
||||
'options' => $options,
|
||||
]);
|
||||
|
||||
return [
|
||||
'collection' => $collectionDoc,
|
||||
'document' => $document,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build index Document objects from a definition array
|
||||
*
|
||||
* @return array{collection: Document, document: Document}
|
||||
*/
|
||||
protected function buildIndexDocument(Document $database, Document $collection, array $indexDef, array $attributeDocuments): array
|
||||
{
|
||||
$key = $indexDef['key'];
|
||||
$type = $indexDef['type'];
|
||||
$indexAttributes = $indexDef['attributes'];
|
||||
$orders = $indexDef['orders'] ?? [];
|
||||
$lengths = $indexDef['lengths'] ?? [];
|
||||
|
||||
$attrKeys = array_map(fn ($a) => $a->getAttribute('key'), $attributeDocuments);
|
||||
|
||||
$systemAttrs = ['$id', '$createdAt', '$updatedAt'];
|
||||
|
||||
foreach ($indexAttributes as $i => $attr) {
|
||||
if (!in_array($attr, $attrKeys) && !in_array($attr, $systemAttrs)) {
|
||||
throw new Exception($this->getParentUnknownException(), "Unknown attribute: " . $attr . ". Verify the attribute name or ensure it's in the attributes list.");
|
||||
}
|
||||
|
||||
$attrIndex = array_search($attr, $attrKeys);
|
||||
if ($attrIndex !== false) {
|
||||
$attrDoc = $attributeDocuments[$attrIndex];
|
||||
$attrType = $attrDoc->getAttribute('type');
|
||||
$attrArray = $attrDoc->getAttribute('array', false);
|
||||
|
||||
if ($attrType === Database::VAR_RELATIONSHIP) {
|
||||
throw new Exception($this->getParentInvalidTypeException(), "Cannot create an index for a relationship attribute: " . $attr);
|
||||
}
|
||||
|
||||
if (empty($lengths[$i])) {
|
||||
$lengths[$i] = null;
|
||||
}
|
||||
|
||||
if ($attrArray === true) {
|
||||
$lengths[$i] = Database::MAX_ARRAY_INDEX_LENGTH;
|
||||
$orders[$i] = null;
|
||||
}
|
||||
} else {
|
||||
if (empty($lengths[$i])) {
|
||||
$lengths[$i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$collectionDoc = new Document([
|
||||
'$id' => $key,
|
||||
'key' => $key,
|
||||
'type' => $type,
|
||||
'attributes' => $indexAttributes,
|
||||
'lengths' => $lengths,
|
||||
'orders' => $orders,
|
||||
]);
|
||||
|
||||
$document = new Document([
|
||||
'$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key),
|
||||
'key' => $key,
|
||||
'status' => 'available',
|
||||
'databaseInternalId' => $database->getSequence(),
|
||||
'databaseId' => $database->getId(),
|
||||
'collectionInternalId' => $collection->getSequence(),
|
||||
'collectionId' => $collection->getId(),
|
||||
'type' => $type,
|
||||
'attributes' => $indexAttributes,
|
||||
'lengths' => $lengths,
|
||||
'orders' => $orders,
|
||||
]);
|
||||
|
||||
return [
|
||||
'collection' => $collectionDoc,
|
||||
'document' => $document,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ use Appwrite\SDK\AuthType;
|
|||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Attributes as AttributesValidator;
|
||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Database\Validator\Indexes as IndexesValidator;
|
||||
use Appwrite\Utopia\Response as UtopiaResponse;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\UID;
|
||||
|
|
@ -60,6 +62,8 @@ class Create extends CollectionCreate
|
|||
->param('permissions', null, new Nullable(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('rowSecurity', false, new Boolean(true), 'Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a row. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
|
||||
->param('enabled', true, new Boolean(), 'Is table enabled? When set to \'disabled\', users cannot access the table but Server SDKs with and API key can still read and write to the table. No data is lost when this is toggled.', true)
|
||||
->param('columns', [], new AttributesValidator(), 'Array of column definitions to create. Each column should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.', true)
|
||||
->param('indexes', [], new IndexesValidator(), 'Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of column keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional).', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
|
|
|
|||
211
src/Appwrite/Utopia/Database/Validator/Attributes.php
Normal file
211
src/Appwrite/Utopia/Database/Validator/Attributes.php
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator;
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Validator;
|
||||
|
||||
class Attributes extends Validator
|
||||
{
|
||||
protected int $maxAttributes;
|
||||
protected string $message = 'Invalid attributes';
|
||||
|
||||
/**
|
||||
* @var array<string> Supported attribute types
|
||||
*/
|
||||
protected array $supportedTypes = [
|
||||
Database::VAR_STRING,
|
||||
Database::VAR_INTEGER,
|
||||
Database::VAR_FLOAT,
|
||||
Database::VAR_BOOLEAN,
|
||||
Database::VAR_DATETIME,
|
||||
Database::VAR_RELATIONSHIP,
|
||||
Database::VAR_POINT,
|
||||
Database::VAR_LINESTRING,
|
||||
Database::VAR_POLYGON,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string> Supported formats for string attributes
|
||||
*/
|
||||
protected array $supportedFormats = [
|
||||
'',
|
||||
APP_DATABASE_ATTRIBUTE_EMAIL,
|
||||
APP_DATABASE_ATTRIBUTE_ENUM,
|
||||
APP_DATABASE_ATTRIBUTE_IP,
|
||||
APP_DATABASE_ATTRIBUTE_URL,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param int $maxAttributes Maximum number of attributes allowed
|
||||
*/
|
||||
public function __construct(int $maxAttributes = APP_LIMIT_ARRAY_PARAMS_SIZE)
|
||||
{
|
||||
$this->maxAttributes = $maxAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
$this->message = 'Attributes must be an array';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($value) > $this->maxAttributes) {
|
||||
$this->message = 'Maximum of ' . $this->maxAttributes . ' attributes allowed';
|
||||
return false;
|
||||
}
|
||||
|
||||
$keyValidator = new Key();
|
||||
$keys = [];
|
||||
|
||||
foreach ($value as $index => $attribute) {
|
||||
if (!is_array($attribute)) {
|
||||
$this->message = "Attribute at index $index must be an object";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!isset($attribute['key'])) {
|
||||
$this->message = "Attribute at index $index is missing required field 'key'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($attribute['type'])) {
|
||||
$this->message = "Attribute at index $index is missing required field 'type'";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate key
|
||||
if (!$keyValidator->isValid($attribute['key'])) {
|
||||
$this->message = "Invalid key for attribute at index $index: " . $keyValidator->getDescription();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate keys
|
||||
if (in_array($attribute['key'], $keys)) {
|
||||
$this->message = "Duplicate attribute key: " . $attribute['key'];
|
||||
return false;
|
||||
}
|
||||
$keys[] = $attribute['key'];
|
||||
|
||||
// Validate type
|
||||
if (!in_array($attribute['type'], $this->supportedTypes)) {
|
||||
$this->message = "Invalid type for attribute '" . $attribute['key'] . "': " . $attribute['type'];
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate size for string types
|
||||
if ($attribute['type'] === Database::VAR_STRING) {
|
||||
if (!isset($attribute['size']) || !is_int($attribute['size']) || $attribute['size'] < 1 || $attribute['size'] > APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH) {
|
||||
$this->message = "Invalid or missing size for string attribute '" . $attribute['key'] . "'. Size must be between 1 and " . APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate format if provided
|
||||
if (isset($attribute['format']) && $attribute['format'] !== '') {
|
||||
if (!in_array($attribute['format'], $this->supportedFormats)) {
|
||||
$this->message = "Invalid format for attribute '" . $attribute['key'] . "': " . $attribute['format'];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required field if provided
|
||||
if (isset($attribute['required']) && !is_bool($attribute['required'])) {
|
||||
$this->message = "Invalid 'required' value for attribute '" . $attribute['key'] . "': must be a boolean";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate array field if provided
|
||||
if (isset($attribute['array']) && !is_bool($attribute['array'])) {
|
||||
$this->message = "Invalid 'array' value for attribute '" . $attribute['key'] . "': must be a boolean";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate signed field if provided
|
||||
if (isset($attribute['signed']) && !is_bool($attribute['signed'])) {
|
||||
$this->message = "Invalid 'signed' value for attribute '" . $attribute['key'] . "': must be a boolean";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required and default conflict
|
||||
if (isset($attribute['required']) && $attribute['required'] === true && isset($attribute['default']) && $attribute['default'] !== null) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "' cannot have a default value when required is true";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate array and default conflict
|
||||
if (isset($attribute['array']) && $attribute['array'] === true && isset($attribute['default']) && $attribute['default'] !== null) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "' cannot have a default value when array is true";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate enum elements if format is enum
|
||||
if (isset($attribute['format']) && $attribute['format'] === APP_DATABASE_ATTRIBUTE_ENUM) {
|
||||
if (!isset($attribute['elements']) || !is_array($attribute['elements']) || empty($attribute['elements'])) {
|
||||
$this->message = "Attribute '" . $attribute['key'] . "' with enum format must have 'elements' array";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate relationship options
|
||||
if ($attribute['type'] === Database::VAR_RELATIONSHIP) {
|
||||
if (!isset($attribute['relatedCollection']) || empty($attribute['relatedCollection'])) {
|
||||
$this->message = "Relationship attribute '" . $attribute['key'] . "' must have 'relatedCollection'";
|
||||
return false;
|
||||
}
|
||||
if (!isset($attribute['relationType']) || !in_array($attribute['relationType'], [
|
||||
Database::RELATION_ONE_TO_ONE,
|
||||
Database::RELATION_ONE_TO_MANY,
|
||||
Database::RELATION_MANY_TO_ONE,
|
||||
Database::RELATION_MANY_TO_MANY,
|
||||
])) {
|
||||
$this->message = "Relationship attribute '" . $attribute['key'] . "' must have valid 'relationType'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is array
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_ARRAY;
|
||||
}
|
||||
}
|
||||
190
src/Appwrite/Utopia/Database/Validator/Indexes.php
Normal file
190
src/Appwrite/Utopia/Database/Validator/Indexes.php
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator;
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Validator;
|
||||
|
||||
class Indexes extends Validator
|
||||
{
|
||||
protected int $maxIndexes;
|
||||
protected string $message = 'Invalid indexes';
|
||||
|
||||
/**
|
||||
* @var array<string> Supported index types
|
||||
*/
|
||||
protected array $supportedTypes = [
|
||||
Database::INDEX_KEY,
|
||||
Database::INDEX_FULLTEXT,
|
||||
Database::INDEX_UNIQUE,
|
||||
Database::INDEX_SPATIAL,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string> Supported orders
|
||||
*/
|
||||
protected array $supportedOrders = [
|
||||
Database::ORDER_ASC,
|
||||
Database::ORDER_DESC,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param int $maxIndexes Maximum number of indexes allowed
|
||||
*/
|
||||
public function __construct(int $maxIndexes = APP_LIMIT_ARRAY_PARAMS_SIZE)
|
||||
{
|
||||
$this->maxIndexes = $maxIndexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* Returns validator description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is valid
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
$this->message = 'Indexes must be an array';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($value) > $this->maxIndexes) {
|
||||
$this->message = 'Maximum of ' . $this->maxIndexes . ' indexes allowed';
|
||||
return false;
|
||||
}
|
||||
|
||||
$keyValidator = new Key();
|
||||
$keys = [];
|
||||
|
||||
foreach ($value as $index => $indexDef) {
|
||||
if (!is_array($indexDef)) {
|
||||
$this->message = "Index at position $index must be an object";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!isset($indexDef['key'])) {
|
||||
$this->message = "Index at position $index is missing required field 'key'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($indexDef['type'])) {
|
||||
$this->message = "Index at position $index is missing required field 'type'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($indexDef['attributes']) || !is_array($indexDef['attributes'])) {
|
||||
$this->message = "Index at position $index is missing required field 'attributes' (must be an array)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate key
|
||||
if (!$keyValidator->isValid($indexDef['key'])) {
|
||||
$this->message = "Invalid key for index at position $index: " . $keyValidator->getDescription();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate keys
|
||||
if (in_array($indexDef['key'], $keys)) {
|
||||
$this->message = "Duplicate index key: " . $indexDef['key'];
|
||||
return false;
|
||||
}
|
||||
$keys[] = $indexDef['key'];
|
||||
|
||||
// Validate type
|
||||
if (!in_array($indexDef['type'], $this->supportedTypes)) {
|
||||
$this->message = "Invalid type for index '" . $indexDef['key'] . "': " . $indexDef['type'];
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate attributes array
|
||||
if (empty($indexDef['attributes'])) {
|
||||
$this->message = "Index '" . $indexDef['key'] . "' must have at least one attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($indexDef['attributes']) > APP_LIMIT_ARRAY_PARAMS_SIZE) {
|
||||
$this->message = "Index '" . $indexDef['key'] . "' cannot have more than " . APP_LIMIT_ARRAY_PARAMS_SIZE . " attributes";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate each attribute in the index
|
||||
foreach ($indexDef['attributes'] as $attrIndex => $attr) {
|
||||
if (!is_string($attr)) {
|
||||
$this->message = "Invalid attribute at position $attrIndex in index '" . $indexDef['key'] . "': must be a string";
|
||||
return false;
|
||||
}
|
||||
if (!$keyValidator->isValid($attr) && !in_array($attr, ['$id', '$createdAt', '$updatedAt'])) {
|
||||
$this->message = "Invalid attribute name '$attr' in index '" . $indexDef['key'] . "'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate orders if provided
|
||||
if (isset($indexDef['orders'])) {
|
||||
if (!is_array($indexDef['orders'])) {
|
||||
$this->message = "Index '" . $indexDef['key'] . "' orders must be an array";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($indexDef['orders'] as $order) {
|
||||
if ($order !== null && $order !== '' && !in_array($order, $this->supportedOrders)) {
|
||||
$this->message = "Invalid order '$order' in index '" . $indexDef['key'] . "'. Must be 'ASC' or 'DESC'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate lengths if provided
|
||||
if (isset($indexDef['lengths'])) {
|
||||
if (!is_array($indexDef['lengths'])) {
|
||||
$this->message = "Index '" . $indexDef['key'] . "' lengths must be an array";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($indexDef['lengths'] as $length) {
|
||||
if ($length !== null && (!is_int($length) || $length < 0)) {
|
||||
$this->message = "Invalid length in index '" . $indexDef['key'] . "': must be a non-negative integer or null";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is array
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isArray(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_ARRAY;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue