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:
Jake Barnby
2026-05-06 18:07:49 +12:00
parent 38efdf18e2
commit 5404bfec75
10 changed files with 265 additions and 119 deletions
+139 -1
View File
@@ -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
+14
View File
@@ -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),
]));
}
);
+2 -2
View File
@@ -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',
])
+13 -43
View File
@@ -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']);
+17 -17
View File
@@ -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',