set('log', fn () => new Log(), []); $container->set('usage', fn () => new Context(), []); $container->set('authorization', function () { $authorization = new Authorization(); $authorization->disable(); return $authorization; }, []); $container->set('dbForPlatform', function (Cache $cache, Group $pools, Authorization $authorization) { $adapter = new DatabasePool($pools->get('console')); $dbForPlatform = new Database($adapter, $cache); $dbForPlatform ->setDatabase(APP_DATABASE) ->setAuthorization($authorization) ->setNamespace('_console') ->setDocumentType('users', User::class); return $dbForPlatform; }, ['cache', 'pools', 'authorization']); $container->set('project', function ($message, Database $dbForPlatform) { $payload = $message->getPayload() ?? []; $project = new Document($payload['project'] ?? []); if ($project->isEmpty() || $project->getId() === 'console') { return $project; } return $dbForPlatform->getDocument('projects', $project->getId()); }, ['message', 'dbForPlatform']); $container->set('dbForProject', function (Cache $cache, Group $pools, Document $project, Database $dbForPlatform, Authorization $authorization) { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForPlatform; } try { $dsn = new DSN($project->getAttribute('database')); } catch (\InvalidArgumentException) { // TODO: Temporary until all projects are using shared tables $dsn = new DSN('mysql://' . $project->getAttribute('database')); } $adapter = new DatabasePool($pools->get($dsn->getHost())); $database = new Database($adapter, $cache); $database->setDocumentType('users', User::class); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); if (\in_array($dsn->getHost(), $sharedTables)) { /** @var array $collections */ $collections = Config::getParam('collections', []); $projectCollections = $collections['projects'] ?? []; $projectsGlobalCollections = array_keys($projectCollections); $projectsGlobalCollections[] = 'audit'; $database ->setSharedTables(true) ->setGlobalCollections($projectsGlobalCollections) ->setTenant($project->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database ->setSharedTables(false) ->setTenant(null) ->setNamespace('_' . $project->getSequence()); } $database ->setDatabase(APP_DATABASE) ->setAuthorization($authorization) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); return $database; }, ['cache', 'pools', 'project', 'dbForPlatform', 'authorization']); $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) { $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database { if ($project->isEmpty() || $project->getId() === 'console') { return $dbForPlatform; } try { $dsn = new DSN($project->getAttribute('database')); } catch (\InvalidArgumentException) { // TODO: Temporary until all projects are using shared tables $dsn = new DSN('mysql://' . $project->getAttribute('database')); } if (isset($databases[$dsn->getHost()])) { $database = $databases[$dsn->getHost()]; $database->setAuthorization($authorization); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); if (\in_array($dsn->getHost(), $sharedTables)) { /** @var array $collections */ $collections = Config::getParam('collections', []); $projectCollections = $collections['projects'] ?? []; $projectsGlobalCollections = array_keys($projectCollections); $projectsGlobalCollections[] = 'audit'; $database ->setSharedTables(true) ->setGlobalCollections($projectsGlobalCollections) ->setTenant($project->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database ->setSharedTables(false) ->setTenant(null) ->setNamespace('_' . $project->getSequence()); } return $database; } $adapter = new DatabasePool($pools->get($dsn->getHost())); $database = new Database($adapter, $cache); $databases[$dsn->getHost()] = $database; $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); if (\in_array($dsn->getHost(), $sharedTables)) { /** @var array $collections */ $collections = Config::getParam('collections', []); $projectCollections = $collections['projects'] ?? []; $projectsGlobalCollections = array_keys($projectCollections); $projectsGlobalCollections[] = 'audit'; $database ->setSharedTables(true) ->setGlobalCollections($projectsGlobalCollections) ->setTenant($project->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database ->setSharedTables(false) ->setTenant(null) ->setNamespace('_' . $project->getSequence()); } $database ->setDatabase(APP_DATABASE) ->setAuthorization($authorization) ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); return $database; }; }, ['pools', 'dbForPlatform', 'cache', 'authorization']); $container->set('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) { return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database { $projectDocument ??= $project; $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', '')); $databaseType = $database->getAttribute('type', ''); // Backwards-compatibility: older or seeded legacy databases may not have a DSN stored // in the "database" attribute. In that case, fall back to the project's database DSN. if ($databaseDSN === '') { $databaseDSN = $projectDocument->getAttribute('database', ''); } try { $databaseDSN = new DSN($databaseDSN); } catch (\InvalidArgumentException) { $databaseDSN = new DSN('mysql://' . $databaseDSN); } try { $dsn = new DSN($projectDocument->getAttribute('database')); } catch (\InvalidArgumentException) { // Temporary fallback until all projects use shared tables $dsn = new DSN('mysql://' . $projectDocument->getAttribute('database')); } $pools = $register->get('pools'); $databaseHost = $databaseDSN->getHost(); $pool = $pools->get($databaseHost); $adapter = new DatabasePool($pool); $database = new Database($adapter, $cache); $database ->setDatabase(APP_DATABASE) ->setAuthorization($authorization); $database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB); $sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''))); /** @var array $collections */ $collections = Config::getParam('collections', []); $projectCollections = $collections['projects'] ?? []; $projectsGlobalCollections = array_keys($projectCollections); $projectsGlobalCollections[] = 'audit'; $database->setGlobalCollections($projectsGlobalCollections); // For separate pools (documentsdb/vectorsdb), check their own shared tables config. // If not configured, use dedicated mode to avoid cross-engine tenant type mismatches. if ($databaseHost !== $dsn->getHost()) { $dbTypeSharedTables = match ($databaseType) { DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))), VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))), default => [], }; if (\in_array($databaseHost, $dbTypeSharedTables)) { $database ->setSharedTables(true) ->setGlobalCollections($projectsGlobalCollections) ->setTenant($projectDocument->getSequence()) ->setNamespace($databaseDSN->getParam('namespace')); } else { $database ->setSharedTables(false) ->setTenant(null) ->setNamespace('_' . $projectDocument->getSequence()); } } elseif (\in_array($dsn->getHost(), $sharedTables, true)) { $database ->setSharedTables(true) ->setGlobalCollections($projectsGlobalCollections) ->setTenant($projectDocument->getSequence()) ->setNamespace($dsn->getParam('namespace')); } else { $database ->setSharedTables(false) ->setTenant(null) ->setNamespace('_' . $projectDocument->getSequence()); } $database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER); return $database; }; }, ['cache', 'register', 'project', 'authorization']); $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) { $database = null; return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { $database->setTenant($project->getSequence()); return $database; } /** @var array $collections */ $collections = Config::getParam('collections', []); $logsCollections = $collections['logs'] ?? []; $logsCollections = array_keys($logsCollections); $adapter = new DatabasePool($pools->get('logs')); $database = new Database($adapter, $cache); $database ->setDatabase(APP_DATABASE) ->setAuthorization($authorization) ->setSharedTables(true) ->setGlobalCollections($logsCollections) ->setNamespace('logsV1') ->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER) ->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER); if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') { $database->setTenant($project->getSequence()); } return $database; }; }, ['pools', 'cache', 'authorization']); $container->set('abuseRetention', function () { return \time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day }, []); $container->set('auditRetention', function (Document $project) { if ($project->getId() === 'console') { return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months } return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days }, ['project']); $container->set('executionRetention', function () { return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days }, []); $container->set('queueForEvents', function (Publisher $publisher) { return new Event($publisher); }, ['publisher']); $container->set('queueForWebhooks', function (Publisher $publisher) { return new Webhook($publisher); }, ['publisher']); $container->set('queueForRealtime', function () { return new Realtime(); }, []); $container->set('deviceForSites', function (Document $project, Telemetry $telemetry) { return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())); }, ['project', 'telemetry']); $container->set('deviceForMigrations', function (Document $project, Telemetry $telemetry) { return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())); }, ['project', 'telemetry']); $container->set('deviceForFunctions', function (Document $project, Telemetry $telemetry) { return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); }, ['project', 'telemetry']); $container->set('deviceForFiles', function (Document $project, Telemetry $telemetry) { return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); }, ['project', 'telemetry']); $container->set('deviceForBuilds', function (Document $project, Telemetry $telemetry) { return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); }, ['project', 'telemetry']); $container->set('deviceForCache', function (Document $project, Telemetry $telemetry) { return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId())); }, ['project', 'telemetry']); $container->set('logError', function (Registry $register, Document $project) { return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) { $logger = $register->get('logger'); if ($logger) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); $log = new Log(); $log->setNamespace($namespace); $log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname())); $log->setVersion($version); $log->setType(Log::TYPE_ERROR); $log->setMessage($error->getMessage()); $log->addTag('code', $error->getCode()); $log->addTag('verboseType', \get_class($error)); $log->addTag('projectId', $project->getId()); $log->addExtra('file', $error->getFile()); $log->addExtra('line', $error->getLine()); $log->addExtra('trace', $error->getTraceAsString()); if ($error->getPrevious() !== null) { if ($error->getPrevious()->getMessage() != $error->getMessage()) { $log->addExtra('previousMessage', $error->getPrevious()->getMessage()); } $log->addExtra('previousFile', $error->getPrevious()->getFile()); $log->addExtra('previousLine', $error->getPrevious()->getLine()); } foreach (($extras ?? []) as $key => $value) { $log->addExtra($key, $value); } $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::warning("Failed: {$error->getMessage()}"); Console::warning($error->getTraceAsString()); if ($error->getPrevious() !== null) { if ($error->getPrevious()->getMessage() != $error->getMessage()) { Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}"); } Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}"); } }; }, ['register', 'project']); $container->set('getAudit', function (Database $dbForPlatform, callable $getProjectDB) { return function (Document $project) use ($dbForPlatform, $getProjectDB) { if ($project->isEmpty() || $project->getId() === 'console') { $adapter = new AdapterDatabase($dbForPlatform); return new UtopiaAudit($adapter); } $dbForProject = $getProjectDB($project); $adapter = new AdapterDatabase($dbForProject); return new UtopiaAudit($adapter); }; }, ['dbForPlatform', 'getProjectDB']); $container->set('executionsRetentionCount', function (Document $project, array $plan) { if ($project->getId() === 'console' || empty($plan)) { return 0; } return (int) ($plan['executionsRetentionCount'] ?? 100); }, ['project', 'plan']); };