Skip to content

Commit df94765

Browse files
committed
Update validation
1 parent c6ccbfa commit df94765

5 files changed

Lines changed: 132 additions & 353 deletions

File tree

src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php

Lines changed: 69 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use Appwrite\SDK\Response as SDKResponse;
1212
use Appwrite\Utopia\Database\Validator\Attributes as AttributesValidator;
1313
use Appwrite\Utopia\Database\Validator\CustomId;
14-
use Appwrite\Utopia\Database\Validator\Indexes as IndexesValidator;
1514
use Appwrite\Utopia\Response as UtopiaResponse;
1615
use Utopia\Database\Database;
1716
use Utopia\Database\Document;
@@ -26,9 +25,10 @@
2625
use Utopia\Database\Validator\Permissions;
2726
use Utopia\Database\Validator\UID;
2827
use Utopia\Swoole\Response as SwooleResponse;
28+
use Utopia\Validator\ArrayList;
2929
use Utopia\Validator\Boolean;
30+
use Utopia\Validator\JSON;
3031
use Utopia\Validator\Nullable;
31-
use Utopia\Validator\Text;
3232

3333
class Create extends Action
3434
{
@@ -78,8 +78,8 @@ public function __construct()
7878
->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)
7979
->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)
8080
->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)
81-
->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)
82-
->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)
81+
->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)
82+
->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)
8383
->inject('response')
8484
->inject('dbForProject')
8585
->inject('queueForEvents')
@@ -121,11 +121,28 @@ public function action(string $databaseId, string $collectionId, string $name, ?
121121
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
122122
$databaseKey = 'database_' . $database->getSequence();
123123

124+
$attributesValidator = new AttributesValidator(
125+
APP_LIMIT_ARRAY_PARAMS_SIZE,
126+
$dbForProject->getAdapter()->getSupportForSpatialAttributes()
127+
);
128+
129+
if (!$attributesValidator->isValid($attributes)) {
130+
$dbForProject->deleteDocument($databaseKey, $collection->getId());
131+
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $attributesValidator->getDescription());
132+
}
133+
134+
foreach ($attributes as $attribute) {
135+
if (($attribute['type'] ?? '') === Database::VAR_RELATIONSHIP) {
136+
$dbForProject->deleteDocument($databaseKey, $collection->getId());
137+
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Relationship attributes cannot be created inline. Use the create relationship endpoint instead.');
138+
}
139+
}
140+
124141
$collectionAttributes = [];
125142
$attributeDocuments = [];
126143
try {
127144
foreach ($attributes as $attributeDef) {
128-
$attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef, $dbForProject);
145+
$attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef);
129146
$collectionAttributes[] = $attrDoc['collection'];
130147
$attributeDocuments[] = $attrDoc['document'];
131148
}
@@ -134,17 +151,11 @@ public function action(string $databaseId, string $collectionId, string $name, ?
134151
throw $e;
135152
}
136153

137-
$indexLimit = $dbForProject->getLimitForIndexes();
138-
if (\count($indexes) > $indexLimit) {
139-
$dbForProject->deleteDocument($databaseKey, $collection->getId());
140-
throw new Exception($this->getLimitException(), "Cannot create more than $indexLimit indexes for a collection");
141-
}
142-
143154
$collectionIndexes = [];
144155
$indexDocuments = [];
145156
try {
146157
foreach ($indexes as $indexDef) {
147-
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes, $dbForProject);
158+
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes);
148159
$collectionIndexes[] = $idxDoc['collection'];
149160
$indexDocuments[] = $idxDoc['document'];
150161
}
@@ -153,6 +164,28 @@ public function action(string $databaseId, string $collectionId, string $name, ?
153164
throw $e;
154165
}
155166

