mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
feat(insights): add CRUD endpoints
Adds the create, get, list, update, and delete endpoints under the `insights` SDK namespace. Mutating endpoints are admin/key-only because insights are produced by analyzers; reads are open to sessions and JWTs so console UIs can surface them. Updates use sparse documents so unset fields keep their existing value. 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
7f7be46547
commit
5a8be81484
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Insights\Http\Insights;
|
||||
|
||||
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\Database\Validator\CustomId;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Validator\Datetime as DatetimeValidator;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\JSON;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Create extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'createInsight';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/insights')
|
||||
->desc('Create insight')
|
||||
->groups(['api', 'insights'])
|
||||
->label('scope', 'insights.write')
|
||||
->label('event', 'insights.[insightId].create')
|
||||
->label('resourceType', RESOURCE_TYPE_INSIGHTS)
|
||||
->label('audits.event', 'insight.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: 'create',
|
||||
description: <<<EOT
|
||||
Create a new insight. Server-side only: insights are produced by analyzers and surfaced to project members.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_CREATED,
|
||||
model: Response::MODEL_INSIGHT,
|
||||
),
|
||||
]
|
||||
))
|
||||
->param('insightId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Insight ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
|
||||
->param('type', '', new WhiteList(INSIGHT_TYPES, true), 'Insight type. Determines the analyzer that owns this insight and the shape of `payload`.')
|
||||
->param('severity', INSIGHT_SEVERITY_INFO, new WhiteList(INSIGHT_SEVERITIES, true), 'Insight severity. One of `info`, `warning`, `critical`.', true)
|
||||
->param('resourceType', '', new Text(64), 'Plural resource type the insight is about, e.g. `databases`, `sites`, `functions`.')
|
||||
->param('resourceId', '', new Text(36), 'ID of the resource the insight is about.')
|
||||
->param('resourceInternalId', '', new Text(36), 'Internal ID of the resource the insight is about.', true)
|
||||
->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 ArrayList(new JSON(), 16), 'Array of call-to-action descriptors. Each must contain `id`, `label`, `action`, and optional `params`.', true)
|
||||
->param('analyzedAt', null, new Nullable(new DatetimeValidator()), 'Time the insight was analyzed in ISO 8601 format. Defaults to now.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $insightId,
|
||||
string $type,
|
||||
string $severity,
|
||||
string $resourceType,
|
||||
string $resourceId,
|
||||
string $resourceInternalId,
|
||||
string $title,
|
||||
string $summary,
|
||||
?array $payload,
|
||||
array $ctas,
|
||||
?string $analyzedAt,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents
|
||||
) {
|
||||
$insightId = ($insightId === 'unique()') ? ID::unique() : $insightId;
|
||||
|
||||
$normalizedCtas = [];
|
||||
foreach ($ctas as $cta) {
|
||||
if (!isset($cta['id'], $cta['label'], $cta['action'])) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Each CTA must define `id`, `label`, and `action`.');
|
||||
}
|
||||
$normalizedCtas[] = [
|
||||
'id' => (string) $cta['id'],
|
||||
'label' => (string) $cta['label'],
|
||||
'action' => (string) $cta['action'],
|
||||
'params' => $cta['params'] ?? new \stdClass(),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$insight = $dbForProject->createDocument('insights', new Document([
|
||||
'$id' => $insightId,
|
||||
'type' => $type,
|
||||
'severity' => $severity,
|
||||
'resourceType' => $resourceType,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceInternalId' => $resourceInternalId,
|
||||
'title' => $title,
|
||||
'summary' => $summary,
|
||||
'payload' => $payload,
|
||||
'ctas' => $normalizedCtas,
|
||||
'analyzedAt' => $analyzedAt,
|
||||
'dismissedAt' => null,
|
||||
'dismissedBy' => '',
|
||||
]));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::INSIGHT_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('insightId', $insight->getId());
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($insight, Response::MODEL_INSIGHT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Insights\Http\Insights;
|
||||
|
||||
use Appwrite\Event\Event;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Delete extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'deleteInsight';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/insights/:insightId')
|
||||
->desc('Delete insight')
|
||||
->groups(['api', 'insights'])
|
||||
->label('scope', 'insights.write')
|
||||
->label('event', 'insights.[insightId].delete')
|
||||
->label('resourceType', RESOURCE_TYPE_INSIGHTS)
|
||||
->label('audits.event', 'insight.delete')
|
||||
->label('audits.resource', '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: <<<EOT
|
||||
Delete an insight by its unique ID.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
),
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('insightId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Insight ID.', false, ['dbForProject'])
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $insightId,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents
|
||||
) {
|
||||
$insight = $dbForProject->getDocument('insights', $insightId);
|
||||
|
||||
if ($insight->isEmpty()) {
|
||||
throw new Exception(Exception::INSIGHT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$dbForProject->deleteDocument('insights', $insight->getId())) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove insight from DB');
|
||||
}
|
||||
|
||||
$queueForEvents
|
||||
->setParam('insightId', $insight->getId())
|
||||
->setPayload($response->output($insight, Response::MODEL_INSIGHT));
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Insights\Http\Insights;
|
||||
|
||||
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\Validator\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getInsight';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/insights/:insightId')
|
||||
->desc('Get insight')
|
||||
->groups(['api', 'insights'])
|
||||
->label('scope', 'insights.read')
|
||||
->label('resourceType', RESOURCE_TYPE_INSIGHTS)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'insights',
|
||||
group: 'insights',
|
||||
name: 'get',
|
||||
description: <<<EOT
|
||||
Get an insight by its unique ID.
|
||||
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('dbForProject')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $insightId,
|
||||
Response $response,
|
||||
Database $dbForProject
|
||||
) {
|
||||
$insight = $dbForProject->getDocument('insights', $insightId);
|
||||
|
||||
if ($insight->isEmpty()) {
|
||||
throw new Exception(Exception::INSIGHT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($insight, Response::MODEL_INSIGHT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Insights\Http\Insights;
|
||||
|
||||
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\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\ArrayList;
|
||||
use Utopia\Validator\JSON;
|
||||
use Utopia\Validator\Nullable;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
class Update extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'updateInsight';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
|
||||
->setHttpPath('/v1/insights/:insightId')
|
||||
->desc('Update insight')
|
||||
->groups(['api', 'insights'])
|
||||
->label('scope', 'insights.write')
|
||||
->label('event', 'insights.[insightId].update')
|
||||
->label('resourceType', RESOURCE_TYPE_INSIGHTS)
|
||||
->label('audits.event', 'insight.update')
|
||||
->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: 'update',
|
||||
description: <<<EOT
|
||||
Update an insight. Pass only the attributes you want to change.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::KEY],
|
||||
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'])
|
||||
->param('severity', null, new Nullable(new WhiteList(INSIGHT_SEVERITIES, true)), 'Insight severity. One of `info`, `warning`, `critical`.', 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('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $insightId,
|
||||
?string $severity,
|
||||
?string $title,
|
||||
?string $summary,
|
||||
?array $payload,
|
||||
?array $ctas,
|
||||
?string $analyzedAt,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents
|
||||
) {
|
||||
$insight = $dbForProject->getDocument('insights', $insightId);
|
||||
|
||||
if ($insight->isEmpty()) {
|
||||
throw new Exception(Exception::INSIGHT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$changes = [];
|
||||
|
||||
if ($severity !== null) {
|
||||
$changes['severity'] = $severity;
|
||||
}
|
||||
if ($title !== null) {
|
||||
$changes['title'] = $title;
|
||||
}
|
||||
if ($summary !== null) {
|
||||
$changes['summary'] = $summary;
|
||||
}
|
||||
if ($payload !== null) {
|
||||
$changes['payload'] = $payload;
|
||||
}
|
||||
if ($ctas !== null) {
|
||||
$normalized = [];
|
||||
foreach ($ctas as $cta) {
|
||||
if (!isset($cta['id'], $cta['label'], $cta['action'])) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Each CTA must define `id`, `label`, and `action`.');
|
||||
}
|
||||
$normalized[] = [
|
||||
'id' => (string) $cta['id'],
|
||||
'label' => (string) $cta['label'],
|
||||
'action' => (string) $cta['action'],
|
||||
'params' => $cta['params'] ?? new \stdClass(),
|
||||
];
|
||||
}
|
||||
$changes['ctas'] = $normalized;
|
||||
}
|
||||
if ($analyzedAt !== null) {
|
||||
$changes['analyzedAt'] = $analyzedAt;
|
||||
}
|
||||
|
||||
if ($changes !== []) {
|
||||
$insight = $dbForProject->updateDocument('insights', $insight->getId(), new Document($changes));
|
||||
}
|
||||
|
||||
$queueForEvents->setParam('insightId', $insight->getId());
|
||||
|
||||
$response->dynamic($insight, Response::MODEL_INSIGHT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Insights\Http\Insights;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Insights;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
|
||||
class XList extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listInsights';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/insights')
|
||||
->desc('List insights')
|
||||
->groups(['api', 'insights'])
|
||||
->label('scope', 'insights.read')
|
||||
->label('resourceType', RESOURCE_TYPE_INSIGHTS)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'insights',
|
||||
group: 'insights',
|
||||
name: 'list',
|
||||
description: <<<EOT
|
||||
Get a list of all the project's insights. You can use the query params to filter your results.
|
||||
EOT,
|
||||
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_INSIGHT_LIST,
|
||||
),
|
||||
]
|
||||
))
|
||||
->param('queries', [], new Insights(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Insights::ALLOWED_ATTRIBUTES), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
array $queries,
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Database $dbForProject
|
||||
) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$cursor = Query::getCursorQueries($queries, false);
|
||||
$cursor = \reset($cursor);
|
||||
|
||||
if ($cursor !== false) {
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$insightId = $cursor->getValue();
|
||||
$cursorDocument = $dbForProject->getDocument('insights', $insightId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Insight '{$insightId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
|
||||
try {
|
||||
$insights = $dbForProject->find('insights', $queries);
|
||||
$total = $includeTotal ? $dbForProject->count('insights', $filterQueries, APP_LIMIT_COUNT) : 0;
|
||||
} catch (OrderException $e) {
|
||||
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'insights' => $insights,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_INSIGHT_LIST);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user