From 6d0eab258373327b4d2a67ae6efc859aac39a210 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 06:07:23 +0000 Subject: [PATCH] refactor(advisor): make insights API read-only in CE Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/8d7897b5-ac68-487d-954a-be717380bf66 Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com> --- app/config/collections/platform.php | 14 + app/config/roles.php | 1 - app/config/scopes/project.php | 10 +- app/config/services.php | 8 +- app/init/constants.php | 50 +- app/init/database/filters.php | 16 + .../Insights/Enums/InsightCTAMethod.php | 8 + .../Insights/Enums/InsightCTAService.php | 11 + .../Insights/Enums/InsightSeverity.php | 10 + .../Modules/Insights/Enums/InsightStatus.php | 9 + .../Modules/Insights/Enums/InsightType.php | 16 + .../Modules/Insights/Enums/ReportType.php | 10 + .../Modules/Insights/Http/Insights/Delete.php | 113 --- .../Modules/Insights/Http/Insights/Get.php | 2 +- .../Modules/Insights/Http/Insights/Update.php | 135 --- .../Modules/Insights/Http/Insights/XList.php | 2 +- .../Insights/Http/Manager/Insights/Create.php | 188 ----- .../Modules/Insights/Http/Reports/Create.php | 117 --- .../Modules/Insights/Http/Reports/Delete.php | 109 --- .../Modules/Insights/Http/Reports/Get.php | 2 +- .../Modules/Insights/Http/Reports/Update.php | 115 --- .../Modules/Insights/Http/Reports/XList.php | 2 +- .../Modules/Insights/Services/Http.php | 14 - src/Appwrite/Utopia/Response/Model/Report.php | 7 + tests/e2e/Scopes/ProjectCustom.php | 2 - tests/e2e/Services/Insights/InsightsBase.php | 783 ++---------------- .../Insights/InsightsCustomServerTest.php | 34 +- 27 files changed, 211 insertions(+), 1577 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Insights/Enums/InsightCTAMethod.php create mode 100644 src/Appwrite/Platform/Modules/Insights/Enums/InsightCTAService.php create mode 100644 src/Appwrite/Platform/Modules/Insights/Enums/InsightSeverity.php create mode 100644 src/Appwrite/Platform/Modules/Insights/Enums/InsightStatus.php create mode 100644 src/Appwrite/Platform/Modules/Insights/Enums/InsightType.php create mode 100644 src/Appwrite/Platform/Modules/Insights/Enums/ReportType.php delete mode 100644 src/Appwrite/Platform/Modules/Insights/Http/Insights/Delete.php delete mode 100644 src/Appwrite/Platform/Modules/Insights/Http/Insights/Update.php delete mode 100644 src/Appwrite/Platform/Modules/Insights/Http/Manager/Insights/Create.php delete mode 100644 src/Appwrite/Platform/Modules/Insights/Http/Reports/Create.php delete mode 100644 src/Appwrite/Platform/Modules/Insights/Http/Reports/Delete.php delete mode 100644 src/Appwrite/Platform/Modules/Insights/Http/Reports/Update.php diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index b156e4aefc..c70a976677 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -2056,6 +2056,20 @@ $platformCollections = [ 'array' => true, 'filters' => [], ], + [ + // Virtual attribute — insights live in the `insights` collection + // back-referenced by `reportInternalId`. The subQuery filter joins + // them at read time. + '$id' => ID::custom('insights'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 65535, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['subQueryReportInsights'], + ], [ '$id' => ID::custom('analyzedAt'), 'type' => Database::VAR_DATETIME, diff --git a/app/config/roles.php b/app/config/roles.php index cb4b178a29..db4437216c 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -106,7 +106,6 @@ $admins = [ 'insights.read', 'insights.write', 'reports.read', - 'reports.write', ]; return [ diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php index 3b8a86e220..264a6fc731 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -368,11 +368,7 @@ return [ 'category' => 'Other', ], 'insights.write' => [ - 'description' => 'Access to update, dismiss, and delete insights.', - 'category' => 'Other', - ], - 'insights.manager' => [ - 'description' => 'Internal-only: ingest insights produced by Appwrite analyzers (edge, executor, …). Not granted to user roles.', + 'description' => 'Access to ingest analyzer reports and insights.', 'category' => 'Other', ], @@ -381,8 +377,4 @@ return [ 'description' => 'Access to read analyzer reports and their insights.', 'category' => 'Other', ], - 'reports.write' => [ - 'description' => 'Access to create, update, and delete analyzer reports.', - 'category' => 'Other', - ], ]; diff --git a/app/config/services.php b/app/config/services.php index a285224b1e..6ce828c4c0 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -309,10 +309,10 @@ return [ 'icon' => '/images/services/messaging.png', 'platforms' => ['client', 'server', 'console'], ], - 'insights' => [ - 'key' => 'insights', - 'name' => 'Insights', - 'subtitle' => 'The Insights service surfaces actionable reports about your project resources, with CTA descriptors for one-click remediation in the console.', + 'advisor' => [ + 'key' => 'advisor', + 'name' => 'Advisor', + 'subtitle' => 'The Advisor service surfaces actionable reports about your project resources, with CTA descriptors for one-click remediation in the console.', 'description' => '/docs/services/insights.md', 'controller' => '', // Uses modules 'sdk' => true, diff --git a/app/init/constants.php b/app/init/constants.php index 299499ddab..aa732383f5 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -1,6 +1,12 @@ value; // legacy databases.createIndex +const INSIGHT_TYPE_TABLES_DB_INDEX = InsightType::TABLES_DB_INDEX->value; // tablesDB.createIndex +const INSIGHT_TYPE_DOCUMENTS_DB_INDEX = InsightType::DOCUMENTS_DB_INDEX->value; // documentsDB.createIndex +const INSIGHT_TYPE_VECTORS_DB_INDEX = InsightType::VECTORS_DB_INDEX->value; // vectorsDB.createIndex +const INSIGHT_TYPE_DATABASE_PERFORMANCE = InsightType::DATABASE_PERFORMANCE->value; +const INSIGHT_TYPE_SITE_PERFORMANCE = InsightType::SITE_PERFORMANCE->value; +const INSIGHT_TYPE_SITE_ACCESSIBILITY = InsightType::SITE_ACCESSIBILITY->value; +const INSIGHT_TYPE_SITE_SEO = InsightType::SITE_SEO->value; +const INSIGHT_TYPE_FUNCTION_PERFORMANCE = InsightType::FUNCTION_PERFORMANCE->value; const INSIGHT_TYPES = [ INSIGHT_TYPE_DATABASE_INDEX, @@ -452,18 +458,18 @@ const INSIGHT_TYPES = [ // Public API services (SDK namespaces) that an insight CTA's `service` can reference. // Analyzers must pick the one matching the engine the resource lives in. -const INSIGHT_CTA_SERVICE_DATABASES = 'databases'; // legacy -const INSIGHT_CTA_SERVICE_TABLES_DB = 'tablesDB'; -const INSIGHT_CTA_SERVICE_DOCUMENTS_DB = 'documentsDB'; -const INSIGHT_CTA_SERVICE_VECTORS_DB = 'vectorsDB'; +const INSIGHT_CTA_SERVICE_DATABASES = InsightCTAService::DATABASES->value; // legacy +const INSIGHT_CTA_SERVICE_TABLES_DB = InsightCTAService::TABLES_DB->value; +const INSIGHT_CTA_SERVICE_DOCUMENTS_DB = InsightCTAService::DOCUMENTS_DB->value; +const INSIGHT_CTA_SERVICE_VECTORS_DB = InsightCTAService::VECTORS_DB->value; // Public API method names that an insight CTA's `method` can reference for index suggestions. -const INSIGHT_CTA_METHOD_CREATE_INDEX = 'createIndex'; +const INSIGHT_CTA_METHOD_CREATE_INDEX = InsightCTAMethod::CREATE_INDEX->value; // Insight severities -const INSIGHT_SEVERITY_INFO = 'info'; -const INSIGHT_SEVERITY_WARNING = 'warning'; -const INSIGHT_SEVERITY_CRITICAL = 'critical'; +const INSIGHT_SEVERITY_INFO = InsightSeverity::INFO->value; +const INSIGHT_SEVERITY_WARNING = InsightSeverity::WARNING->value; +const INSIGHT_SEVERITY_CRITICAL = InsightSeverity::CRITICAL->value; const INSIGHT_SEVERITIES = [ INSIGHT_SEVERITY_INFO, @@ -472,8 +478,8 @@ const INSIGHT_SEVERITIES = [ ]; // Insight statuses -const INSIGHT_STATUS_ACTIVE = 'active'; -const INSIGHT_STATUS_DISMISSED = 'dismissed'; +const INSIGHT_STATUS_ACTIVE = InsightStatus::ACTIVE->value; +const INSIGHT_STATUS_DISMISSED = InsightStatus::DISMISSED->value; const INSIGHT_STATUSES = [ INSIGHT_STATUS_ACTIVE, @@ -481,9 +487,9 @@ const INSIGHT_STATUSES = [ ]; // Report types -const REPORT_TYPE_LIGHTHOUSE = 'lighthouse'; -const REPORT_TYPE_AUDIT = 'audit'; -const REPORT_TYPE_DATABASE_ANALYZER = 'databaseAnalyzer'; +const REPORT_TYPE_LIGHTHOUSE = ReportType::LIGHTHOUSE->value; +const REPORT_TYPE_AUDIT = ReportType::AUDIT->value; +const REPORT_TYPE_DATABASE_ANALYZER = ReportType::DATABASE_ANALYZER->value; const REPORT_TYPES = [ REPORT_TYPE_LIGHTHOUSE, diff --git a/app/init/database/filters.php b/app/init/database/filters.php index f6afb28304..351b8053f3 100644 --- a/app/init/database/filters.php +++ b/app/init/database/filters.php @@ -484,8 +484,24 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return $database->getAuthorization()->skip(fn () => $database ->find('insightCTAs', [ + Query::equal('projectInternalId', [$document->getAttribute('projectInternalId')]), Query::equal('insightInternalId', [$document->getSequence()]), Query::limit(APP_LIMIT_SUBQUERY), ])); } ); + +Database::addFilter( + 'subQueryReportInsights', + function (mixed $value) { + return; + }, + function (mixed $value, Document $document, Database $database) { + return $database->getAuthorization()->skip(fn () => $database + ->find('insights', [ + Query::equal('projectInternalId', [$document->getAttribute('projectInternalId')]), + Query::equal('reportInternalId', [$document->getSequence()]), + Query::limit(APP_LIMIT_SUBQUERY), + ])); + } +); diff --git a/src/Appwrite/Platform/Modules/Insights/Enums/InsightCTAMethod.php b/src/Appwrite/Platform/Modules/Insights/Enums/InsightCTAMethod.php new file mode 100644 index 0000000000..c8b84d1330 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Insights/Enums/InsightCTAMethod.php @@ -0,0 +1,8 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE) - ->setHttpPath('/v1/reports/:reportId/insights/:insightId') - ->desc('Delete insight') - ->groups(['api', 'insights']) - ->label('scope', 'insights.write') - ->label('event', 'reports.[reportId].insights.[insightId].delete') - ->label('resourceType', RESOURCE_TYPE_INSIGHTS) - ->label('audits.event', 'insight.delete') - ->label('audits.resource', 'report/{request.reportId}/insight/{request.insightId}') - ->label('abuse-key', 'projectId:{projectId},userId:{userId}') - ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT) - ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) - ->label('sdk', new Method( - namespace: 'insights', - group: 'insights', - name: 'delete', - description: <<param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Parent report ID.', false, ['dbForPlatform']) - ->param('insightId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Insight ID.', false, ['dbForPlatform']) - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->inject('queueForEvents') - ->callback($this->action(...)); - } - - public function action( - string $reportId, - string $insightId, - Response $response, - Document $project, - Database $dbForPlatform, - Event $queueForEvents - ) { - $report = $dbForPlatform->getDocument('reports', $reportId); - - if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) { - throw new Exception(Exception::REPORT_NOT_FOUND); - } - - $insight = $dbForPlatform->getDocument('insights', $insightId); - - if ( - $insight->isEmpty() - || $insight->getAttribute('projectInternalId') !== $project->getSequence() - || $insight->getAttribute('reportInternalId') !== $report->getSequence() - ) { - throw new Exception(Exception::INSIGHT_NOT_FOUND); - } - - // Cascade delete child CTAs first. - $childCTAs = $dbForPlatform->find('insightCTAs', [ - Query::equal('insightInternalId', [$insight->getSequence()]), - Query::limit(APP_LIMIT_COUNT), - ]); - - foreach ($childCTAs as $cta) { - $dbForPlatform->deleteDocument('insightCTAs', $cta->getId()); - } - - if (!$dbForPlatform->deleteDocument('insights', $insight->getId())) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove insight from DB'); - } - - $queueForEvents - ->setParam('reportId', $report->getId()) - ->setParam('insightId', $insight->getId()) - ->setPayload($response->output($insight, Response::MODEL_INSIGHT)); - - $response->noContent(); - } -} diff --git a/src/Appwrite/Platform/Modules/Insights/Http/Insights/Get.php b/src/Appwrite/Platform/Modules/Insights/Http/Insights/Get.php index ea3c88349c..126ea759ae 100644 --- a/src/Appwrite/Platform/Modules/Insights/Http/Insights/Get.php +++ b/src/Appwrite/Platform/Modules/Insights/Http/Insights/Get.php @@ -32,7 +32,7 @@ class Get extends Action ->label('scope', 'insights.read') ->label('resourceType', RESOURCE_TYPE_INSIGHTS) ->label('sdk', new Method( - namespace: 'insights', + namespace: 'advisor', group: 'insights', name: 'get', description: <<setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) - ->setHttpPath('/v1/reports/:reportId/insights/:insightId') - ->desc('Update insight') - ->groups(['api', 'insights']) - ->label('scope', 'insights.write') - ->label('event', 'reports.[reportId].insights.[insightId].update') - ->label('resourceType', RESOURCE_TYPE_INSIGHTS) - ->label('audits.event', 'insight.update') - ->label('audits.resource', 'report/{request.reportId}/insight/{response.$id}') - ->label('abuse-key', 'projectId:{projectId},userId:{userId}') - ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT) - ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) - ->label('sdk', new Method( - namespace: 'insights', - group: 'insights', - name: 'update', - description: <<param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Parent report ID.', false, ['dbForPlatform']) - ->param('insightId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Insight ID.', false, ['dbForPlatform']) - ->param('severity', null, new Nullable(new WhiteList(INSIGHT_SEVERITIES, true)), 'Insight severity. One of `info`, `warning`, `critical`.', true) - ->param('status', null, new Nullable(new WhiteList(INSIGHT_STATUSES, true)), 'Insight status. Set to `dismissed` to dismiss the insight, `active` to undo a dismissal.', true) - ->inject('response') - ->inject('user') - ->inject('project') - ->inject('dbForPlatform') - ->inject('queueForEvents') - ->callback($this->action(...)); - } - - public function action( - string $reportId, - string $insightId, - ?string $severity, - ?string $status, - Response $response, - Document $user, - Document $project, - Database $dbForPlatform, - Event $queueForEvents - ) { - $report = $dbForPlatform->getDocument('reports', $reportId); - - if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) { - throw new Exception(Exception::REPORT_NOT_FOUND); - } - - $insight = $dbForPlatform->getDocument('insights', $insightId); - - if ( - $insight->isEmpty() - || $insight->getAttribute('projectInternalId') !== $project->getSequence() - || $insight->getAttribute('reportInternalId') !== $report->getSequence() - ) { - throw new Exception(Exception::INSIGHT_NOT_FOUND); - } - - $changes = []; - - if ($severity !== null) { - $changes['severity'] = $severity; - } - if ($status !== null && $status !== $insight->getAttribute('status')) { - $changes['status'] = $status; - if ($status === INSIGHT_STATUS_DISMISSED) { - $changes['dismissedAt'] = DateTime::now(); - $changes['dismissedBy'] = $user->getId(); - } else { - $changes['dismissedAt'] = null; - $changes['dismissedBy'] = ''; - } - } - - if ($changes !== []) { - foreach ($changes as $key => $value) { - $insight->setAttribute($key, $value); - } - $insight = $dbForPlatform->updateDocument('insights', $insight->getId(), $insight); - } - - $queueForEvents - ->setParam('reportId', $report->getId()) - ->setParam('insightId', $insight->getId()); - - $response->dynamic($insight, Response::MODEL_INSIGHT); - } -} diff --git a/src/Appwrite/Platform/Modules/Insights/Http/Insights/XList.php b/src/Appwrite/Platform/Modules/Insights/Http/Insights/XList.php index dba5b6da7b..111d2f167f 100644 --- a/src/Appwrite/Platform/Modules/Insights/Http/Insights/XList.php +++ b/src/Appwrite/Platform/Modules/Insights/Http/Insights/XList.php @@ -38,7 +38,7 @@ class XList extends Action ->label('scope', 'insights.read') ->label('resourceType', RESOURCE_TYPE_INSIGHTS) ->label('sdk', new Method( - namespace: 'insights', + namespace: 'advisor', group: 'insights', name: 'list', description: <<