mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b43f77d353 | ||
|
|
5dadb9a998 | ||
|
|
164c372b21 | ||
|
|
cb260fa3d8 | ||
|
|
05a2f56945 | ||
|
|
82a5d138e0 | ||
|
|
3d4f37c482 | ||
|
|
b03ad6d523 | ||
|
|
5c2b1be581 | ||
|
|
b3b2fe3ba2 | ||
|
|
9667ec2434 | ||
|
|
3c4c78d086 | ||
|
|
98aab7b34d | ||
|
|
81e828a200 | ||
|
|
218d25ba1b | ||
|
|
abe21d51cb | ||
|
|
339ecc418d | ||
|
|
5ff7e2af24 | ||
|
|
9aa47c267d | ||
|
|
12dca64b60 | ||
|
|
952f48c72a | ||
|
|
4e80f7eb9a | ||
|
|
9db037722c | ||
|
|
4500349fda | ||
|
|
611495ec28 | ||
|
|
347436d370 | ||
|
|
db85004b12 | ||
|
|
88ce622e4a | ||
|
|
c041e57b8f | ||
|
|
4a0f00f7db | ||
|
|
de21cab22a | ||
|
|
f8929e1f2e | ||
|
|
754ddbef3c | ||
|
|
ed93b77faa | ||
|
|
5a6cfdbe4c | ||
|
|
6b90d00cc2 | ||
|
|
3ba5e12f89 | ||
|
|
dc441c3973 | ||
|
|
9745df4d37 | ||
|
|
715adf62ca | ||
|
|
90ab70dfc6 | ||
|
|
b0c5185ebd | ||
|
|
d58873ef14 | ||
|
|
105c885c3f | ||
|
|
ad625029e8 |
@@ -26,7 +26,7 @@ $console = [
|
||||
'hostname' => 'localhost',
|
||||
], // Current host is added on app init
|
||||
],
|
||||
'region' => 'fra',
|
||||
'region' => System::getEnv('_APP_REGION', 'default'),
|
||||
'legalName' => '',
|
||||
'legalCountry' => '',
|
||||
'legalState' => '',
|
||||
|
||||
+69
-41
@@ -248,48 +248,62 @@ $adapter
|
||||
|
||||
$server = new Server($adapter);
|
||||
|
||||
$logError = function (Throwable $error, string $action) use ($register) {
|
||||
$logger = $register->get('realtimeLogger');
|
||||
// Allows overriding
|
||||
if (!function_exists('logError')) {
|
||||
function logError(Throwable $error, string $action, array $tags = [], ?Document $project = null, ?Document $user = null, ?Authorization $authorization = null): void
|
||||
{
|
||||
global $register;
|
||||
|
||||
if ($logger && !$error instanceof Exception) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$logger = $register->get('realtimeLogger');
|
||||
|
||||
$log = new Log();
|
||||
$log->setNamespace("realtime");
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
if ($logger && !$error instanceof Exception) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log = new Log();
|
||||
$log->setNamespace("realtime");
|
||||
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
|
||||
$log->setVersion($version);
|
||||
$log->setType(Log::TYPE_ERROR);
|
||||
$log->setMessage($error->getMessage());
|
||||
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addTag('code', $error->getCode());
|
||||
$log->addTag('verboseType', get_class($error));
|
||||
$log->addTag('projectId', $project?->getId() ?: 'n/a');
|
||||
$log->addTag('userId', $user?->getId() ?: 'n/a');
|
||||
|
||||
$log->setAction($action);
|
||||
foreach ($tags as $key => $value) {
|
||||
$log->addTag($key, $value ?: 'n/a');
|
||||
}
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
$log->addExtra('file', $error->getFile());
|
||||
$log->addExtra('line', $error->getLine());
|
||||
$log->addExtra('trace', $error->getTraceAsString());
|
||||
$log->addExtra('detailedTrace', $error->getTrace());
|
||||
$log->addExtra('roles', $authorization?->getRoles() ?? []);
|
||||
|
||||
try {
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Error log pushed with status code: ' . $responseCode);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Error pushing log: ' . $th->getMessage());
|
||||
$log->setAction($action);
|
||||
|
||||
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
|
||||
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
|
||||
|
||||
try {
|
||||
$responseCode = $logger->addLog($log);
|
||||
Console::info('Error log pushed with status code: ' . $responseCode);
|
||||
} catch (Throwable $th) {
|
||||
Console::error('Error pushing log: ' . $th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
Console::error('[Error] Message: ' . $error->getMessage());
|
||||
Console::error('[Error] File: ' . $error->getFile());
|
||||
Console::error('[Error] Line: ' . $error->getLine());
|
||||
}
|
||||
}
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
Console::error('[Error] Message: ' . $error->getMessage());
|
||||
Console::error('[Error] File: ' . $error->getFile());
|
||||
Console::error('[Error] Line: ' . $error->getLine());
|
||||
};
|
||||
$server->error(logError(...));
|
||||
|
||||
$server->error($logError);
|
||||
|
||||
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {
|
||||
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
|
||||
sleep(5); // wait for the initial database schema to be ready
|
||||
Console::success('Server started successfully');
|
||||
|
||||
@@ -326,7 +340,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
*/
|
||||
// TODO: Remove this if check once it doesn't cause issues for cloud
|
||||
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
|
||||
Timer::tick(5000, function () use ($register, $stats, &$statsDocument, $logError) {
|
||||
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
|
||||
$payload = [];
|
||||
foreach ($stats as $projectId => $value) {
|
||||
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
|
||||
@@ -344,13 +358,13 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
|
||||
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
|
||||
} catch (Throwable $th) {
|
||||
$logError($th, "updateWorkerDocument");
|
||||
logError($th, "updateWorkerDocument");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime, $logError) {
|
||||
$server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime) {
|
||||
Console::success('Worker ' . $workerId . ' started successfully');
|
||||
|
||||
$telemetry = getTelemetry($workerId);
|
||||
@@ -362,7 +376,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
|
||||
Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $logError) {
|
||||
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
|
||||
/**
|
||||
* Sending current connections to project channels on the console project every 5 seconds.
|
||||
*/
|
||||
@@ -548,7 +562,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
}
|
||||
});
|
||||
} catch (Throwable $th) {
|
||||
$logError($th, "pubSubConnection");
|
||||
logError($th, "pubSubConnection");
|
||||
|
||||
Console::error('Pub/sub error: ' . $th->getMessage());
|
||||
$attempts++;
|
||||
@@ -560,7 +574,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
Console::error('Failed to restart pub/sub...');
|
||||
});
|
||||
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $logError) {
|
||||
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
|
||||
$app = new Http('UTC');
|
||||
$request = new Request($request);
|
||||
$response = new Response(new SwooleResponse());
|
||||
@@ -571,6 +585,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
Http::setResource('request', fn () => $request);
|
||||
Http::setResource('response', fn () => $response);
|
||||
|
||||
$project = null;
|
||||
$logUser = null;
|
||||
$authorization = null;
|
||||
|
||||
try {
|
||||
/** @var Document $project */
|
||||
$project = $app->getResource('project');
|
||||
@@ -591,8 +609,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
$projectRegion = $project->getAttribute('region', '');
|
||||
$currentRegion = System::getEnv('_APP_REGION', 'default');
|
||||
if (!empty($projectRegion) && $projectRegion !== $currentRegion) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Project is not accessible in this region. Please make sure you are using the correct endpoint');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
@@ -683,11 +708,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
$stats->incr($project->getId(), 'connections');
|
||||
$stats->incr($project->getId(), 'connectionsTotal');
|
||||
} catch (Throwable $th) {
|
||||
$logError($th, "initServer");
|
||||
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
|
||||
|
||||
// Handle SQL error code is 'HY000'
|
||||
$code = $th->getCode();
|
||||
if (!is_int($code)) {
|
||||
if (!\is_int($code)) {
|
||||
$code = 500;
|
||||
}
|
||||
|
||||
@@ -718,7 +743,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
}
|
||||
});
|
||||
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId, $logError) {
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
|
||||
$project = null;
|
||||
$authorization = null;
|
||||
|
||||
try {
|
||||
$response = new Response(new SwooleResponse());
|
||||
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
|
||||
@@ -844,7 +872,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
|
||||
}
|
||||
} catch (Throwable $th) {
|
||||
$logError($th, "realtimeMessage");
|
||||
logError($th, 'realtimeMessage', project: $project, authorization: $authorization);
|
||||
$code = $th->getCode();
|
||||
if (!is_int($code)) {
|
||||
$code = 500;
|
||||
|
||||
Generated
+14
-14
@@ -5215,16 +5215,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/vcs",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/vcs.git",
|
||||
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14"
|
||||
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14",
|
||||
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14",
|
||||
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/92a1650824ba0c5e6a1bc46e622ac87c50a08920",
|
||||
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5258,9 +5258,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/vcs/issues",
|
||||
"source": "https://github.com/utopia-php/vcs/tree/2.0.0"
|
||||
"source": "https://github.com/utopia-php/vcs/tree/2.0.1"
|
||||
},
|
||||
"time": "2026-02-25T11:36:45+00:00"
|
||||
"time": "2026-02-27T12:18:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/websocket",
|
||||
@@ -5438,16 +5438,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.11.1",
|
||||
"version": "1.11.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb"
|
||||
"reference": "45d22c0107a53bb9a0a4e39db0e738d461631d11"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb",
|
||||
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/45d22c0107a53bb9a0a4e39db0e738d461631d11",
|
||||
"reference": "45d22c0107a53bb9a0a4e39db0e738d461631d11",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5483,9 +5483,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.1"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.3"
|
||||
},
|
||||
"time": "2026-02-25T07:15:19+00:00"
|
||||
"time": "2026-02-27T06:54:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -9043,7 +9043,7 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "dev",
|
||||
"stability-flags": {},
|
||||
"stability-flags": [],
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
@@ -9067,5 +9067,5 @@
|
||||
"platform-overrides": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -725,7 +725,9 @@ class Event
|
||||
$events = $pairedEvents;
|
||||
}
|
||||
// mirrored events can have duplicates in case of smaller events
|
||||
return array_unique($events);
|
||||
// array unique can turns list to hasmap in case duplicates present
|
||||
// so forcing array value will turn this to array list always
|
||||
return array_values(array_unique($events));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -510,25 +510,13 @@ class Realtime extends MessagingAdapter
|
||||
$collectionId = $payload->getAttribute('$collectionId', '');
|
||||
$resourceId = $tableId ?: $collectionId;
|
||||
$channels = [];
|
||||
// backward compat(tablesdb will have databases channels + tablesdb prefixed channels)
|
||||
if ($parts[0] === 'databases' || $parts[0] === 'tablesdb') {
|
||||
$prefix = 'databases';
|
||||
|
||||
$channels = self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), $prefix);
|
||||
|
||||
$channels = array_unique([
|
||||
...$channels,
|
||||
...self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), $prefix)
|
||||
]);
|
||||
}
|
||||
|
||||
// prefixed channels -> tablesdb
|
||||
if ($parts[0] !== 'databases') {
|
||||
$channels = array_unique([
|
||||
...$channels,
|
||||
...self::getDatabaseChannels($parts[0], $database->getId(), $resourceId, $payload->getId()),
|
||||
]);
|
||||
}
|
||||
// sending legacy + tablesdb events to both legacy and tablesdb
|
||||
$channels = array_values(array_unique(array_merge(
|
||||
self::getDatabaseChannels('legacy', $database->getId(), $resourceId, $payload->getId(), 'databases'),
|
||||
self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId(), 'databases'),
|
||||
self::getDatabaseChannels('tablesdb', $database->getId(), $resourceId, $payload->getId())
|
||||
)));
|
||||
|
||||
$roles = $collection->getAttribute('documentSecurity', false)
|
||||
? \array_merge($collection->getRead(), $payload->getRead())
|
||||
@@ -620,6 +608,7 @@ class Realtime extends MessagingAdapter
|
||||
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents";
|
||||
$channels[] = "{$basePrefix}.{$databaseId}.collections.{$resourceId}.documents.{$payloadId}";
|
||||
break;
|
||||
|
||||
case 'tablesdb':
|
||||
$channels[] = 'rows';
|
||||
$channels[] = "{$basePrefix}.{$databaseId}.tables.{$resourceId}.rows";
|
||||
|
||||
@@ -4,13 +4,12 @@ namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Callback;
|
||||
|
||||
use Appwrite\Auth\OAuth2\Github as OAuth2Github;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Platform\Permission as AppwritePermission;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
@@ -21,6 +20,7 @@ use Utopia\VCS\Adapter\Git\GitHub;
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
use AppwritePermission;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
@@ -132,13 +132,7 @@ class Get extends Action
|
||||
|
||||
$installation = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'developer')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
|
||||
],
|
||||
'$permissions' => $this->getPermissions($teamId, $projectId),
|
||||
'providerInstallationId' => $providerInstallationId,
|
||||
'projectId' => $projectId,
|
||||
'projectInternalId' => $projectInternalId,
|
||||
|
||||
@@ -152,6 +152,8 @@ class Create extends Action
|
||||
$repository['pushedAt'] = $repository['pushed_at'] ?? '';
|
||||
$repository['organization'] = $installation->getAttribute('organization', '');
|
||||
$repository['provider'] = $installation->getAttribute('provider', '');
|
||||
$repository['providerInstallationId'] = $installation->getAttribute('providerInstallationId', '');
|
||||
$repository['authorized'] = true;
|
||||
|
||||
$response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY);
|
||||
}
|
||||
|
||||
@@ -85,11 +85,23 @@ class Get extends Action
|
||||
|
||||
$repository = $github->getRepository($owner, $repositoryName);
|
||||
|
||||
$authorized = false;
|
||||
try {
|
||||
$installationRepository = $github->getInstallationRepository($repositoryName);
|
||||
if (!empty($installationRepository)) {
|
||||
$authorized = true;
|
||||
}
|
||||
} catch (RepositoryNotFound $e) {
|
||||
$authorized = false;
|
||||
}
|
||||
|
||||
$repository['id'] = \strval($repository['id']) ?? '';
|
||||
$repository['pushedAt'] = $repository['pushed_at'] ?? '';
|
||||
$repository['organization'] = $installation->getAttribute('organization', '');
|
||||
$repository['provider'] = $installation->getAttribute('provider', '');
|
||||
$repository['defaultBranch'] = $repository['default_branch'] ?? '';
|
||||
$repository['authorized'] = $authorized;
|
||||
$repository['providerInstallationId'] = $providerInstallationId;
|
||||
|
||||
$response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY);
|
||||
}
|
||||
|
||||
@@ -148,6 +148,8 @@ class XList extends Action
|
||||
$repo['pushedAt'] = $repo['pushed_at'] ?? null;
|
||||
$repo['provider'] = $installation->getAttribute('provider', '') ?? '';
|
||||
$repo['organization'] = $installation->getAttribute('organization', '') ?? '';
|
||||
$repo['providerInstallationId'] = $installation->getAttribute('providerInstallationId', '');
|
||||
$repo['authorized'] = true;
|
||||
return $repo;
|
||||
}, $repos);
|
||||
|
||||
|
||||
@@ -805,6 +805,16 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
->setExcludePaths([
|
||||
'.github/workflows/**',
|
||||
'.github/ISSUE_TEMPLATE/**',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'install.sh',
|
||||
'install.ps1',
|
||||
'README.md',
|
||||
'CHANGELOG.md',
|
||||
'LICENSE',
|
||||
'LICENSE.md',
|
||||
'scoop/*.json',
|
||||
'lib/constants.ts',
|
||||
])
|
||||
->setMaxDiffLines(500)
|
||||
->setUserId('sdk-analyst');
|
||||
|
||||
@@ -313,6 +313,8 @@ class Migrations extends Action
|
||||
'files.write',
|
||||
'functions.read',
|
||||
'functions.write',
|
||||
'sites.read',
|
||||
'sites.write',
|
||||
'tokens.read',
|
||||
'tokens.write',
|
||||
]
|
||||
|
||||
@@ -453,6 +453,38 @@ abstract class Format
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'migrations':
|
||||
switch ($method) {
|
||||
case 'createAppwriteMigration':
|
||||
case 'getAppwriteReport':
|
||||
switch ($param) {
|
||||
case 'resources':
|
||||
return 'AppwriteMigrationResource';
|
||||
}
|
||||
break;
|
||||
case 'createFirebaseMigration':
|
||||
case 'getFirebaseReport':
|
||||
switch ($param) {
|
||||
case 'resources':
|
||||
return 'FirebaseMigrationResource';
|
||||
}
|
||||
break;
|
||||
case 'createSupabaseMigration':
|
||||
case 'getSupabaseReport':
|
||||
switch ($param) {
|
||||
case 'resources':
|
||||
return 'SupabaseMigrationResource';
|
||||
}
|
||||
break;
|
||||
case 'createNHostMigration':
|
||||
case 'getNHostReport':
|
||||
switch ($param) {
|
||||
case 'resources':
|
||||
return 'NHostMigrationResource';
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'project':
|
||||
switch ($method) {
|
||||
case 'getUsage':
|
||||
|
||||
@@ -53,6 +53,12 @@ class MigrationReport extends Model
|
||||
'default' => 0,
|
||||
'example' => 20,
|
||||
])
|
||||
->addRule(Resource::TYPE_SITE, [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Number of sites to be migrated.',
|
||||
'default' => 0,
|
||||
'example' => 5,
|
||||
])
|
||||
->addRule('size', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Size of files to be migrated in mb.',
|
||||
|
||||
@@ -47,6 +47,18 @@ class ProviderRepository extends Model
|
||||
'default' => '',
|
||||
'example' => 'main',
|
||||
])
|
||||
->addRule('providerInstallationId', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'VCS (Version Control System) installation ID.',
|
||||
'default' => '',
|
||||
'example' => '108104697',
|
||||
])
|
||||
->addRule('authorized', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Is VCS (Version Control System) repository authorized for the installation?',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('pushedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Last commit date in ISO 8601 format.',
|
||||
|
||||
@@ -7,6 +7,7 @@ use Tests\E2E\Client;
|
||||
use Tests\E2E\General\UsageTest;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Services\Functions\FunctionsBase;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
@@ -1017,6 +1018,158 @@ trait MigrationsBase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sites
|
||||
*/
|
||||
public function testAppwriteMigrationSite(): void
|
||||
{
|
||||
$site = $this->client->call(Client::METHOD_POST, '/sites', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'siteId' => ID::unique(),
|
||||
'name' => 'Test Site',
|
||||
'framework' => 'other',
|
||||
'buildRuntime' => 'node-22',
|
||||
'adapter' => 'static',
|
||||
'outputDirectory' => './',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $site['headers']['status-code'], 'Create site failed: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
|
||||
$this->assertNotEmpty($site['body']['$id']);
|
||||
|
||||
$siteId = $site['body']['$id'];
|
||||
|
||||
// Create deployment
|
||||
$deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', [
|
||||
'content-type' => 'multipart/form-data',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'code' => $this->packageSite('static'),
|
||||
'activate' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $deployment['headers']['status-code']);
|
||||
$this->assertNotEmpty($deployment['body']['$id']);
|
||||
|
||||
$deploymentId = $deployment['body']['$id'];
|
||||
|
||||
// Wait for deployment to be ready
|
||||
$this->assertEventually(function () use ($siteId, $deploymentId) {
|
||||
$response = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertEquals('ready', $response['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($response['body'], JSON_PRETTY_PRINT));
|
||||
}, 300000, 500);
|
||||
|
||||
// Create environment variable
|
||||
$variable = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/variables', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'key' => 'TEST_VAR',
|
||||
'value' => 'test_value',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $variable['headers']['status-code']);
|
||||
|
||||
// Perform migration
|
||||
$result = $this->performMigrationSync([
|
||||
'resources' => [
|
||||
Resource::TYPE_SITE,
|
||||
Resource::TYPE_SITE_DEPLOYMENT,
|
||||
Resource::TYPE_SITE_VARIABLE,
|
||||
],
|
||||
'endpoint' => $this->webEndpoint,
|
||||
'projectId' => $this->getProject()['$id'],
|
||||
'apiKey' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals('completed', $result['status']);
|
||||
$this->assertEquals([Resource::TYPE_SITE, Resource::TYPE_SITE_DEPLOYMENT, Resource::TYPE_SITE_VARIABLE], $result['resources']);
|
||||
|
||||
foreach ([Resource::TYPE_SITE, Resource::TYPE_SITE_DEPLOYMENT, Resource::TYPE_SITE_VARIABLE] as $resource) {
|
||||
$this->assertArrayHasKey($resource, $result['statusCounters']);
|
||||
$this->assertEquals(0, $result['statusCounters'][$resource]['error']);
|
||||
$this->assertEquals(0, $result['statusCounters'][$resource]['pending']);
|
||||
$this->assertEquals(1, $result['statusCounters'][$resource]['success']);
|
||||
$this->assertEquals(0, $result['statusCounters'][$resource]['processing']);
|
||||
$this->assertEquals(0, $result['statusCounters'][$resource]['warning']);
|
||||
}
|
||||
|
||||
// Verify site in destination
|
||||
$response = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
$this->assertNotEmpty($response['body']);
|
||||
$this->assertEquals($siteId, $response['body']['$id']);
|
||||
$this->assertEquals('Test Site', $response['body']['name']);
|
||||
$this->assertEquals('node-22', $response['body']['buildRuntime']);
|
||||
$this->assertEquals('other', $response['body']['framework']);
|
||||
$this->assertEquals('static', $response['body']['adapter']);
|
||||
|
||||
// Verify deployment in destination
|
||||
$this->assertEventually(function () use ($siteId) {
|
||||
$deployments = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $deployments['headers']['status-code']);
|
||||
$this->assertNotEmpty($deployments['body']);
|
||||
$this->assertEquals(1, $deployments['body']['total']);
|
||||
$this->assertEquals('ready', $deployments['body']['deployments'][0]['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployments['body']['deployments'][0], JSON_PRETTY_PRINT));
|
||||
}, 100000, 500);
|
||||
|
||||
// Verify variable in destination
|
||||
$variables = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/variables', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $variables['headers']['status-code']);
|
||||
$this->assertEquals(1, $variables['body']['total']);
|
||||
$this->assertEquals('TEST_VAR', $variables['body']['variables'][0]['key']);
|
||||
|
||||
// Cleanup
|
||||
$this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getDestinationProject()['$id'],
|
||||
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function packageSite(string $site): CURLFile
|
||||
{
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
|
||||
$tarPath = "$folderPath/code.tar.gz";
|
||||
|
||||
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
|
||||
|
||||
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Import documents from a CSV file.
|
||||
*/
|
||||
|
||||
@@ -131,4 +131,23 @@ trait RealtimeBase
|
||||
$this->expectException(ConnectionException::class); // Check if server disconnected client
|
||||
$client->close();
|
||||
}
|
||||
|
||||
public function testConnectionRegionCheck(): void
|
||||
{
|
||||
/**
|
||||
* Test for SUCCESS
|
||||
* A project whose region matches the server region should connect successfully.
|
||||
*/
|
||||
$client = $this->getWebsocket(['documents']);
|
||||
$response = json_decode($client->receive(), true);
|
||||
|
||||
$this->assertArrayHasKey('type', $response);
|
||||
$this->assertArrayHasKey('data', $response);
|
||||
$this->assertEquals('connected', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('channels', $response['data']);
|
||||
$this->assertContains('documents', $response['data']['channels']);
|
||||
|
||||
$client->close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,8 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertNotEmpty($response['data']['user']);
|
||||
$this->assertCount(16, $response['data']['channels']);
|
||||
$this->assertIsList($response['data']['channels']);
|
||||
$this->assertTrue(array_is_list($response['data']['channels']));
|
||||
$this->assertContains('account', $response['data']['channels']);
|
||||
$this->assertContains('account.' . $userId, $response['data']['channels']);
|
||||
$this->assertContains('files', $response['data']['channels']);
|
||||
@@ -818,7 +820,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains('documents', $response['data']['channels']);
|
||||
$this->assertContains('databases.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
|
||||
$this->assertContains('databases.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
|
||||
@@ -863,7 +865,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains('documents', $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
|
||||
@@ -919,7 +921,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains('documents', $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
|
||||
@@ -975,7 +977,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.create", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
|
||||
@@ -1007,7 +1009,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.create", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.create", $response['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response['data']['events']);
|
||||
@@ -1056,7 +1058,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
|
||||
@@ -1084,7 +1086,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
|
||||
@@ -1112,7 +1114,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.update", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.update", $response['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response['data']['events']);
|
||||
@@ -1149,7 +1151,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
|
||||
@@ -1178,7 +1180,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
|
||||
@@ -1207,7 +1209,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.delete", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.delete", $response['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response['data']['events']);
|
||||
@@ -1254,7 +1256,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']);
|
||||
@@ -1433,7 +1435,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response1['type']);
|
||||
$this->assertNotEmpty($response1['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response1['data']);
|
||||
$this->assertCount(6, $response1['data']['channels']);
|
||||
$this->assertCount(8, $response1['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.create", $response1['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.create", $response1['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response1['data']['events']);
|
||||
@@ -1464,7 +1466,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response2['type']);
|
||||
$this->assertNotEmpty($response2['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response2['data']);
|
||||
$this->assertCount(6, $response2['data']['channels']);
|
||||
$this->assertCount(8, $response2['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.create", $response2['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.create", $response2['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.create", $response2['data']['events']);
|
||||
@@ -1514,7 +1516,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response1['type']);
|
||||
$this->assertNotEmpty($response1['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response1['data']);
|
||||
$this->assertCount(6, $response1['data']['channels']);
|
||||
$this->assertCount(8, $response1['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.update", $response1['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.update", $response1['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']);
|
||||
@@ -1568,7 +1570,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response2['type']);
|
||||
$this->assertNotEmpty($response2['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response2['data']);
|
||||
$this->assertCount(6, $response2['data']['channels']);
|
||||
$this->assertCount(8, $response2['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.update", $response2['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.update", $response2['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']);
|
||||
@@ -1621,7 +1623,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response1['type']);
|
||||
$this->assertNotEmpty($response1['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response1['data']);
|
||||
$this->assertCount(6, $response1['data']['channels']);
|
||||
$this->assertCount(8, $response1['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.update", $response1['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.update", $response1['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response1['data']['events']);
|
||||
@@ -1648,7 +1650,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response2['type']);
|
||||
$this->assertNotEmpty($response2['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response2['data']);
|
||||
$this->assertCount(6, $response2['data']['channels']);
|
||||
$this->assertCount(8, $response2['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.update", $response2['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.update", $response2['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.update", $response2['data']['events']);
|
||||
@@ -1687,7 +1689,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response1['type']);
|
||||
$this->assertNotEmpty($response1['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response1['data']);
|
||||
$this->assertCount(6, $response1['data']['channels']);
|
||||
$this->assertCount(8, $response1['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response1['data']['payload']['$id']}.delete", $response1['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.delete", $response1['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response1['data']['events']);
|
||||
@@ -1718,7 +1720,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response2['type']);
|
||||
$this->assertNotEmpty($response2['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response2['data']);
|
||||
$this->assertCount(6, $response2['data']['channels']);
|
||||
$this->assertCount(8, $response2['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response2['data']['payload']['$id']}.delete", $response2['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.delete", $response2['data']['events']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.*.documents.*.delete", $response2['data']['events']);
|
||||
@@ -1771,7 +1773,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']);
|
||||
@@ -1809,7 +1811,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$response['data']['payload']['$id']}.upsert", $response['data']['events']);
|
||||
$this->assertContains("databases.*.collections.*.documents.*.upsert", $response['data']['events']);
|
||||
@@ -1951,7 +1953,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains('documents', $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
|
||||
@@ -1990,7 +1992,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains('documents', $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
|
||||
@@ -2040,7 +2042,7 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertCount(6, $response['data']['channels']);
|
||||
$this->assertCount(8, $response['data']['channels']);
|
||||
$this->assertContains('documents', $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
|
||||
@@ -2565,17 +2567,66 @@ class RealtimeCustomClientTest extends Scope
|
||||
$session = $user['session'] ?? '';
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
/**
|
||||
* Create a shared TablesDB database using the /tablesdb API.
|
||||
* This database will then be accessed via both /databases and /tablesdb routes.
|
||||
*/
|
||||
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
]), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'TablesDB Realtime DB',
|
||||
'name' => 'TablesDB Cross API Realtime DB',
|
||||
]);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Legacy collection in the shared database (/databases API).
|
||||
*/
|
||||
$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' => 'Legacy Actors',
|
||||
'permissions' => [
|
||||
Permission::create(Role::user($user['$id'])),
|
||||
],
|
||||
'documentSecurity' => true,
|
||||
]);
|
||||
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
$attribute = $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' => 'name',
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $attribute['headers']['status-code']);
|
||||
|
||||
$this->assertEventually(function () use ($databaseId, $collectionId) {
|
||||
$attribute = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]));
|
||||
|
||||
$this->assertEquals('available', $attribute['body']['status']);
|
||||
}, 30000, 250);
|
||||
|
||||
/**
|
||||
* TablesDB table in the same database (/tablesdb API).
|
||||
*/
|
||||
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
@@ -2616,20 +2667,92 @@ class RealtimeCustomClientTest extends Scope
|
||||
$this->assertEquals('available', $column['body']['status']);
|
||||
}, 120000, 500);
|
||||
|
||||
$client = $this->getWebsocket(['documents', 'collections'], [
|
||||
/**
|
||||
* Two different clients subscribing via legacy (documents/collections)
|
||||
* and new (rows/tables) channels.
|
||||
*/
|
||||
$clientLegacy = $this->getWebsocket(['documents', 'collections'], [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
]);
|
||||
|
||||
$response = json_decode($client->receive(), true);
|
||||
$response = json_decode($clientLegacy->receive(), true);
|
||||
|
||||
$this->assertArrayHasKey('type', $response);
|
||||
$this->assertArrayHasKey('data', $response);
|
||||
$this->assertEquals('connected', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertCount(2, $response['data']['channels']);
|
||||
$this->assertContains('documents', $response['data']['channels']);
|
||||
|
||||
$clientTables = $this->getWebsocket(['rows', 'tables'], [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
]);
|
||||
|
||||
$response = json_decode($clientTables->receive(), true);
|
||||
|
||||
$this->assertArrayHasKey('type', $response);
|
||||
$this->assertArrayHasKey('data', $response);
|
||||
$this->assertEquals('connected', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertContains('rows', $response['data']['channels']);
|
||||
|
||||
/**
|
||||
* 1) Operation via legacy /databases API (document create).
|
||||
* Both clients should receive an event that includes both document-
|
||||
* style and row-style channels on the shared database.
|
||||
*/
|
||||
$documentId = ID::unique();
|
||||
|
||||
$document = $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' => [
|
||||
'name' => 'Legacy Chris Evans',
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $document['headers']['status-code']);
|
||||
|
||||
$legacyEventForLegacyClient = json_decode($clientLegacy->receive(), true);
|
||||
$legacyEventForTablesClient = json_decode($clientTables->receive(), true);
|
||||
|
||||
foreach ([$legacyEventForLegacyClient, $legacyEventForTablesClient] as $event) {
|
||||
$this->assertArrayHasKey('type', $event);
|
||||
$this->assertArrayHasKey('data', $event);
|
||||
$this->assertEquals('event', $event['type']);
|
||||
$this->assertNotEmpty($event['data']);
|
||||
$this->assertArrayHasKey('timestamp', $event['data']);
|
||||
|
||||
$channels = $event['data']['channels'];
|
||||
|
||||
// Legacy-style channels
|
||||
$this->assertContains('documents', $channels);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$collectionId}.documents", $channels);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$collectionId}.documents.{$documentId}", $channels);
|
||||
|
||||
// New rows-style channels mirrored for legacy API
|
||||
$this->assertContains('rows', $channels);
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$collectionId}.rows", $channels);
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$collectionId}.rows.{$documentId}", $channels);
|
||||
|
||||
// TablesDB-prefixed channels should also be present for a tablesdb database
|
||||
$this->assertContains("tablesdb.{$databaseId}.tables.{$collectionId}.rows", $channels);
|
||||
$this->assertContains("tablesdb.{$databaseId}.tables.{$collectionId}.rows.{$documentId}", $channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* 2) Operation via /tablesdb API (row create).
|
||||
* Both clients should again receive an event that now also includes
|
||||
* the tablesdb-prefixed channels alongside the databases-prefixed ones.
|
||||
*/
|
||||
$rowId = ID::unique();
|
||||
|
||||
$row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
|
||||
@@ -2650,31 +2773,296 @@ class RealtimeCustomClientTest extends Scope
|
||||
|
||||
$this->assertEquals(201, $row['headers']['status-code']);
|
||||
|
||||
$tablesEventForLegacyClient = json_decode($clientLegacy->receive(), true);
|
||||
$tablesEventForTablesClient = json_decode($clientTables->receive(), true);
|
||||
|
||||
foreach ([$tablesEventForLegacyClient, $tablesEventForTablesClient] as $event) {
|
||||
$this->assertArrayHasKey('type', $event);
|
||||
$this->assertArrayHasKey('data', $event);
|
||||
$this->assertEquals('event', $event['type']);
|
||||
$this->assertNotEmpty($event['data']);
|
||||
$this->assertArrayHasKey('timestamp', $event['data']);
|
||||
|
||||
$channels = $event['data']['channels'];
|
||||
|
||||
// Core tablesdb row channels
|
||||
$this->assertContains('rows', $channels);
|
||||
$this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows", $channels);
|
||||
$this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $channels);
|
||||
|
||||
// Collections/legacy-style compatibility channels
|
||||
$this->assertContains('documents', $channels);
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows", $channels);
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $channels);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$tableId}.documents", $channels);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$tableId}.documents.{$rowId}", $channels);
|
||||
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}.create", $event['data']['events']);
|
||||
$this->assertNotEmpty($event['data']['payload']);
|
||||
$this->assertEquals('Chris Evans', $event['data']['payload']['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 3) Legacy database accessed via /tablesdb routes.
|
||||
* A database created via /databases but operated on via /tablesdb
|
||||
* should also expose both legacy and tablesdb-prefixed channels.
|
||||
*/
|
||||
$legacyDatabase = $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' => 'Legacy DB via TablesDB Route',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $legacyDatabase['headers']['status-code']);
|
||||
|
||||
$legacyDatabaseId = $legacyDatabase['body']['$id'];
|
||||
|
||||
$legacyTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $legacyDatabaseId . '/tables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
'tableId' => ID::unique(),
|
||||
'name' => 'Legacy Actors',
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$legacyTableId = $legacyTable['body']['$id'];
|
||||
|
||||
$legacyColumn = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $legacyDatabaseId . '/tables/' . $legacyTableId . '/columns/string', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
'key' => 'name',
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $legacyColumn['headers']['status-code']);
|
||||
|
||||
$this->assertEventually(function () use ($legacyDatabaseId, $legacyTableId) {
|
||||
$column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $legacyDatabaseId . '/tables/' . $legacyTableId . '/columns/name', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $column['headers']['status-code']);
|
||||
$this->assertEquals('available', $column['body']['status']);
|
||||
}, 120000, 500);
|
||||
|
||||
$legacyRowId = ID::unique();
|
||||
|
||||
$legacyRow = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $legacyDatabaseId . '/tables/' . $legacyTableId . '/rows', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
'rowId' => $legacyRowId,
|
||||
'data' => [
|
||||
'name' => 'Legacy Tables Route',
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $legacyRow['headers']['status-code']);
|
||||
|
||||
$legacyTablesEventForLegacyClient = json_decode($clientLegacy->receive(), true);
|
||||
$legacyTablesEventForTablesClient = json_decode($clientTables->receive(), true);
|
||||
|
||||
foreach ([$legacyTablesEventForLegacyClient, $legacyTablesEventForTablesClient] as $event) {
|
||||
$this->assertArrayHasKey('type', $event);
|
||||
$this->assertArrayHasKey('data', $event);
|
||||
$this->assertEquals('event', $event['type']);
|
||||
$this->assertNotEmpty($event['data']);
|
||||
$this->assertArrayHasKey('timestamp', $event['data']);
|
||||
|
||||
$channels = $event['data']['channels'];
|
||||
$events = $event['data']['events'];
|
||||
$this->assertIsList($channels);
|
||||
$this->assertIsList($events);
|
||||
|
||||
// Core tablesdb row channels for legacy db accessed via tablesdb
|
||||
$this->assertContains('rows', $channels);
|
||||
$this->assertContains("tablesdb.{$legacyDatabaseId}.tables.{$legacyTableId}.rows", $channels);
|
||||
$this->assertContains("tablesdb.{$legacyDatabaseId}.tables.{$legacyTableId}.rows.{$legacyRowId}", $channels);
|
||||
|
||||
// Legacy compatibility channels must also exist
|
||||
$this->assertContains('documents', $channels);
|
||||
$this->assertContains("databases.{$legacyDatabaseId}.tables.{$legacyTableId}.rows", $channels);
|
||||
$this->assertContains("databases.{$legacyDatabaseId}.tables.{$legacyTableId}.rows.{$legacyRowId}", $channels);
|
||||
$this->assertContains("databases.{$legacyDatabaseId}.collections.{$legacyTableId}.documents", $channels);
|
||||
$this->assertContains("databases.{$legacyDatabaseId}.collections.{$legacyTableId}.documents.{$legacyRowId}", $channels);
|
||||
}
|
||||
|
||||
$clientLegacy->close();
|
||||
$clientTables->close();
|
||||
}
|
||||
|
||||
public function testChannelTablesDBRowUpdate()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$session = $user['session'] ?? '';
|
||||
$projectId = $this->getProject()['$id'];
|
||||
|
||||
/**
|
||||
* Create a tablesdb database + table + column + row.
|
||||
*/
|
||||
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'Row Update DB',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $database['headers']['status-code']);
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
'tableId' => ID::unique(),
|
||||
'name' => 'Assembly',
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::create(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $table['headers']['status-code']);
|
||||
$tableId = $table['body']['$id'];
|
||||
|
||||
$column = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
'key' => 'name',
|
||||
'size' => 256,
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(202, $column['headers']['status-code']);
|
||||
|
||||
$this->assertEventually(function () use ($databaseId, $tableId) {
|
||||
$column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $column['headers']['status-code']);
|
||||
$this->assertEquals('available', $column['body']['status']);
|
||||
}, 120000, 500);
|
||||
|
||||
// Seed a row so we can listen to its update
|
||||
$rowId = ID::unique();
|
||||
|
||||
$row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
'rowId' => $rowId,
|
||||
'data' => [
|
||||
'name' => 'Initial Name',
|
||||
],
|
||||
'permissions' => [
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
Permission::delete(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $row['headers']['status-code']);
|
||||
|
||||
/**
|
||||
* Subscribe to a specific row channel using both legacy and tablesdb-style prefixes.
|
||||
* This mimics a client subscribing to a concrete "resource" channel and
|
||||
* expecting a single event list for updates.
|
||||
*/
|
||||
$client = $this->getWebsocket([
|
||||
"databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}",
|
||||
"tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}",
|
||||
], [
|
||||
'origin' => 'http://localhost',
|
||||
'cookie' => 'a_session_' . $projectId . '=' . $session,
|
||||
]);
|
||||
|
||||
$response = json_decode($client->receive(), true);
|
||||
|
||||
$this->assertArrayHasKey('type', $response);
|
||||
$this->assertArrayHasKey('data', $response);
|
||||
$this->assertEquals('event', $response['type']);
|
||||
$this->assertEquals('connected', $response['type']);
|
||||
$this->assertNotEmpty($response['data']);
|
||||
$this->assertArrayHasKey('timestamp', $response['data']);
|
||||
$this->assertIsList($response['data']['channels']);
|
||||
|
||||
// Core channels for tablesdb row events
|
||||
$this->assertContains('rows', $response['data']['channels']);
|
||||
/**
|
||||
* Trigger a row update via the dedicated /tablesdb row update endpoint.
|
||||
* Event label: databases.[databaseId].tables.[tableId].rows.[rowId].update
|
||||
* Our Event + Realtime logic should enrich this to include:
|
||||
* - databases.{dbId}.tables.{tableId}.rows.{rowId}.update
|
||||
* - tablesdb.{dbId}.tables.{tableId}.rows.{rowId}.update
|
||||
*/
|
||||
$update = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $projectId,
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], $this->getHeaders()), [
|
||||
'data' => [
|
||||
'name' => 'Updated Name',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows", $response['data']['channels']);
|
||||
$this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $response['data']['channels']);
|
||||
$this->assertEquals(200, $update['headers']['status-code']);
|
||||
|
||||
// Collections-style compatibility channels
|
||||
$this->assertContains('documents', $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows", $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$tableId}.documents", $response['data']['channels']);
|
||||
$this->assertContains("databases.{$databaseId}.collections.{$tableId}.documents.{$rowId}", $response['data']['channels']);
|
||||
$event = json_decode($client->receive(), true);
|
||||
|
||||
// Primary event should still be present
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}.create", $response['data']['events']);
|
||||
$this->assertNotEmpty($response['data']['payload']);
|
||||
$this->assertEquals('Chris Evans', $response['data']['payload']['name']);
|
||||
$this->assertArrayHasKey('type', $event);
|
||||
$this->assertArrayHasKey('data', $event);
|
||||
$this->assertEquals('event', $event['type']);
|
||||
$this->assertNotEmpty($event['data']);
|
||||
$this->assertArrayHasKey('timestamp', $event['data']);
|
||||
|
||||
$channels = $event['data']['channels'];
|
||||
$events = $event['data']['events'];
|
||||
|
||||
// Ensure channels and events are list-type arrays
|
||||
$this->assertIsList($channels);
|
||||
$this->assertIsList($events);
|
||||
|
||||
// Legacy + tablesdb row channels must be present
|
||||
$this->assertContains('rows', $channels);
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows", $channels);
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $channels);
|
||||
$this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows", $channels);
|
||||
$this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}", $channels);
|
||||
|
||||
// Both databases.* and tablesdb.* update events should be emitted
|
||||
$this->assertContains("databases.{$databaseId}.tables.{$tableId}.rows.{$rowId}.update", $events);
|
||||
$this->assertContains("tablesdb.{$databaseId}.tables.{$tableId}.rows.{$rowId}.update", $events);
|
||||
|
||||
$this->assertNotEmpty($event['data']['payload']);
|
||||
$this->assertEquals('Updated Name', $event['data']['payload']['name']);
|
||||
|
||||
$client->close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user