Update validation

This commit is contained in:
Jake Barnby
2025-12-04 23:25:15 +13:00
parent c6ccbfaae1
commit df94765cf3
5 changed files with 132 additions and 353 deletions
@@ -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',