From 4d560bdff2f901b25b34b37897490208ceef2c6b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 1 May 2026 15:12:41 +1200 Subject: [PATCH] feat(insights): validate insight and project documents in CTA actions Replace the no-op `fn () => true` validators (gated by skipValidation) on the `insight` and `project` params with dedicated InsightDocument and ProjectDocument validators. They check that the injected value is a non-empty Document with the expected attributes, so a misconfigured dispatcher or unbound injection fails fast with a useful message. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CTA/Action/Databases/Indexes/Create.php | 6 +- .../Insights/Validator/InsightDocument.php | 48 +++++++++++++ .../Insights/Validator/ProjectDocument.php | 39 +++++++++++ .../Validator/InsightDocumentTest.php | 69 +++++++++++++++++++ .../Validator/ProjectDocumentTest.php | 53 ++++++++++++++ 5 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 src/Appwrite/Insights/Validator/InsightDocument.php create mode 100644 src/Appwrite/Insights/Validator/ProjectDocument.php create mode 100644 tests/unit/Insights/Validator/InsightDocumentTest.php create mode 100644 tests/unit/Insights/Validator/ProjectDocumentTest.php diff --git a/src/Appwrite/Insights/CTA/Action/Databases/Indexes/Create.php b/src/Appwrite/Insights/CTA/Action/Databases/Indexes/Create.php index a3f2487516..a72c21e284 100644 --- a/src/Appwrite/Insights/CTA/Action/Databases/Indexes/Create.php +++ b/src/Appwrite/Insights/CTA/Action/Databases/Indexes/Create.php @@ -7,6 +7,8 @@ use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Insights\CTA\Action; use Appwrite\Insights\Validator\CTAParams\DatabasesCreateIndex as DatabasesCreateIndexParams; +use Appwrite\Insights\Validator\InsightDocument as InsightDocumentValidator; +use Appwrite\Insights\Validator\ProjectDocument as ProjectDocumentValidator; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; @@ -28,8 +30,8 @@ class Create extends Action ->desc('Create a database index from an insight CTA.') ->label('scope', 'collections.write') ->param('params', [], new DatabasesCreateIndexParams(), 'CTA params describing the index to create.') - ->param('insight', null, fn () => true, 'Parent insight document.', skipValidation: true) - ->param('project', null, fn () => true, 'Project document.', skipValidation: true) + ->param('insight', null, new InsightDocumentValidator(), 'Parent insight document.') + ->param('project', null, new ProjectDocumentValidator(), 'Project document.') ->inject('dbForProject') ->inject('getDatabasesDB') ->inject('queueForDatabase') diff --git a/src/Appwrite/Insights/Validator/InsightDocument.php b/src/Appwrite/Insights/Validator/InsightDocument.php new file mode 100644 index 0000000000..6ad76d3fb4 --- /dev/null +++ b/src/Appwrite/Insights/Validator/InsightDocument.php @@ -0,0 +1,48 @@ +message; + } + + public function isArray(): bool + { + return false; + } + + public function getType(): string + { + return self::TYPE_OBJECT; + } + + public function isValid($value): bool + { + if (!$value instanceof Document) { + return false; + } + + if ($value->isEmpty()) { + return false; + } + + $type = $value->getAttribute('type'); + if (!\is_string($type) || $type === '') { + return false; + } + + if (!\is_array($value->getAttribute('ctas', []))) { + return false; + } + + return true; + } +} diff --git a/src/Appwrite/Insights/Validator/ProjectDocument.php b/src/Appwrite/Insights/Validator/ProjectDocument.php new file mode 100644 index 0000000000..89b819eacd --- /dev/null +++ b/src/Appwrite/Insights/Validator/ProjectDocument.php @@ -0,0 +1,39 @@ +message; + } + + public function isArray(): bool + { + return false; + } + + public function getType(): string + { + return self::TYPE_OBJECT; + } + + public function isValid($value): bool + { + if (!$value instanceof Document) { + return false; + } + + if ($value->isEmpty()) { + return false; + } + + return $value->getId() !== ''; + } +} diff --git a/tests/unit/Insights/Validator/InsightDocumentTest.php b/tests/unit/Insights/Validator/InsightDocumentTest.php new file mode 100644 index 0000000000..77ab865d48 --- /dev/null +++ b/tests/unit/Insights/Validator/InsightDocumentTest.php @@ -0,0 +1,69 @@ + 'insight-1', + 'type' => 'databaseIndex', + 'ctas' => [], + ]); + + $this->assertTrue($validator->isValid($insight)); + } + + public function testRejectsNonDocument(): void + { + $validator = new InsightDocument(); + + $this->assertFalse($validator->isValid('not a document')); + $this->assertFalse($validator->isValid(null)); + $this->assertFalse($validator->isValid(['type' => 'databaseIndex'])); + } + + public function testRejectsEmptyDocument(): void + { + $validator = new InsightDocument(); + + $this->assertFalse($validator->isValid(new Document())); + } + + public function testRejectsMissingType(): void + { + $validator = new InsightDocument(); + $insight = new Document([ + '$id' => 'insight-1', + 'ctas' => [], + ]); + + $this->assertFalse($validator->isValid($insight)); + } + + public function testRejectsNonArrayCtas(): void + { + $validator = new InsightDocument(); + $insight = new Document([ + '$id' => 'insight-1', + 'type' => 'databaseIndex', + 'ctas' => 'not-an-array', + ]); + + $this->assertFalse($validator->isValid($insight)); + } + + public function testReportsObjectType(): void + { + $validator = new InsightDocument(); + + $this->assertSame('object', $validator->getType()); + $this->assertFalse($validator->isArray()); + } +} diff --git a/tests/unit/Insights/Validator/ProjectDocumentTest.php b/tests/unit/Insights/Validator/ProjectDocumentTest.php new file mode 100644 index 0000000000..c053a02f4e --- /dev/null +++ b/tests/unit/Insights/Validator/ProjectDocumentTest.php @@ -0,0 +1,53 @@ + 'project-1', + 'name' => 'Test', + ]); + + $this->assertTrue($validator->isValid($project)); + } + + public function testRejectsNonDocument(): void + { + $validator = new ProjectDocument(); + + $this->assertFalse($validator->isValid('not a document')); + $this->assertFalse($validator->isValid(null)); + $this->assertFalse($validator->isValid(['$id' => 'project-1'])); + } + + public function testRejectsEmptyDocument(): void + { + $validator = new ProjectDocument(); + + $this->assertFalse($validator->isValid(new Document())); + } + + public function testRejectsMissingId(): void + { + $validator = new ProjectDocument(); + $project = new Document(['name' => 'Test']); + + $this->assertFalse($validator->isValid($project)); + } + + public function testReportsObjectType(): void + { + $validator = new ProjectDocument(); + + $this->assertSame('object', $validator->getType()); + $this->assertFalse($validator->isArray()); + } +}