mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
refactor(insights): collapse dismissal into a status field on update
Dismissal was a sub-resource (POST /v1/insights/:id/dismissals) but a dismissal is just a state transition, not a thing the client creates. Drop the dedicated endpoint and add a `status` enum (`active` | `dismissed`) to the insights schema, settable via the existing PATCH update route. The server still derives `dismissedAt` and `dismissedBy` on transition for audit/sorting, but the client-facing API is just a single status toggle. - Schema: add `status` attribute (default `active`) - Constants: add `INSIGHT_STATUSES` - Update endpoint: accept `status` param, derive dismissedAt/By on active <-> dismissed transitions - Response model: add `status` rule - Drop Insights/Dismissal/Create.php, the createInsightDismissal SDK method, the `insights.[id].dismissals.create` event, and the `insight.dismissal.create` audit - E2E: replace testCreateDismissal with testDismissViaUpdate covering both directions of the toggle Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
cd539d972a
commit
f779c7aa3b
@@ -2782,6 +2782,17 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
// Possible values: active, dismissed
|
||||
'$id' => ID::custom('status'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 16,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => 'active',
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
// Possible values: databases, collections, sites, functions
|
||||
'$id' => ID::custom('resourceType'),
|
||||
|
||||
@@ -440,14 +440,6 @@ return [
|
||||
'delete' => [
|
||||
'$description' => 'This event triggers when an insight is deleted.',
|
||||
],
|
||||
'dismissals' => [
|
||||
'$model' => Response::MODEL_INSIGHT,
|
||||
'$resource' => true,
|
||||
'$description' => 'This event triggers on any insight dismissal event.',
|
||||
'create' => [
|
||||
'$description' => 'This event triggers when an insight is dismissed.',
|
||||
],
|
||||
],
|
||||
'ctas' => [
|
||||
'$model' => Response::MODEL_INSIGHT_CTA,
|
||||
'$resource' => true,
|
||||
|
||||
@@ -453,6 +453,15 @@ const INSIGHT_SEVERITIES = [
|
||||
INSIGHT_SEVERITY_CRITICAL,
|
||||
];
|
||||
|
||||
// Insight statuses
|
||||
const INSIGHT_STATUS_ACTIVE = 'active';
|
||||
const INSIGHT_STATUS_DISMISSED = 'dismissed';
|
||||
|
||||
const INSIGHT_STATUSES = [
|
||||
INSIGHT_STATUS_ACTIVE,
|
||||
INSIGHT_STATUS_DISMISSED,
|
||||
];
|
||||
|
||||
// Insight CTA actions
|
||||
const INSIGHT_CTA_ACTION_DATABASES_CREATE_INDEX = 'databases.createIndex';
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ class Create extends Action
|
||||
'$id' => $insightId,
|
||||
'type' => $type,
|
||||
'severity' => $severity,
|
||||
'status' => INSIGHT_STATUS_ACTIVE,
|
||||
'resourceType' => $resourceType,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceInternalId' => $resourceInternalId,
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Insights\Http\Insights\Dismissal;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createInsightDismissal';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/insights/:insightId/dismissals')
|
||||
->desc('Create insight dismissal')
|
||||
->groups(['api', 'insights'])
|
||||
->label('scope', 'insights.write')
|
||||
->label('event', 'insights.[insightId].dismissals.create')
|
||||
->label('resourceType', RESOURCE_TYPE_INSIGHTS)
|
||||
->label('audits.event', 'insight.dismissal.create')
|
||||
->label('audits.resource', '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: 'createDismissal',
|
||||
description: <<<EOT
|
||||
Dismiss an insight. Stamps the current user and time on the insight without deleting it, so analyzers can see it has been acknowledged.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_INSIGHT,
|
||||
),
|
||||
]
|
||||
))
|
||||
->param('insightId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Insight ID.', false, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $insightId,
|
||||
Response $response,
|
||||
Document $user,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents
|
||||
) {
|
||||
$insight = $dbForProject->getDocument('insights', $insightId);
|
||||
|
||||
if ($insight->isEmpty()) {
|
||||
throw new Exception(Exception::INSIGHT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$insight = $dbForProject->updateDocument('insights', $insight->getId(), new Document([
|
||||
'dismissedAt' => DateTime::now(),
|
||||
'dismissedBy' => $user->getId(),
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('insightId', $insight->getId());
|
||||
|
||||
$response->dynamic($insight, Response::MODEL_INSIGHT);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Database\Validator\UID;
|
||||
@@ -61,12 +62,14 @@ class Update extends Action
|
||||
))
|
||||
->param('insightId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Insight ID.', false, ['dbForProject'])
|
||||
->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)
|
||||
->param('title', null, new Nullable(new Text(256)), 'Short, human-readable title.', true)
|
||||
->param('summary', null, new Nullable(new Text(4096, 0)), 'Markdown summary describing the insight.', true)
|
||||
->param('payload', null, new Nullable(new JSON()), 'Type-specific structured payload.', true)
|
||||
->param('ctas', null, new Nullable(new ArrayList(new JSON(), 16)), 'Array of call-to-action descriptors.', true)
|
||||
->param('analyzedAt', null, new Nullable(new DatetimeValidator()), 'Time the insight was analyzed in ISO 8601 format.', true)
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
@@ -75,12 +78,14 @@ class Update extends Action
|
||||
public function action(
|
||||
string $insightId,
|
||||
?string $severity,
|
||||
?string $status,
|
||||
?string $title,
|
||||
?string $summary,
|
||||
?array $payload,
|
||||
?array $ctas,
|
||||
?string $analyzedAt,
|
||||
Response $response,
|
||||
Document $user,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents
|
||||
) {
|
||||
@@ -95,6 +100,16 @@ class Update extends Action
|
||||
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 ($title !== null) {
|
||||
$changes['title'] = $title;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace Appwrite\Platform\Modules\Insights\Services;
|
||||
use Appwrite\Platform\Modules\Insights\Http\CTA\Execution\Create as CreateInsightCTAExecution;
|
||||
use Appwrite\Platform\Modules\Insights\Http\Insights\Create as CreateInsight;
|
||||
use Appwrite\Platform\Modules\Insights\Http\Insights\Delete as DeleteInsight;
|
||||
use Appwrite\Platform\Modules\Insights\Http\Insights\Dismissal\Create as CreateInsightDismissal;
|
||||
use Appwrite\Platform\Modules\Insights\Http\Insights\Get as GetInsight;
|
||||
use Appwrite\Platform\Modules\Insights\Http\Insights\Update as UpdateInsight;
|
||||
use Appwrite\Platform\Modules\Insights\Http\Insights\XList as ListInsights;
|
||||
@@ -22,7 +21,6 @@ class Http extends Service
|
||||
$this->addAction(ListInsights::getName(), new ListInsights());
|
||||
$this->addAction(UpdateInsight::getName(), new UpdateInsight());
|
||||
$this->addAction(DeleteInsight::getName(), new DeleteInsight());
|
||||
$this->addAction(CreateInsightDismissal::getName(), new CreateInsightDismissal());
|
||||
$this->addAction(CreateInsightCTAExecution::getName(), new CreateInsightCTAExecution());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ class Insight extends Model
|
||||
'default' => 'info',
|
||||
'example' => 'warning',
|
||||
])
|
||||
->addRule('status', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Insight status. One of active, dismissed.',
|
||||
'default' => 'active',
|
||||
'example' => 'active',
|
||||
])
|
||||
->addRule('resourceType', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Type of the resource the insight is about. Plural noun, e.g. databases, sites, functions.',
|
||||
|
||||
@@ -129,20 +129,31 @@ trait InsightsBase
|
||||
/**
|
||||
* @depends testUpdate
|
||||
*/
|
||||
public function testCreateDismissal(array $data): array
|
||||
public function testDismissViaUpdate(array $data): array
|
||||
{
|
||||
$insightId = $data['insightId'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/insights/' . $insightId . '/dismissals', $this->serverHeaders());
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/insights/' . $insightId, $this->serverHeaders(), [
|
||||
'status' => 'dismissed',
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $response['headers']['status-code']);
|
||||
$this->assertSame('dismissed', $response['body']['status']);
|
||||
$this->assertNotEmpty($response['body']['dismissedAt']);
|
||||
|
||||
$undismiss = $this->client->call(Client::METHOD_PATCH, '/insights/' . $insightId, $this->serverHeaders(), [
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $undismiss['headers']['status-code']);
|
||||
$this->assertSame('active', $undismiss['body']['status']);
|
||||
$this->assertEmpty($undismiss['body']['dismissedAt']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateDismissal
|
||||
* @depends testDismissViaUpdate
|
||||
*/
|
||||
public function testCreateCTAExecution(array $data): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user