mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.8.x' into feat-collection-size
This commit is contained in:
@@ -2,14 +2,12 @@
|
||||
|
||||
use Appwrite\Auth\OAuth2\Github as OAuth2Github;
|
||||
use Appwrite\Event\Build;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\MethodType;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Installations;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Vcs\Comment;
|
||||
@@ -22,15 +20,12 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate;
|
||||
use Utopia\Database\Exception\Order as OrderException;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Cursor;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Detector\Detection\Framework\Analog;
|
||||
@@ -1612,158 +1607,6 @@ Http::post('/v1/vcs/github/events')
|
||||
}
|
||||
);
|
||||
|
||||
Http::get('/v1/vcs/installations')
|
||||
->desc('List installations')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'vcs.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vcs',
|
||||
group: 'installations',
|
||||
name: 'listInstallations',
|
||||
description: '/docs/references/vcs/list-installations.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_INSTALLATION_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('queries', [], new Installations(), '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(', ', Installations::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('project')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$installationId = $cursor->getValue();
|
||||
$cursorDocument = $dbForPlatform->getDocument('installations', $installationId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Installation '{$installationId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
try {
|
||||
$results = $dbForPlatform->find('installations', $queries);
|
||||
$total = $includeTotal ? $dbForPlatform->count('installations', $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([
|
||||
'installations' => $results,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_INSTALLATION_LIST);
|
||||
});
|
||||
|
||||
Http::get('/v1/vcs/installations/:installationId')
|
||||
->desc('Get installation')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'vcs.read')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vcs',
|
||||
group: 'installations',
|
||||
name: 'getInstallation',
|
||||
description: '/docs/references/vcs/get-installation.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_INSTALLATION,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('installationId', '', new Text(256), 'Installation Id')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $installationId, Response $response, Document $project, Database $dbForPlatform) {
|
||||
$installation = $dbForPlatform->getDocument('installations', $installationId);
|
||||
|
||||
if ($installation === false || $installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($installation->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($installation, Response::MODEL_INSTALLATION);
|
||||
});
|
||||
|
||||
Http::delete('/v1/vcs/installations/:installationId')
|
||||
->desc('Delete installation')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'vcs.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vcs',
|
||||
group: 'installations',
|
||||
name: 'deleteInstallation',
|
||||
description: '/docs/references/vcs/delete-installation.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('installationId', '', new Text(256), 'Installation Id')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForDeletes')
|
||||
->action(function (string $installationId, Response $response, Document $project, Database $dbForPlatform, Delete $queueForDeletes) {
|
||||
$installation = $dbForPlatform->getDocument('installations', $installationId);
|
||||
|
||||
if ($installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$dbForPlatform->deleteDocument('installations', $installation->getId())) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove installation from DB');
|
||||
}
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($installation);
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
Http::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId')
|
||||
->desc('Update external deployment (authorize)')
|
||||
->groups(['api', 'vcs'])
|
||||
|
||||
@@ -369,6 +369,7 @@ const RESOURCE_TYPE_TOPICS = 'topics';
|
||||
const RESOURCE_TYPE_SUBSCRIBERS = 'subscribers';
|
||||
const RESOURCE_TYPE_MESSAGES = 'messages';
|
||||
const RESOURCE_TYPE_EXECUTIONS = 'executions';
|
||||
const RESOURCE_TYPE_VCS = 'vcs';
|
||||
|
||||
// Resource types for Tokens
|
||||
const TOKENS_RESOURCE_TYPE_FILES = 'files';
|
||||
|
||||
@@ -377,7 +377,7 @@ class Realtime extends MessagingAdapter
|
||||
$subscriptionsByIndex = [];
|
||||
|
||||
foreach ($channelNames as $channel) {
|
||||
$channelSubscriptions = $getQueryParam($channel);
|
||||
$channelSubscriptions = $getQueryParam(str_replace(".", "_", $channel));
|
||||
|
||||
// Backward compatibility: if no channel-specific query params, treat as subscription 0 with select("*")
|
||||
if ($channelSubscriptions === null) {
|
||||
|
||||
@@ -14,6 +14,7 @@ use Appwrite\Platform\Modules\Proxy;
|
||||
use Appwrite\Platform\Modules\Sites;
|
||||
use Appwrite\Platform\Modules\Storage;
|
||||
use Appwrite\Platform\Modules\Tokens;
|
||||
use Appwrite\Platform\Modules\VCS;
|
||||
use Utopia\Platform\Platform;
|
||||
|
||||
class Appwrite extends Platform
|
||||
@@ -32,5 +33,6 @@ class Appwrite extends Platform
|
||||
$this->addModule(new Proxy\Module());
|
||||
$this->addModule(new Tokens\Module());
|
||||
$this->addModule(new Storage\Module());
|
||||
$this->addModule(new VCS\Module());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\VCS\Http\Installations;
|
||||
|
||||
use Appwrite\Event\Delete as DeleteEvent;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
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\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Delete extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'deleteInstallation';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
|
||||
->setHttpPath('/v1/vcs/installations/:installationId')
|
||||
->desc('Delete installation')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'vcs.write')
|
||||
->label('resourceType', RESOURCE_TYPE_VCS)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vcs',
|
||||
group: 'installations',
|
||||
name: 'deleteInstallation',
|
||||
description: '/docs/references/vcs/delete-installation.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_NOCONTENT,
|
||||
model: Response::MODEL_NONE,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::NONE
|
||||
))
|
||||
->param('installationId', '', new Text(256), 'Installation Id')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->inject('queueForDeletes')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $installationId,
|
||||
Response $response,
|
||||
Database $dbForPlatform,
|
||||
DeleteEvent $queueForDeletes
|
||||
) {
|
||||
$installation = $dbForPlatform->getDocument('installations', $installationId);
|
||||
|
||||
if ($installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$dbForPlatform->deleteDocument('installations', $installation->getId())) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove installation from DB');
|
||||
}
|
||||
|
||||
$queueForDeletes
|
||||
->setType(DELETE_TYPE_DOCUMENT)
|
||||
->setDocument($installation);
|
||||
|
||||
$response->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\VCS\Http\Installations;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
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\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getInstallation';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/vcs/installations/:installationId')
|
||||
->desc('Get installation')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'vcs.read')
|
||||
->label('resourceType', RESOURCE_TYPE_VCS)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vcs',
|
||||
group: 'installations',
|
||||
name: 'getInstallation',
|
||||
description: '/docs/references/vcs/get-installation.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_INSTALLATION,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('installationId', '', new Text(256), 'Installation Id')
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForPlatform')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $installationId,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Database $dbForPlatform
|
||||
) {
|
||||
$installation = $dbForPlatform->getDocument('installations', $installationId);
|
||||
|
||||
if ($installation === false || $installation->isEmpty()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
if ($installation->getAttribute('projectInternalId') !== $project->getSequence()) {
|
||||
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$response->dynamic($installation, Response::MODEL_INSTALLATION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\VCS\Http\Installations;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Action;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\Installations;
|
||||
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\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\Text;
|
||||
|
||||
class XList extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listInstallations';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/vcs/installations')
|
||||
->desc('List installations')
|
||||
->groups(['api', 'vcs'])
|
||||
->label('scope', 'vcs.read')
|
||||
->label('resourceType', RESOURCE_TYPE_VCS)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'vcs',
|
||||
group: 'installations',
|
||||
name: 'listInstallations',
|
||||
description: '/docs/references/vcs/list-installations.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_INSTALLATION_LIST,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('queries', [], new Installations(), '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(', ', Installations::ALLOWED_ATTRIBUTES), true)
|
||||
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('project')
|
||||
->inject('dbForPlatform')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
array $queries,
|
||||
string $search,
|
||||
bool $includeTotal,
|
||||
Response $response,
|
||||
Document $project,
|
||||
Database $dbForPlatform
|
||||
) {
|
||||
try {
|
||||
$queries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
|
||||
*/
|
||||
$cursor = \array_filter($queries, function ($query) {
|
||||
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
|
||||
});
|
||||
$cursor = reset($cursor);
|
||||
if ($cursor) {
|
||||
/** @var Query $cursor */
|
||||
|
||||
$validator = new Cursor();
|
||||
if (!$validator->isValid($cursor)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$installationId = $cursor->getValue();
|
||||
$cursorDocument = $dbForPlatform->getDocument('installations', $installationId);
|
||||
|
||||
if ($cursorDocument->isEmpty()) {
|
||||
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Installation '{$installationId}' for the 'cursor' value not found.");
|
||||
}
|
||||
|
||||
$cursor->setValue($cursorDocument);
|
||||
}
|
||||
|
||||
$filterQueries = Query::groupByType($queries)['filters'];
|
||||
try {
|
||||
$results = $dbForPlatform->find('installations', $queries);
|
||||
$total = $includeTotal ? $dbForPlatform->count('installations', $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([
|
||||
'installations' => $results,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_INSTALLATION_LIST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\VCS;
|
||||
|
||||
use Appwrite\Platform\Modules\VCS\Services\Http;
|
||||
use Utopia\Platform;
|
||||
|
||||
class Module extends Platform\Module
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->addService('http', new Http());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\VCS\Services;
|
||||
|
||||
use Appwrite\Platform\Modules\VCS\Http\Installations\Delete as DeleteInstallation;
|
||||
use Appwrite\Platform\Modules\VCS\Http\Installations\Get as GetInstallation;
|
||||
use Appwrite\Platform\Modules\VCS\Http\Installations\XList as ListInstallations;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
class Http extends Service
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->type = Service::TYPE_HTTP;
|
||||
|
||||
// Installations
|
||||
$this->addAction(GetInstallation::getName(), new GetInstallation());
|
||||
$this->addAction(ListInstallations::getName(), new ListInstallations());
|
||||
$this->addAction(DeleteInstallation::getName(), new DeleteInstallation());
|
||||
}
|
||||
}
|
||||
@@ -290,9 +290,11 @@ class Certificates extends Action
|
||||
$certificate->setAttribute('domain', $domain->get());
|
||||
}
|
||||
|
||||
$date = \date('H:i:s');
|
||||
$logs = "\033[90m[{$date}] \033[97mProcessing SSL certificate issuance. \033[0m\n";
|
||||
|
||||
try {
|
||||
$date = \date('H:i:s');
|
||||
$certificate->setAttribute('logs', "\033[90m[{$date}] \033[97mCertificate generation started. \033[0m\n");
|
||||
$certificate->setAttribute('logs', $logs);
|
||||
|
||||
// Persist ASAP so that logs are reset in retry flow and user can see the latest logs on Console.
|
||||
$certificate = $this->upsertCertificate($rule, $certificate, $dbForPlatform);
|
||||
@@ -314,10 +316,16 @@ class Certificates extends Action
|
||||
$certName = ID::unique();
|
||||
$renewDate = $certificates->issueCertificate($certName, $domain->get(), $domainType);
|
||||
|
||||
$date = \date('H:i:s');
|
||||
// If certificate is generated instantly, we can mark the rule as 'verified'.
|
||||
if ($certificates->isInstantGeneration($domain->get(), $domainType)) {
|
||||
$rule->setAttribute('status', RULE_STATUS_VERIFIED);
|
||||
$certificate->setAttribute('logs', 'Certificate successfully generated.');
|
||||
$logs .= "\033[90m[{$date}] \033[97mSSL certificate successfully issued. \033[0m\n";
|
||||
$certificate->setAttribute('logs', $logs);
|
||||
} else {
|
||||
// Delayed generation: third-party handles certificate issuance asynchronously
|
||||
$logs .= "\033[90m[{$date}] \033[97mSSL certificate is being issued. This usually takes a few minutes — no action needed on your end. We'll periodically check and update the status. \033[0m\n";
|
||||
$certificate->setAttribute('logs', $logs);
|
||||
}
|
||||
|
||||
$certificate->setAttributes([
|
||||
@@ -326,16 +334,14 @@ class Certificates extends Action
|
||||
'renewDate' => $renewDate,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
$logs = $e->getMessage();
|
||||
$currentLogs = $certificate->getAttribute('logs', '');
|
||||
$date = \date('H:i:s');
|
||||
$errorMessage = "\033[90m[{$date}] \033[31mCertificate generation failed: \033[0m\n";
|
||||
$logs .= "\033[90m[{$date}] \033[31mSSL certificate issuance failed: \033[0m\n";
|
||||
$logs .= \mb_strcut($e->getMessage(), 0, 500000); // Limit to 500kb
|
||||
|
||||
$attempts = $certificate->getAttribute('attempts', 0) + 1; // Increase attempts count
|
||||
|
||||
// Update attributes on certificate document
|
||||
$certificate->setAttributes([
|
||||
'logs' => $currentLogs . $errorMessage . \mb_strcut($logs, 0, 500000), // Limit to 500kb
|
||||
'attempts' => $attempts,
|
||||
'renewDate' => DateTime::now(), // Store current time as renew date to ensure another attempt in next maintenance cycle.
|
||||
]);
|
||||
@@ -348,14 +354,13 @@ class Certificates extends Action
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
// All actions result in new 'updated' date
|
||||
$certificate->setAttribute('updated', DateTime::now());
|
||||
// Save certificate document to database
|
||||
// Update certificate document with logs
|
||||
$certificate->setAttribute('logs', $logs);
|
||||
$this->upsertCertificate($rule, $certificate, $dbForPlatform);
|
||||
|
||||
// Ensure certificate is associated with the rule
|
||||
$rule->setAttribute('certificateId', $certificate->getId());
|
||||
// Update rule and emit events
|
||||
$rule->setAttribute('certificateId', $certificate->getId());
|
||||
$rule->setAttribute('logs', $logs);
|
||||
$this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1234,6 +1234,183 @@ class RealtimeCustomClientQueryTest extends Scope
|
||||
$client->close();
|
||||
}
|
||||
|
||||
public function testCollectionScopedDocumentsChannelReceivesEvents()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$session = $user['session'] ?? '';
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
// Setup database and collection
|
||||
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Scoped Channel DB',
|
||||
]);
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Scoped Channel Collection',
|
||||
'permissions' => [
|
||||
Permission::create(Role::user($user['$id'])),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'status',
|
||||
'size' => 256,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Subscribe only to the fully-qualified documents channel for this collection
|
||||
$scopedChannel = 'databases.' . $databaseId . '.collections.' . $collectionId . '.documents';
|
||||
$client = $this->getWebsocket([$scopedChannel], [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
]);
|
||||
|
||||
$response = json_decode($client->receive(), true);
|
||||
$this->assertEquals('connected', $response['type']);
|
||||
$this->assertContains($scopedChannel, $response['data']['channels']);
|
||||
|
||||
// Create document in that collection - should receive event on the scoped channel
|
||||
$documentId = ID::unique();
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => $documentId,
|
||||
'data' => [
|
||||
'status' => 'active'
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$event = json_decode($client->receive(), true);
|
||||
$this->assertEquals('event', $event['type']);
|
||||
$this->assertEquals($documentId, $event['data']['payload']['$id']);
|
||||
|
||||
$client->close();
|
||||
}
|
||||
|
||||
public function testCollectionScopedDocumentsChannelWithQuery()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$session = $user['session'] ?? '';
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
// Setup database and collection
|
||||
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Scoped Channel Query DB',
|
||||
]);
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Scoped Channel Query Collection',
|
||||
'permissions' => [
|
||||
Permission::create(Role::user($user['$id'])),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'status',
|
||||
'size' => 256,
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
$targetDocumentId = ID::unique();
|
||||
|
||||
// Subscribe with query for specific document ID on the fully-qualified documents channel
|
||||
$scopedChannel = 'databases.' . $databaseId . '.collections.' . $collectionId . '.documents';
|
||||
$client = $this->getWebsocket([$scopedChannel], [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
], null, [
|
||||
Query::equal('$id', [$targetDocumentId])->toString(),
|
||||
]);
|
||||
|
||||
$response = json_decode($client->receive(), true);
|
||||
$this->assertEquals('connected', $response['type']);
|
||||
$this->assertContains($scopedChannel, $response['data']['channels']);
|
||||
|
||||
// Create document with matching ID - should receive event
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => $targetDocumentId,
|
||||
'data' => [
|
||||
'status' => 'active'
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$event = json_decode($client->receive(), true);
|
||||
$this->assertEquals('event', $event['type']);
|
||||
$this->assertEquals($targetDocumentId, $event['data']['payload']['$id']);
|
||||
|
||||
// Create document with different ID - should NOT receive event
|
||||
$otherDocumentId = ID::unique();
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => $otherDocumentId,
|
||||
'data' => [
|
||||
'status' => 'inactive'
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
try {
|
||||
$client->receive();
|
||||
$this->fail('Expected TimeoutException - event should be filtered for scoped channel query');
|
||||
} catch (TimeoutException $e) {
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
$client->close();
|
||||
}
|
||||
|
||||
public function testFilesChannelWithQuery()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
Reference in New Issue
Block a user