mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Update validation
This commit is contained in:
@@ -11,7 +11,6 @@ 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;
|
||||
@@ -26,9 +25,10 @@ use Utopia\Database\Validator\Index as IndexValidator;
|
||||
use Utopia\Database\Validator\Permissions;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\JSON;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
@@ -78,8 +78,8 @@ 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)
|
||||
->param('attributes', [], new ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), '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 ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), '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')
|
||||
@@ -121,11 +121,28 @@ class Create extends Action
|
||||
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
$databaseKey = 'database_' . $database->getSequence();
|
||||
|
||||
$attributesValidator = new AttributesValidator(
|
||||
APP_LIMIT_ARRAY_PARAMS_SIZE,
|
||||
$dbForProject->getAdapter()->getSupportForSpatialAttributes()
|
||||
);
|
||||
|
||||
if (!$attributesValidator->isValid($attributes)) {
|
||||
$dbForProject->deleteDocument($databaseKey, $collection->getId());
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $attributesValidator->getDescription());
|
||||
}
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
if (($attribute['type'] ?? '') === Database::VAR_RELATIONSHIP) {
|
||||
$dbForProject->deleteDocument($databaseKey, $collection->getId());
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Relationship attributes cannot be created inline. Use the create relationship endpoint instead.');
|
||||
}
|
||||
}
|
||||
|
||||
$collectionAttributes = [];
|
||||
$attributeDocuments = [];
|
||||
try {
|
||||
foreach ($attributes as $attributeDef) {
|
||||
$attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef, $dbForProject);
|
||||
$attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef);
|
||||
$collectionAttributes[] = $attrDoc['collection'];
|
||||
$attributeDocuments[] = $attrDoc['document'];
|
||||
}
|
||||
@@ -134,17 +151,11 @@ class Create extends Action
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$indexLimit = $dbForProject->getLimitForIndexes();
|
||||
if (\count($indexes) > $indexLimit) {
|
||||
$dbForProject->deleteDocument($databaseKey, $collection->getId());
|
||||
throw new Exception($this->getLimitException(), "Cannot create more than $indexLimit indexes for a collection");
|
||||
}
|
||||
|
||||
$collectionIndexes = [];
|
||||
$indexDocuments = [];
|
||||
try {
|
||||
foreach ($indexes as $indexDef) {
|
||||
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes, $dbForProject);
|
||||
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes);
|
||||
$collectionIndexes[] = $idxDoc['collection'];
|
||||
$indexDocuments[] = $idxDoc['document'];
|
||||
}
|
||||
@@ -153,6 +164,28 @@ class Create extends Action
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Validate indexes with DB adapter capabilities
|
||||
$indexValidator = new IndexValidator(
|
||||
$collectionAttributes,
|
||||
[],
|
||||
$dbForProject->getAdapter()->getMaxIndexLength(),
|
||||
$dbForProject->getAdapter()->getInternalIndexesKeys(),
|
||||
$dbForProject->getAdapter()->getSupportForIndexArray(),
|
||||
$dbForProject->getAdapter()->getSupportForSpatialIndexNull(),
|
||||
$dbForProject->getAdapter()->getSupportForSpatialIndexOrder(),
|
||||
$dbForProject->getAdapter()->getSupportForVectors(),
|
||||
$dbForProject->getAdapter()->getSupportForAttributes(),
|
||||
$dbForProject->getAdapter()->getSupportForMultipleFulltextIndexes(),
|
||||
$dbForProject->getAdapter()->getSupportForIdenticalIndexes()
|
||||
);
|
||||
|
||||
foreach ($collectionIndexes as $indexDoc) {
|
||||
if (!$indexValidator->isValid($indexDoc)) {
|
||||
$dbForProject->deleteDocument($databaseKey, $collection->getId());
|
||||
throw new Exception($this->getInvalidIndexException(), $indexValidator->getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$dbForProject->createCollection(
|
||||
id: $collectionKey,
|
||||
@@ -209,64 +242,37 @@ class Create extends Action
|
||||
*
|
||||
* @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'] ?? '';
|
||||
protected function buildAttributeDocument(
|
||||
Document $database,
|
||||
Document $collection,
|
||||
array $attribute,
|
||||
): array {
|
||||
$key = $attribute['key'];
|
||||
$type = $attribute['type'];
|
||||
$size = $attribute['size'] ?? 0;
|
||||
$required = $attribute['required'] ?? false;
|
||||
$signed = $attribute['signed'] ?? true;
|
||||
$array = $attribute['array'] ?? false;
|
||||
$format = $attribute['format'] ?? '';
|
||||
$formatOptions = [];
|
||||
$filters = $attributeDef['filters'] ?? [];
|
||||
$default = $attributeDef['default'] ?? null;
|
||||
$options = [];
|
||||
$filters = $attribute['filters'] ?? [];
|
||||
$default = $attribute['default'] ?? null;
|
||||
|
||||
if ($type === Database::VAR_STRING) {
|
||||
if ($size === 0) {
|
||||
$size = 256; // Default size for strings
|
||||
}
|
||||
if ($format === APP_DATABASE_ATTRIBUTE_ENUM && isset($attribute['elements'])) {
|
||||
$formatOptions = ['elements' => $attribute['elements']];
|
||||
}
|
||||
|
||||
if ($format === APP_DATABASE_ATTRIBUTE_ENUM && isset($attributeDef['elements'])) {
|
||||
$formatOptions = ['elements' => $attributeDef['elements']];
|
||||
}
|
||||
if (isset($attribute['min']) || isset($attribute['max'])) {
|
||||
$format = $type === Database::VAR_INTEGER
|
||||
? APP_DATABASE_ATTRIBUTE_INT_RANGE
|
||||
: APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
|
||||
|
||||
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'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
|
||||
'max' => $attributeDef['max'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
|
||||
'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
|
||||
'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MAX : \PHP_FLOAT_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 (\in_array($type, Database::SPATIAL_TYPES)) {
|
||||
if (!$dbForProject->getAdapter()->getSupportForSpatialIndex()) {
|
||||
throw new Exception($this->getFormatUnsupportedException(), "Spatial attributes are not supported by the current database");
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -279,7 +285,6 @@ class Create extends Action
|
||||
'format' => $format,
|
||||
'formatOptions' => $formatOptions,
|
||||
'filters' => $filters,
|
||||
'options' => $options,
|
||||
]);
|
||||
|
||||
$document = new Document([
|
||||
@@ -299,7 +304,6 @@ class Create extends Action
|
||||
'format' => $format,
|
||||
'formatOptions' => $formatOptions,
|
||||
'filters' => $filters,
|
||||
'options' => $options,
|
||||
]);
|
||||
|
||||
return [
|
||||
@@ -313,7 +317,7 @@ class Create extends Action
|
||||
*
|
||||
* @return array{collection: Document, document: Document}
|
||||
*/
|
||||
protected function buildIndexDocument(Document $database, Document $collection, array $indexDef, array $attributeDocuments, Database $dbForProject): array
|
||||
protected function buildIndexDocument(Document $database, Document $collection, array $indexDef, array $attributeDocuments): array
|
||||
{
|
||||
$key = $indexDef['key'];
|
||||
$type = $indexDef['type'];
|
||||
@@ -323,23 +327,13 @@ class Create extends Action
|
||||
|
||||
$attrKeys = array_map(fn ($a) => $a->getAttribute('key'), $attributeDocuments);
|
||||
|
||||
$systemAttrs = ['$id', '$createdAt', '$updatedAt'];
|
||||
|
||||
// Build lengths and orders based on attribute properties
|
||||
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;
|
||||
}
|
||||
@@ -378,24 +372,6 @@ class Create extends Action
|
||||
'orders' => $orders,
|
||||
]);
|
||||
|
||||
$indexValidator = new IndexValidator(
|
||||
$attributeDocuments,
|
||||
[],
|
||||
$dbForProject->getAdapter()->getMaxIndexLength(),
|
||||
$dbForProject->getAdapter()->getInternalIndexesKeys(),
|
||||
$dbForProject->getAdapter()->getSupportForIndexArray(),
|
||||
$dbForProject->getAdapter()->getSupportForSpatialIndexNull(),
|
||||
$dbForProject->getAdapter()->getSupportForSpatialIndexOrder(),
|
||||
$dbForProject->getAdapter()->getSupportForVectors(),
|
||||
$dbForProject->getAdapter()->getSupportForAttributes(),
|
||||
$dbForProject->getAdapter()->getSupportForMultipleFulltextIndexes(),
|
||||
$dbForProject->getAdapter()->getSupportForIdenticalIndexes()
|
||||
);
|
||||
|
||||
if (!$indexValidator->isValid($collectionDoc)) {
|
||||
throw new Exception($this->getInvalidIndexException(), $indexValidator->getDescription());
|
||||
}
|
||||
|
||||
return [
|
||||
'collection' => $collectionDoc,
|
||||
'document' => $document,
|
||||
|
||||
@@ -7,14 +7,14 @@ 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;
|
||||
use Utopia\Swoole\Response as SwooleResponse;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\JSON;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
@@ -62,8 +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)
|
||||
->param('columns', [], new ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), '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 ArrayList(new JSON(), APP_LIMIT_ARRAY_PARAMS_SIZE), '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')
|
||||
|
||||
@@ -6,8 +6,11 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\Key;
|
||||
use Utopia\Validator;
|
||||
use Utopia\Validator\Email;
|
||||
use Utopia\Validator\IP;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\URL;
|
||||
|
||||
class Attributes extends Validator
|
||||
{
|
||||
@@ -23,7 +26,6 @@ class Attributes extends Validator
|
||||
Database::VAR_FLOAT,
|
||||
Database::VAR_BOOLEAN,
|
||||
Database::VAR_DATETIME,
|
||||
Database::VAR_RELATIONSHIP,
|
||||
Database::VAR_POINT,
|
||||
Database::VAR_LINESTRING,
|
||||
Database::VAR_POLYGON,
|
||||
@@ -42,9 +44,12 @@ class Attributes extends Validator
|
||||
|
||||
/**
|
||||
* @param int $maxAttributes Maximum number of attributes allowed
|
||||
* @param bool $supportForSpatialAttributes Whether DB supports spatial attributes
|
||||
*/
|
||||
public function __construct(int $maxAttributes = APP_LIMIT_ARRAY_PARAMS_SIZE)
|
||||
{
|
||||
public function __construct(
|
||||
int $maxAttributes = APP_LIMIT_ARRAY_PARAMS_SIZE,
|
||||
protected bool $supportForSpatialAttributes = true,
|
||||
) {
|
||||
$this->maxAttributes = $maxAttributes;
|
||||
}
|
||||
|
||||
@@ -113,17 +118,23 @@ class Attributes extends Validator
|
||||
|
||||
// Check for reserved keys
|
||||
$reservedKeys = ['$id', '$createdAt', '$updatedAt', '$permissions', '$collection'];
|
||||
if (in_array($attribute['key'], $reservedKeys)) {
|
||||
if (\in_array($attribute['key'], $reservedKeys)) {
|
||||
$this->message = "Attribute key '" . $attribute['key'] . "' is reserved and cannot be used";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if (!in_array($attribute['type'], $this->supportedTypes)) {
|
||||
if (!\in_array($attribute['type'], $this->supportedTypes)) {
|
||||
$this->message = "Invalid type for attribute '" . $attribute['key'] . "': " . $attribute['type'];
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate spatial type support
|
||||
if (\in_array($attribute['type'], Database::SPATIAL_TYPES) && !$this->supportForSpatialAttributes) {
|
||||
$this->message = "Spatial attributes are not supported by the current database";
|
||||
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) {
|
||||
@@ -213,6 +224,28 @@ class Attributes extends Validator
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate format-specific defaults
|
||||
$format = $attribute['format'] ?? '';
|
||||
if ($format === APP_DATABASE_ATTRIBUTE_EMAIL) {
|
||||
$emailValidator = new Email();
|
||||
if (!$emailValidator->isValid($attribute['default'])) {
|
||||
$this->message = "Default value for email attribute '" . $attribute['key'] . "' must be a valid email address";
|
||||
return false;
|
||||
}
|
||||
} elseif ($format === APP_DATABASE_ATTRIBUTE_IP) {
|
||||
$ipValidator = new IP();
|
||||
if (!$ipValidator->isValid($attribute['default'])) {
|
||||
$this->message = "Default value for IP attribute '" . $attribute['key'] . "' must be a valid IP address";
|
||||
return false;
|
||||
}
|
||||
} elseif ($format === APP_DATABASE_ATTRIBUTE_URL) {
|
||||
$urlValidator = new URL();
|
||||
if (!$urlValidator->isValid($attribute['default'])) {
|
||||
$this->message = "Default value for URL attribute '" . $attribute['key'] . "' must be a valid URL";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Database::VAR_INTEGER:
|
||||
@@ -298,55 +331,6 @@ class Attributes extends Validator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate relationship options
|
||||
if ($attribute['type'] === Database::VAR_RELATIONSHIP) {
|
||||
// Validate array cannot be true for relationship
|
||||
if (isset($attribute['array']) && $attribute['array'] === true) {
|
||||
$this->message = "Relationship attribute '" . $attribute['key'] . "' cannot be an array type";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields for relationship
|
||||
if (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;
|
||||
}
|
||||
|
||||
// Validate twoWay if provided
|
||||
if (isset($attribute['twoWay']) && !is_bool($attribute['twoWay'])) {
|
||||
$this->message = "Invalid 'twoWay' value for relationship attribute '" . $attribute['key'] . "': must be a boolean";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate twoWayKey if provided
|
||||
if (!empty($attribute['twoWayKey'])) {
|
||||
if (!$keyValidator->isValid($attribute['twoWayKey'])) {
|
||||
$this->message = "Invalid 'twoWayKey' for relationship attribute '" . $attribute['key'] . "': " . $keyValidator->getDescription();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate onDelete if provided
|
||||
if (isset($attribute['onDelete']) && !in_array($attribute['onDelete'], [
|
||||
Database::RELATION_MUTATE_CASCADE,
|
||||
Database::RELATION_MUTATE_RESTRICT,
|
||||
Database::RELATION_MUTATE_SET_NULL,
|
||||
])) {
|
||||
$this->message = "Invalid 'onDelete' value for relationship attribute '" . $attribute['key'] . "': must be 'cascade', 'restrict', or 'setNull'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
<?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 $i => $index) {
|
||||
if (!is_array($index)) {
|
||||
$this->message = "Index at position $i must be an object";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!isset($index['key'])) {
|
||||
$this->message = "Index at position $i is missing required field 'key'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($index['type'])) {
|
||||
$this->message = "Index at position $i is missing required field 'type'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($index['attributes']) || !is_array($index['attributes'])) {
|
||||
$this->message = "Index at position $i is missing required field 'attributes' (must be an array)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate key
|
||||
if (!$keyValidator->isValid($index['key'])) {
|
||||
$this->message = "Invalid key for index at position $i: " . $keyValidator->getDescription();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate keys
|
||||
if (in_array($index['key'], $keys)) {
|
||||
$this->message = "Duplicate index key: " . $index['key'];
|
||||
return false;
|
||||
}
|
||||
$keys[] = $index['key'];
|
||||
|
||||
// Validate type
|
||||
if (!in_array($index['type'], $this->supportedTypes)) {
|
||||
$this->message = "Invalid type for index '" . $index['key'] . "': " . $index['type'];
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate attributes array
|
||||
if (empty($index['attributes'])) {
|
||||
$this->message = "Index '" . $index['key'] . "' must have at least one attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($index['attributes']) > APP_LIMIT_ARRAY_PARAMS_SIZE) {
|
||||
$this->message = "Index '" . $index['key'] . "' cannot have more than " . APP_LIMIT_ARRAY_PARAMS_SIZE . " attributes";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate each attribute in the index
|
||||
foreach ($index['attributes'] as $attrIndex => $attr) {
|
||||
if (!is_string($attr)) {
|
||||
$this->message = "Invalid attribute at position $attrIndex in index '" . $index['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 '" . $index['key'] . "'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate orders if provided
|
||||
if (isset($index['orders'])) {
|
||||
if (!is_array($index['orders'])) {
|
||||
$this->message = "Index '" . $index['key'] . "' orders must be an array";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate orders array length matches attributes length
|
||||
if (count($index['orders']) !== count($index['attributes'])) {
|
||||
$this->message = "Index '" . $index['key'] . "': orders array length (" . count($index['orders']) . ") must match attributes array length (" . count($index['attributes']) . ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($index['orders'] as $order) {
|
||||
if ($order !== null && $order !== '' && !in_array($order, $this->supportedOrders)) {
|
||||
$this->message = "Invalid order '$order' in index '" . $index['key'] . "'. Must be 'ASC' or 'DESC'";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate lengths if provided
|
||||
if (isset($index['lengths'])) {
|
||||
if (!is_array($index['lengths'])) {
|
||||
$this->message = "Index '" . $index['key'] . "' lengths must be an array";
|
||||
return false;
|
||||
}
|
||||
|
||||
// MAJOR-7: Validate lengths array length matches attributes length
|
||||
if (count($index['lengths']) !== count($index['attributes'])) {
|
||||
$this->message = "Index '" . $index['key'] . "': lengths array length (" . count($index['lengths']) . ") must match attributes array length (" . count($index['attributes']) . ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($index['lengths'] as $length) {
|
||||
if ($length !== null && (!is_int($length) || $length < 0)) {
|
||||
$this->message = "Invalid length in index '" . $index['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;
|
||||
}
|
||||
}
|
||||
@@ -7101,6 +7101,27 @@ class DatabasesCustomServerTest extends Scope
|
||||
// Should succeed - system attributes can be indexed
|
||||
$this->assertEquals(201, $collection['headers']['status-code']);
|
||||
|
||||
// Test: Relationship attributes not supported inline (rejected as invalid type)
|
||||
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Relationship Test',
|
||||
'attributes' => [
|
||||
[
|
||||
'key' => 'related',
|
||||
'type' => 'relationship',
|
||||
'relatedCollection' => 'some_collection',
|
||||
'relationType' => 'oneToOne',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $collection['headers']['status-code'], 'Got ' . $collection['headers']['status-code'] . ': ' . json_encode($collection['body']));
|
||||
$this->assertStringContainsString('Invalid type', $collection['body']['message']);
|
||||
|
||||
// Cleanup
|
||||
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user