167+
// Validate indexes with DB adapter capabilities
168+
$indexValidator = new IndexValidator(
169+
$collectionAttributes,
170+
[],
171+
$dbForProject->getAdapter()->getMaxIndexLength(),
172+
$dbForProject->getAdapter()->getInternalIndexesKeys(),
173+
$dbForProject->getAdapter()->getSupportForIndexArray(),
174+
$dbForProject->getAdapter()->getSupportForSpatialIndexNull(),
175+
$dbForProject->getAdapter()->getSupportForSpatialIndexOrder(),
176+
$dbForProject->getAdapter()->getSupportForVectors(),
177+
$dbForProject->getAdapter()->getSupportForAttributes(),
178+
$dbForProject->getAdapter()->getSupportForMultipleFulltextIndexes(),
179+
$dbForProject->getAdapter()->getSupportForIdenticalIndexes()
180+
);
181+
182+
foreach ($collectionIndexes as $indexDoc) {
183+
if (!$indexValidator->isValid($indexDoc)) {
184+
$dbForProject->deleteDocument($databaseKey, $collection->getId());
185+
throw new Exception($this->getInvalidIndexException(), $indexValidator->getDescription());
186+
}
187+
}
188+
156189
try {
157190
$dbForProject->createCollection(
158191
id: $collectionKey,
@@ -209,64 +242,37 @@ public function action(string $databaseId, string $collectionId, string $name, ?
209242
*
210243
* @return array{collection: Document, document: Document}
211244
*/
212-
protected function buildAttributeDocument(Document $database, Document $collection, array $attributeDef, Database $dbForProject): array
213-
{
214-
$key = $attributeDef['key'];
215-
$type = $attributeDef['type'];
216-
$size = $attributeDef['size'] ?? 0;
217-
$required = $attributeDef['required'] ?? false;
218-
$signed = $attributeDef['signed'] ?? true;
219-
$array = $attributeDef['array'] ?? false;
220-
$format = $attributeDef['format'] ?? '';
245+
protected function buildAttributeDocument(
246+
Document $database,
247+
Document $collection,
248+
array $attribute,
249+
): array {
250+
$key = $attribute['key'];
251+
$type = $attribute['type'];
252+
$size = $attribute['size'] ?? 0;
253+
$required = $attribute['required'] ?? false;
254+
$signed = $attribute['signed'] ?? true;
255+
$array = $attribute['array'] ?? false;
256+
$format = $attribute['format'] ?? '';
221257
$formatOptions = [];
222-
$filters = $attributeDef['filters'] ?? [];
223-
$default = $attributeDef['default'] ?? null;
224-
$options = [];
258+
$filters = $attribute['filters'] ?? [];
259+
$default = $attribute['default'] ?? null;
225260

226-
if ($type === Database::VAR_STRING) {
227-
if ($size === 0) {
228-
$size = 256; // Default size for strings
229-
}
261+
if ($format === APP_DATABASE_ATTRIBUTE_ENUM && isset($attribute['elements'])) {
262+
$formatOptions = ['elements' => $attribute['elements']];
230263
}
231264

232-
if ($format === APP_DATABASE_ATTRIBUTE_ENUM && isset($attributeDef['elements'])) {
233-
$formatOptions = ['elements' => $attributeDef['elements']];
234-
}
265+
if (isset($attribute['min']) || isset($attribute['max'])) {
266+
$format = $type === Database::VAR_INTEGER
267+
? APP_DATABASE_ATTRIBUTE_INT_RANGE
268+
: APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
235269

236-
if (isset($attributeDef['min']) || isset($attributeDef['max'])) {
237-
$format = $type === Database::VAR_INTEGER ? APP_DATABASE_ATTRIBUTE_INT_RANGE : APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
238270
$formatOptions = [
239-
'min' => $attributeDef['min'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
240-
'max' => $attributeDef['max'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
271+
'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
272+
'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
241273
];
242274
}
243275

244-
if ($type === Database::VAR_RELATIONSHIP) {
245-
$options = [
246-
'relatedCollection' => $attributeDef['relatedCollection'] ?? '',
247-
'relationType' => $attributeDef['relationType'] ?? Database::RELATION_ONE_TO_ONE,
248-
'twoWay' => $attributeDef['twoWay'] ?? false,
249-
'twoWayKey' => $attributeDef['twoWayKey'] ?? '',
250-
'onDelete' => $attributeDef['onDelete'] ?? Database::RELATION_MUTATE_RESTRICT,
251-
];
252-
}
253-
254-
255-
if (\in_array($type, Database::SPATIAL_TYPES)) {
256-
if (!$dbForProject->getAdapter()->getSupportForSpatialIndex()) {
257-
throw new Exception($this->getFormatUnsupportedException(), "Spatial attributes are not supported by the current database");
258-
}
259-
}
260-
261-
if ($type === Database::VAR_RELATIONSHIP) {
262-
$options['side'] = Database::RELATION_SIDE_PARENT;
263-
$relatedCollection = $dbForProject->getDocument('database_' . $database->getSequence(), $options['relatedCollection'] ?? '');
264-
if ($relatedCollection->isEmpty()) {
265-
$parent = $this->isCollectionsAPI() ? 'collection' : 'table';
266-
throw new Exception($this->getParentNotFoundException(), "The related $parent was not found.");
267-
}
268-
}
269-
270276
$collectionDoc = new Document([
271277
'$id' => $key,
272278
'key' => $key,
@@ -279,7 +285,6 @@ protected function buildAttributeDocument(Document $database, Document $collecti
279285
'format' => $format,
280286
'formatOptions' => $formatOptions,
281287
'filters' => $filters,
282-
'options' => $options,
283288
]);
284289

285290
$document = new Document([
@@ -299,7 +304,6 @@ protected function buildAttributeDocument(Document $database, Document $collecti
299304
'format' => $format,
300305
'formatOptions' => $formatOptions,
301306
'filters' => $filters,
302-
'options' => $options,
303307
]);
304308

305309
return [
@@ -313,7 +317,7 @@ protected function buildAttributeDocument(Document $database, Document $collecti
313317
*
314318
* @return array{collection: Document, document: Document}
315319
*/
316-
protected function buildIndexDocument(Document $database, Document $collection, array $indexDef, array $attributeDocuments, Database $dbForProject): array
320+
protected function buildIndexDocument(Document $database, Document $collection, array $indexDef, array $attributeDocuments): array
317321
{
318322
$key = $indexDef['key'];
319323
$type = $indexDef['type'];
@@ -323,23 +327,13 @@ protected function buildIndexDocument(Document $database, Document $collection,
323327

324328
$attrKeys = array_map(fn ($a) => $a->getAttribute('key'), $attributeDocuments);
325329

326-
$systemAttrs = ['$id', '$createdAt', '$updatedAt'];
327-
330+
// Build lengths and orders based on attribute properties
328331
foreach ($indexAttributes as $i => $attr) {
329-
if (!in_array($attr, $attrKeys) && !in_array($attr, $systemAttrs)) {
330-
throw new Exception($this->getParentUnknownException(), "Unknown attribute: " . $attr . ". Verify the attribute name or ensure it's in the attributes list.");
331-
}
332-
333332
$attrIndex = array_search($attr, $attrKeys);
334333
if ($attrIndex !== false) {
335334
$attrDoc = $attributeDocuments[$attrIndex];
336-
$attrType = $attrDoc->getAttribute('type');
337335
$attrArray = $attrDoc->getAttribute('array', false);
338336

339-
if ($attrType === Database::VAR_RELATIONSHIP) {
340-
throw new Exception($this->getParentInvalidTypeException(), "Cannot create an index for a relationship attribute: " . $attr);
341-
}
342-
343337
if (empty($lengths[$i])) {
344338
$lengths[$i] = null;
345339
}
@@ -378,24 +372,6 @@ protected function buildIndexDocument(Document $database, Document $collection,
378372
'orders' => $orders,
379373
]);
380374

381-
$indexValidator = new IndexValidator(
382-
$attributeDocuments,
383-
[],
384-
$dbForProject->getAdapter()->getMaxIndexLength(),
385-
$dbForProject->getAdapter()->getInternalIndexesKeys(),
386-
$dbForProject->getAdapter()->getSupportForIndexArray(),
387-
$dbForProject->getAdapter()->getSupportForSpatialIndexNull(),
388-
$dbForProject->getAdapter()->getSupportForSpatialIndexOrder(),
389-
$dbForProject->getAdapter()->getSupportForVectors(),
390-
$dbForProject->getAdapter()->getSupportForAttributes(),
391-
$dbForProject->getAdapter()->getSupportForMultipleFulltextIndexes(),
392-
$dbForProject->getAdapter()->getSupportForIdenticalIndexes()
393-
);
394-
395-
if (!$indexValidator->isValid($collectionDoc)) {
396-
throw new Exception($this->getInvalidIndexException(), $indexValidator->getDescription());
397-
}
398-
399375
return [
400376
'collection' => $collectionDoc,
401377
'document' => $document,

src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Create.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
use Appwrite\SDK\ContentType;
88
use Appwrite\SDK\Method;
99
use Appwrite\SDK\Response as SDKResponse;
10-
use Appwrite\Utopia\Database\Validator\Attributes as AttributesValidator;
1110
use Appwrite\Utopia\Database\Validator\CustomId;
12-
use Appwrite\Utopia\Database\Validator\Indexes as IndexesValidator;
1311
use Appwrite\Utopia\Response as UtopiaResponse;
1412
use Utopia\Database\Validator\Permissions;
1513
use Utopia\Database\Validator\UID;
1614
use Utopia\Swoole\Response as SwooleResponse;
15+
use Utopia\Validator\ArrayList;
1716
use Utopia\Validator\Boolean;
17+
use Utopia\Validator\JSON;
1818
use Utopia\Validator\Nullable;
1919
use Utopia\Validator\Text;
2020

@@ -62,8 +62,8 @@ public function __construct()
6262
->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)
6363
->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)
6464
->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)
65-
->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)
66-
->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)
65+
->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)
66+
->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)
6767
->inject('response')
6868
->inject('dbForProject')
6969
->inject('queueForEvents')

0 commit comments

Comments
 (0)