mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
refactor(insights): promote CTAs to own collection with backref
Embedding CTAs as a 16384-byte JSON blob on `insights` was the wrong shape — they're real documents with their own lifecycle. Move them out. Schema: - New platform `ctas` collection. Each row carries `projectInternalId`, `projectId`, `insightInternalId`, `insightId` (backref), plus the CTA fields: `key`, `label`, `service`, `method`, `params`. - Indexes: `(projectInternalId, insightInternalId)` for the subquery lookup and a UNIQUE `(insightInternalId, key)` so the per-insight uniqueness invariant lives at the DB layer (not just in PHP). - The `ctas` field on `insights` becomes a virtual attribute backed by a new `subQueryInsightCTAs` filter that joins child docs at read time. Consumers still get CTAs embedded on the insight response — one round-trip from their perspective. - The CTA descriptor's within-insight identifier renamed `id` → `key` (clashed with the document `$id`). Validator updated. Endpoints: - Manager Create now persists CTAs as separate `ctas` documents after the parent insight, then re-fetches the insight so the response carries the freshly-joined CTA list. - User Update trimmed to user-controlled state only (`severity`, `status`). `title`, `summary`, `payload`, `ctas`, and `analyzedAt` are analyzer-controlled — analyzers re-ingest by deleting and POSTing again to the manager endpoint. - Insight Delete cascades to CTAs. - Report Delete cascades through Insights → CTAs. Response model: - InsightCTA gains the standard document headers (`$id`, `$createdAt`, `$updatedAt`) and an `insightId` backref. The caller-supplied identifier is now `key`. Tests: - E2E sampleCTA factory uses `key` everywhere; testCreate asserts the freshly-created CTA carries `$id`, `$createdAt`, `insightId`, and the right shape. - Dropped the testUpdate*CTA* tests — user Update no longer accepts CTAs. testDismissViaUpdate now depends on testUpdate directly. - Unit tests rewritten to validate `key` instead of `id`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2261,6 +2261,10 @@ $platformCollections = [
|
||||
'filters' => ['json'],
|
||||
],
|
||||
[
|
||||
// Virtual attribute — CTAs live in their own `ctas` collection
|
||||
// back-referenced by `insightInternalId`. The subQuery filter
|
||||
// joins them in at read time, so consumers still see them
|
||||
// embedded on the insight response.
|
||||
'$id' => ID::custom('ctas'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
@@ -2269,7 +2273,7 @@ $platformCollections = [
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
'filters' => ['subQueryInsightCTAs'],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('analyzedAt'),
|
||||
@@ -2364,6 +2368,140 @@ $platformCollections = [
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'ctas' => [
|
||||
'$collection' => ID::custom(Database::METADATA),
|
||||
'$id' => ID::custom('ctas'),
|
||||
'name' => 'Insight CTAs',
|
||||
'attributes' => [
|
||||
[
|
||||
'$id' => ID::custom('projectInternalId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('projectId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('insightInternalId'),
|
||||
'type' => Database::VAR_ID,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('insightId'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
// Caller-supplied identifier, unique within the parent insight.
|
||||
'$id' => ID::custom('key'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => Database::LENGTH_KEY,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('label'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 256,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
// SDK namespace (databases / tablesDB / documentsDB / vectorsDB / …).
|
||||
'$id' => ID::custom('service'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 64,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('method'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 64,
|
||||
'signed' => true,
|
||||
'required' => true,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('params'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 16384,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
'$id' => ID::custom('_key_project'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['projectInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY],
|
||||
'orders' => [Database::ORDER_ASC],
|
||||
],
|
||||
[
|
||||
// Primary lookup — fetch all CTAs of an insight (subQuery filter).
|
||||
'$id' => ID::custom('_key_project_insight'),
|
||||
'type' => Database::INDEX_KEY,
|
||||
'attributes' => ['projectInternalId', 'insightInternalId'],
|
||||
'lengths' => [Database::LENGTH_KEY, 0],
|
||||
'orders' => [],
|
||||
],
|
||||
[
|
||||
// Enforce per-insight key uniqueness at the DB layer.
|
||||
'$id' => ID::custom('_key_insight_key'),
|
||||
'type' => Database::INDEX_UNIQUE,
|
||||
'attributes' => ['insightInternalId', 'key'],
|
||||
'lengths' => [0, Database::LENGTH_KEY],
|
||||
'orders' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Organization API keys subquery
|
||||
|
||||
@@ -475,3 +475,17 @@ Database::addFilter(
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
Database::addFilter(
|
||||
'subQueryInsightCTAs',
|
||||
function (mixed $value) {
|
||||
return;
|
||||
},
|
||||
function (mixed $value, Document $document, Database $database) {
|
||||
return $database->getAuthorization()->skip(fn () => $database
|
||||
->find('ctas', [
|
||||
Query::equal('insightInternalId', [$document->getSequence()]),
|
||||
Query::limit(APP_LIMIT_SUBQUERY),
|
||||
]));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ class CTAs extends Validator
|
||||
{
|
||||
public const MAX_COUNT_DEFAULT = 16;
|
||||
|
||||
protected string $message = 'Value must be an array of CTA descriptors. Each entry must define `id`, `label`, `service`, `method`, and an optional `params` object.';
|
||||
protected string $message = 'Value must be an array of CTA descriptors. Each entry must define `key`, `label`, `service`, `method`, and an optional `params` object.';
|
||||
|
||||
public function __construct(protected int $maxCount = self::MAX_COUNT_DEFAULT)
|
||||
{
|
||||
@@ -45,7 +45,7 @@ class CTAs extends Validator
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (['id', 'label', 'service', 'method'] as $required) {
|
||||
foreach (['key', 'label', 'service', 'method'] as $required) {
|
||||
if (!isset($entry[$required]) || !\is_string($entry[$required]) || $entry[$required] === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -76,6 +77,16 @@ class Delete extends Action
|
||||
throw new Exception(Exception::INSIGHT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Cascade delete child CTAs first.
|
||||
$childCTAs = $dbForPlatform->find('ctas', [
|
||||
Query::equal('insightInternalId', [$insight->getSequence()]),
|
||||
Query::limit(APP_LIMIT_COUNT),
|
||||
]);
|
||||
|
||||
foreach ($childCTAs as $cta) {
|
||||
$dbForPlatform->deleteDocument('ctas', $cta->getId());
|
||||
}
|
||||
|
||||
if (!$dbForPlatform->deleteDocument('insights', $insight->getId())) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove insight from DB');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Insights\Http\Insights;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Insights\Validator\CTAs as CTAsValidator;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
@@ -12,15 +11,20 @@ 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;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\JSON;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/**
|
||||
* User-facing Update endpoint.
|
||||
*
|
||||
* Limited to user-controlled state: dismissal (status), and severity overrides.
|
||||
* Analyzer-controlled fields (title, summary, payload, ctas, analyzedAt) flow
|
||||
* through the manager-only Create endpoint — analyzers re-ingest by deleting
|
||||
* the stale insight and submitting a fresh one.
|
||||
*/
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
@@ -50,7 +54,7 @@ class Update extends Action
|
||||
group: 'insights',
|
||||
name: 'update',
|
||||
description: <<<EOT
|
||||
Update an insight. Pass only the attributes you want to change. Set `status` to `dismissed` to dismiss the insight (the dismissal timestamp and user are recorded automatically) or back to `active` to undo a dismissal.
|
||||
Update user-controlled state on an insight. Set `status` to `dismissed` to dismiss it (the dismissal timestamp and user are recorded automatically) or back to `active` to undo a dismissal. `severity` lets users escalate or downgrade the analyzer's classification.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
@@ -63,11 +67,6 @@ class Update extends Action
|
||||
->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)
|
||||
->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 CTAsValidator()), '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('project')
|
||||
@@ -80,11 +79,6 @@ class Update extends Action
|
||||
string $insightId,
|
||||
?string $severity,
|
||||
?string $status,
|
||||
?string $title,
|
||||
?string $summary,
|
||||
?array $payload,
|
||||
?array $ctas,
|
||||
?string $analyzedAt,
|
||||
Response $response,
|
||||
Document $user,
|
||||
Document $project,
|
||||
@@ -112,38 +106,6 @@ class Update extends Action
|
||||
$changes['dismissedBy'] = '';
|
||||
}
|
||||
}
|
||||
if ($title !== null) {
|
||||
$changes['title'] = $title;
|
||||
}
|
||||
if ($summary !== null) {
|
||||
$changes['summary'] = $summary;
|
||||
}
|
||||
if ($payload !== null) {
|
||||
$changes['payload'] = $payload;
|
||||
}
|
||||
if ($ctas !== null) {
|
||||
$seen = [];
|
||||
$normalized = [];
|
||||
foreach ($ctas as $cta) {
|
||||
$ctaId = (string) $cta['id'];
|
||||
if (isset($seen[$ctaId])) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'CTA `id` values must be unique within an insight.');
|
||||
}
|
||||
$seen[$ctaId] = true;
|
||||
|
||||
$normalized[] = [
|
||||
'id' => $ctaId,
|
||||
'label' => (string) $cta['label'],
|
||||
'service' => (string) $cta['service'],
|
||||
'method' => (string) $cta['method'],
|
||||
'params' => $cta['params'] ?? new \stdClass(),
|
||||
];
|
||||
}
|
||||
$changes['ctas'] = $normalized;
|
||||
}
|
||||
if ($analyzedAt !== null) {
|
||||
$changes['analyzedAt'] = $analyzedAt;
|
||||
}
|
||||
|
||||
if ($changes !== []) {
|
||||
foreach ($changes as $key => $value) {
|
||||
|
||||
@@ -85,7 +85,7 @@ class Create extends Action
|
||||
->param('title', '', new Text(256), 'Short, human-readable title.')
|
||||
->param('summary', '', new Text(4096, 0), 'Markdown summary describing the insight.', true)
|
||||
->param('payload', null, new Nullable(new JSON()), 'Type-specific structured payload.', true)
|
||||
->param('ctas', [], new CTAsValidator(), 'Array of call-to-action descriptors. Each must contain `id`, `label`, `service`, `method`, and an optional `params` object.', true)
|
||||
->param('ctas', [], new CTAsValidator(), 'Array of call-to-action descriptors. Each must contain `key` (unique within the insight), `label`, `service`, `method`, and an optional `params` object.', true)
|
||||
->param('analyzedAt', null, new Nullable(new DatetimeValidator()), 'Time the insight was analyzed in ISO 8601 format. Defaults to now.', true)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
@@ -133,14 +133,14 @@ class Create extends Action
|
||||
$normalizedCTAs = [];
|
||||
|
||||
foreach ($ctas as $cta) {
|
||||
$ctaId = (string) $cta['id'];
|
||||
if (isset($seen[$ctaId])) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'CTA `id` values must be unique within an insight.');
|
||||
$key = (string) $cta['key'];
|
||||
if (isset($seen[$key])) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'CTA `key` values must be unique within an insight.');
|
||||
}
|
||||
$seen[$ctaId] = true;
|
||||
$seen[$key] = true;
|
||||
|
||||
$normalizedCTAs[] = [
|
||||
'id' => $ctaId,
|
||||
'key' => $key,
|
||||
'label' => (string) $cta['label'],
|
||||
'service' => (string) $cta['service'],
|
||||
'method' => (string) $cta['method'],
|
||||
@@ -167,7 +167,6 @@ class Create extends Action
|
||||
'title' => $title,
|
||||
'summary' => $summary,
|
||||
'payload' => $payload,
|
||||
'ctas' => $normalizedCTAs,
|
||||
'analyzedAt' => $analyzedAt,
|
||||
'dismissedAt' => null,
|
||||
'dismissedBy' => '',
|
||||
@@ -176,6 +175,25 @@ class Create extends Action
|
||||
throw new Exception(Exception::INSIGHT_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
foreach ($normalizedCTAs as $cta) {
|
||||
$dbForPlatform->createDocument('ctas', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'projectInternalId' => $project->getSequence(),
|
||||
'projectId' => $project->getId(),
|
||||
'insightInternalId' => $insight->getSequence(),
|
||||
'insightId' => $insight->getId(),
|
||||
'key' => $cta['key'],
|
||||
'label' => $cta['label'],
|
||||
'service' => $cta['service'],
|
||||
'method' => $cta['method'],
|
||||
'params' => $cta['params'],
|
||||
]));
|
||||
}
|
||||
|
||||
// Re-fetch so the subQueryInsightCTAs filter embeds the freshly-created
|
||||
// CTA documents on the response — keeps a single round-trip for callers.
|
||||
$insight = $dbForPlatform->getDocument('insights', $insight->getId());
|
||||
|
||||
$queueForEvents->setParam('insightId', $insight->getId());
|
||||
|
||||
$response
|
||||
|
||||
@@ -84,6 +84,15 @@ class Delete extends Action
|
||||
]);
|
||||
|
||||
foreach ($childInsights as $insight) {
|
||||
// Cascade through CTAs first.
|
||||
$childCTAs = $dbForPlatform->find('ctas', [
|
||||
Query::equal('insightInternalId', [$insight->getSequence()]),
|
||||
Query::limit(APP_LIMIT_COUNT),
|
||||
]);
|
||||
foreach ($childCTAs as $cta) {
|
||||
$dbForPlatform->deleteDocument('ctas', $cta->getId());
|
||||
}
|
||||
|
||||
$dbForPlatform->deleteDocument('insights', $insight->getId());
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,33 @@ class InsightCTA extends Model
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->addRule('id', [
|
||||
->addRule('$id', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'CTA identifier, unique within the parent insight.',
|
||||
'description' => 'CTA document ID.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('$createdAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'CTA creation date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('$updatedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'CTA update date in ISO 8601 format.',
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('insightId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'ID of the insight that owns this CTA.',
|
||||
'default' => '',
|
||||
'example' => '5e5ea5c16897e',
|
||||
])
|
||||
->addRule('key', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Caller-supplied identifier, unique within the parent insight.',
|
||||
'default' => '',
|
||||
'example' => 'createIndex',
|
||||
])
|
||||
|
||||
@@ -84,10 +84,10 @@ trait InsightsBase
|
||||
* - `documentsDB` → service `documentsDB`, method `createIndex` (params use collectionId/attributes)
|
||||
* - `vectorsDB` → service `vectorsDB`, method `createIndex` (params use collectionId/attributes)
|
||||
*/
|
||||
protected function sampleCTA(string $id = 'createIndex', string $engine = 'tablesDB'): array
|
||||
protected function sampleCTA(string $key = 'createIndex', string $engine = 'tablesDB'): array
|
||||
{
|
||||
$base = [
|
||||
'id' => $id,
|
||||
'key' => $key,
|
||||
'label' => 'Create missing index',
|
||||
'method' => 'createIndex',
|
||||
];
|
||||
@@ -349,11 +349,15 @@ trait InsightsBase
|
||||
$this->assertSame('orders', $insight['body']['parentResourceId']);
|
||||
$this->assertSame('Missing index on collection orders', $insight['body']['title']);
|
||||
$this->assertCount(1, $insight['body']['ctas']);
|
||||
$this->assertSame('createIndex', $insight['body']['ctas'][0]['id']);
|
||||
$this->assertSame('createIndex', $insight['body']['ctas'][0]['key']);
|
||||
$this->assertSame($insightId, $insight['body']['ctas'][0]['insightId']);
|
||||
$this->assertSame('Create missing index', $insight['body']['ctas'][0]['label']);
|
||||
$this->assertSame('tablesDB', $insight['body']['ctas'][0]['service']);
|
||||
$this->assertSame('createIndex', $insight['body']['ctas'][0]['method']);
|
||||
$this->assertSame('orders', $insight['body']['ctas'][0]['params']['tableId']);
|
||||
$this->assertSame(['status'], $insight['body']['ctas'][0]['params']['columns']);
|
||||
$this->assertArrayHasKey('$id', $insight['body']['ctas'][0]);
|
||||
$this->assertArrayHasKey('$createdAt', $insight['body']['ctas'][0]);
|
||||
$this->assertEmpty($insight['body']['dismissedAt']);
|
||||
$this->assertEmpty($insight['body']['dismissedBy']);
|
||||
|
||||
@@ -480,8 +484,8 @@ trait InsightsBase
|
||||
'resourceId' => 'main',
|
||||
'title' => 'Should not be created',
|
||||
'ctas' => [
|
||||
['id' => 'dup', 'label' => 'A', 'service' => 'databases', 'method' => 'createIndex'],
|
||||
['id' => 'dup', 'label' => 'B', 'service' => 'databases', 'method' => 'createIndex'],
|
||||
['key' => 'dup', 'label' => 'A', 'service' => 'databases', 'method' => 'createIndex'],
|
||||
['key' => 'dup', 'label' => 'B', 'service' => 'databases', 'method' => 'createIndex'],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -498,7 +502,7 @@ trait InsightsBase
|
||||
'resourceId' => 'main',
|
||||
'title' => 'Should not be created',
|
||||
'ctas' => [
|
||||
['id' => '', 'label' => 'Has empty id', 'service' => 'databases', 'method' => 'createIndex'],
|
||||
['key' => '', 'label' => 'Has empty id', 'service' => 'databases', 'method' => 'createIndex'],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -514,7 +518,7 @@ trait InsightsBase
|
||||
'resourceId' => 'main',
|
||||
'title' => 'Should not be created',
|
||||
'ctas' => [
|
||||
['id' => 'createIndex', 'label' => 'Missing method', 'service' => 'tablesDB'],
|
||||
['key' => 'createIndex', 'label' => 'Missing method', 'service' => 'tablesDB'],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -530,7 +534,7 @@ trait InsightsBase
|
||||
'resourceId' => 'main',
|
||||
'title' => 'Should not be created',
|
||||
'ctas' => [
|
||||
['id' => 'createIndex', 'label' => 'Missing service', 'method' => 'createIndex'],
|
||||
['key' => 'createIndex', 'label' => 'Missing service', 'method' => 'createIndex'],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -542,7 +546,7 @@ trait InsightsBase
|
||||
$ctas = [];
|
||||
for ($i = 0; $i < 17; $i++) {
|
||||
$ctas[] = [
|
||||
'id' => 'cta-' . $i,
|
||||
'key' => 'cta-' . $i,
|
||||
'label' => 'CTA ' . $i,
|
||||
'service' => 'databases',
|
||||
'method' => 'createIndex',
|
||||
@@ -727,40 +731,6 @@ trait InsightsBase
|
||||
/**
|
||||
* @depends testUpdate
|
||||
*/
|
||||
public function testUpdateRejectsDuplicateCTAIds(array $data): array
|
||||
{
|
||||
$response = $this->updateInsight($data['insightId'], [
|
||||
'ctas' => [
|
||||
['id' => 'dup', 'label' => 'A', 'service' => 'databases', 'method' => 'createIndex'],
|
||||
['id' => 'dup', 'label' => 'B', 'service' => 'databases', 'method' => 'createIndex'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
$this->assertSame('general_argument_invalid', $response['body']['type']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testUpdateRejectsDuplicateCTAIds
|
||||
*/
|
||||
public function testUpdateRejectsCTAWithEmptyFields(array $data): array
|
||||
{
|
||||
$response = $this->updateInsight($data['insightId'], [
|
||||
'ctas' => [
|
||||
['id' => '', 'label' => 'Has empty id', 'service' => 'databases', 'method' => 'createIndex'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame(400, $response['headers']['status-code']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testUpdateRejectsCTAWithEmptyFields
|
||||
*/
|
||||
public function testDismissViaUpdate(array $data): array
|
||||
{
|
||||
$dismissed = $this->updateInsight($data['insightId'], ['status' => 'dismissed']);
|
||||
|
||||
@@ -28,7 +28,7 @@ class CTAsTest extends TestCase
|
||||
$validator = new CTAs();
|
||||
|
||||
$this->assertTrue($validator->isValid([[
|
||||
'id' => 'createIndex',
|
||||
'key' => 'createIndex',
|
||||
'label' => 'Create missing index',
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -44,7 +44,7 @@ class CTAsTest extends TestCase
|
||||
$validator = new CTAs();
|
||||
|
||||
$this->assertTrue($validator->isValid([[
|
||||
'id' => 'createIndex',
|
||||
'key' => 'createIndex',
|
||||
'label' => 'Create missing index',
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -55,10 +55,10 @@ class CTAsTest extends TestCase
|
||||
{
|
||||
$validator = new CTAs();
|
||||
|
||||
$this->assertFalse($validator->isValid([['id' => 'x']]));
|
||||
$this->assertFalse($validator->isValid([['id' => 'x', 'label' => 'y']]));
|
||||
$this->assertFalse($validator->isValid([['id' => 'x', 'label' => 'y', 'service' => 'tablesDB']]));
|
||||
$this->assertFalse($validator->isValid([['id' => 'x', 'label' => 'y', 'method' => 'createIndex']]));
|
||||
$this->assertFalse($validator->isValid([['key' => 'x']]));
|
||||
$this->assertFalse($validator->isValid([['key' => 'x', 'label' => 'y']]));
|
||||
$this->assertFalse($validator->isValid([['key' => 'x', 'label' => 'y', 'service' => 'tablesDB']]));
|
||||
$this->assertFalse($validator->isValid([['key' => 'x', 'label' => 'y', 'method' => 'createIndex']]));
|
||||
}
|
||||
|
||||
public function testRejectsEntryWithEmptyStrings(): void
|
||||
@@ -66,7 +66,7 @@ class CTAsTest extends TestCase
|
||||
$validator = new CTAs();
|
||||
|
||||
$this->assertFalse($validator->isValid([[
|
||||
'id' => '',
|
||||
'key' => '',
|
||||
'label' => 'Create missing index',
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -78,7 +78,7 @@ class CTAsTest extends TestCase
|
||||
$validator = new CTAs();
|
||||
|
||||
$this->assertFalse($validator->isValid([[
|
||||
'id' => 123,
|
||||
'key' => 123,
|
||||
'label' => 'Create missing index',
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -90,7 +90,7 @@ class CTAsTest extends TestCase
|
||||
$validator = new CTAs();
|
||||
|
||||
$this->assertFalse($validator->isValid([[
|
||||
'id' => 'createIndex',
|
||||
'key' => 'createIndex',
|
||||
'label' => 'Create missing index',
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -113,7 +113,7 @@ class CTAsTest extends TestCase
|
||||
$entries = [];
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$entries[] = [
|
||||
'id' => 'cta-' . $i,
|
||||
'key' => 'cta-' . $i,
|
||||
'label' => 'Label ' . $i,
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -131,7 +131,7 @@ class CTAsTest extends TestCase
|
||||
$entries = [];
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$entries[] = [
|
||||
'id' => 'cta-' . $i,
|
||||
'key' => 'cta-' . $i,
|
||||
'label' => 'Label ' . $i,
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -146,7 +146,7 @@ class CTAsTest extends TestCase
|
||||
$validator = new CTAs();
|
||||
|
||||
$entry = [
|
||||
'id' => 'createIndex',
|
||||
'key' => 'createIndex',
|
||||
'label' => 'Create missing index',
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -161,7 +161,7 @@ class CTAsTest extends TestCase
|
||||
$validator = new CTAs();
|
||||
|
||||
$this->assertFalse($validator->isValid([[
|
||||
'id' => 'createIndex',
|
||||
'key' => 'createIndex',
|
||||
'label' => 'Create missing index',
|
||||
'service' => '',
|
||||
'method' => 'createIndex',
|
||||
@@ -173,7 +173,7 @@ class CTAsTest extends TestCase
|
||||
$validator = new CTAs();
|
||||
|
||||
$this->assertFalse($validator->isValid([[
|
||||
'id' => 'createIndex',
|
||||
'key' => 'createIndex',
|
||||
'label' => 'Create missing index',
|
||||
'service' => 'tablesDB',
|
||||
'method' => '',
|
||||
@@ -185,7 +185,7 @@ class CTAsTest extends TestCase
|
||||
$validator = new CTAs();
|
||||
|
||||
$this->assertFalse($validator->isValid([[
|
||||
'id' => 'createIndex',
|
||||
'key' => 'createIndex',
|
||||
'label' => '',
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -201,7 +201,7 @@ class CTAsTest extends TestCase
|
||||
$entries = [];
|
||||
for ($i = 0; $i < 16; $i++) {
|
||||
$entries[] = [
|
||||
'id' => 'cta-' . $i,
|
||||
'key' => 'cta-' . $i,
|
||||
'label' => 'Label ' . $i,
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
@@ -211,7 +211,7 @@ class CTAsTest extends TestCase
|
||||
$this->assertTrue($validator->isValid($entries));
|
||||
|
||||
$entries[] = [
|
||||
'id' => 'cta-16',
|
||||
'key' => 'cta-16',
|
||||
'label' => 'Label 16',
|
||||
'service' => 'tablesDB',
|
||||
'method' => 'createIndex',
|
||||
|
||||
Reference in New Issue
Block a user