groups(['api', 'functions']) ->desc('List available function runtime specifications') ->label('scope', 'functions.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'listSpecifications', description: '/docs/references/functions/list-specifications.md', auth: [AuthType::KEY, AuthType::ADMIN], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_SPECIFICATION_LIST, ) ] )) ->inject('response') ->inject('plan') ->action(function (Response $response, array $plan) { $allRuntimeSpecs = Config::getParam('runtime-specifications', []); $runtimeSpecs = []; foreach ($allRuntimeSpecs as $spec) { $spec['enabled'] = true; if (array_key_exists('runtimeSpecifications', $plan)) { $spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']); } // Only add specs that are within the limits set by environment variables if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) { $runtimeSpecs[] = $spec; } } $response->dynamic(new Document([ 'specifications' => $runtimeSpecs, 'total' => count($runtimeSpecs) ]), Response::MODEL_SPECIFICATION_LIST); }); App::get('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Get function') ->label('scope', 'functions.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'get', description: '/docs/references/functions/get-function.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_FUNCTION, ) ] )) ->param('functionId', '', new UID(), 'Function ID.') ->inject('response') ->inject('dbForProject') ->action(function (string $functionId, Response $response, Database $dbForProject) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $response->dynamic($function, Response::MODEL_FUNCTION); }); App::get('/v1/functions/:functionId/usage') ->desc('Get function usage') ->groups(['api', 'functions', 'usage']) ->label('scope', 'functions.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'getFunctionUsage', description: '/docs/references/functions/get-function-usage.md', auth: [AuthType::ADMIN], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_USAGE_FUNCTION, ) ] )) ->param('functionId', '', new UID(), 'Function ID.') ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') ->action(function (string $functionId, string $range, Response $response, Database $dbForProject) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $periods = Config::getParam('usage', []); $stats = $usage = []; $days = $periods[$range]; $metrics = [ str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS), str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE), str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS), str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE), str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS), str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS) ]; Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; $results = $dbForProject->find('stats', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), Query::orderDesc('time'), ]); $stats[$metric]['data'] = []; foreach ($results as $result) { $stats[$metric]['data'][$result->getAttribute('time')] = [ 'value' => $result->getAttribute('value'), ]; } } }); $format = match ($days['period']) { '1h' => 'Y-m-d\TH:00:00.000P', '1d' => 'Y-m-d\T00:00:00.000P', }; foreach ($metrics as $metric) { $usage[$metric]['total'] = $stats[$metric]['total']; $usage[$metric]['data'] = []; $leap = time() - ($days['limit'] * $days['factor']); while ($leap < time()) { $leap += $days['factor']; $formatDate = date($format, $leap); $usage[$metric]['data'][] = [ 'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0, 'date' => $formatDate, ]; } } $response->dynamic(new Document([ 'range' => $range, 'deploymentsTotal' => $usage[$metrics[0]]['total'], 'deploymentsStorageTotal' => $usage[$metrics[1]]['total'], 'buildsTotal' => $usage[$metrics[2]]['total'], 'buildsStorageTotal' => $usage[$metrics[3]]['total'], 'buildsTimeTotal' => $usage[$metrics[4]]['total'], 'executionsTotal' => $usage[$metrics[5]]['total'], 'executionsTimeTotal' => $usage[$metrics[6]]['total'], 'deployments' => $usage[$metrics[0]]['data'], 'deploymentsStorage' => $usage[$metrics[1]]['data'], 'builds' => $usage[$metrics[2]]['data'], 'buildsStorage' => $usage[$metrics[3]]['data'], 'buildsTime' => $usage[$metrics[4]]['data'], 'executions' => $usage[$metrics[5]]['data'], 'executionsTime' => $usage[$metrics[6]]['data'], 'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'], 'buildsMbSeconds' => $usage[$metrics[7]]['data'], 'executionsMbSeconds' => $usage[$metrics[8]]['data'], 'executionsMbSecondsTotal' => $usage[$metrics[8]]['total'] ]), Response::MODEL_USAGE_FUNCTION); }); App::get('/v1/functions/usage') ->desc('Get functions usage') ->groups(['api', 'functions', 'usage']) ->label('scope', 'functions.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'getUsage', description: '/docs/references/functions/get-functions-usage.md', auth: [AuthType::ADMIN], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_USAGE_FUNCTIONS, ) ] )) ->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true) ->inject('response') ->inject('dbForProject') ->action(function (string $range, Response $response, Database $dbForProject) { $periods = Config::getParam('usage', []); $stats = $usage = []; $days = $periods[$range]; $metrics = [ METRIC_FUNCTIONS, METRIC_DEPLOYMENTS, METRIC_DEPLOYMENTS_STORAGE, METRIC_BUILDS, METRIC_BUILDS_STORAGE, METRIC_BUILDS_COMPUTE, METRIC_EXECUTIONS, METRIC_EXECUTIONS_COMPUTE, METRIC_BUILDS_MB_SECONDS, METRIC_EXECUTIONS_MB_SECONDS, ]; Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { $result = $dbForProject->findOne('stats', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; $results = $dbForProject->find('stats', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), Query::orderDesc('time'), ]); $stats[$metric]['data'] = []; foreach ($results as $result) { $stats[$metric]['data'][$result->getAttribute('time')] = [ 'value' => $result->getAttribute('value'), ]; } } }); $format = match ($days['period']) { '1h' => 'Y-m-d\TH:00:00.000P', '1d' => 'Y-m-d\T00:00:00.000P', }; foreach ($metrics as $metric) { $usage[$metric]['total'] = $stats[$metric]['total']; $usage[$metric]['data'] = []; $leap = time() - ($days['limit'] * $days['factor']); while ($leap < time()) { $leap += $days['factor']; $formatDate = date($format, $leap); $usage[$metric]['data'][] = [ 'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0, 'date' => $formatDate, ]; } } $response->dynamic(new Document([ 'range' => $range, 'functionsTotal' => $usage[$metrics[0]]['total'], 'deploymentsTotal' => $usage[$metrics[1]]['total'], 'deploymentsStorageTotal' => $usage[$metrics[2]]['total'], 'buildsTotal' => $usage[$metrics[3]]['total'], 'buildsStorageTotal' => $usage[$metrics[4]]['total'], 'buildsTimeTotal' => $usage[$metrics[5]]['total'], 'executionsTotal' => $usage[$metrics[6]]['total'], 'executionsTimeTotal' => $usage[$metrics[7]]['total'], 'functions' => $usage[$metrics[0]]['data'], 'deployments' => $usage[$metrics[1]]['data'], 'deploymentsStorage' => $usage[$metrics[2]]['data'], 'builds' => $usage[$metrics[3]]['data'], 'buildsStorage' => $usage[$metrics[4]]['data'], 'buildsTime' => $usage[$metrics[5]]['data'], 'executions' => $usage[$metrics[6]]['data'], 'executionsTime' => $usage[$metrics[7]]['data'], 'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'], 'buildsMbSeconds' => $usage[$metrics[8]]['data'], 'executionsMbSeconds' => $usage[$metrics[9]]['data'], 'executionsMbSecondsTotal' => $usage[$metrics[9]]['total'], ]), Response::MODEL_USAGE_FUNCTIONS); }); App::get('/v1/functions/:functionId/deployments/:deploymentId/download') ->groups(['api', 'functions']) ->desc('Download deployment') ->label('scope', 'functions.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'getDeploymentDownload', description: '/docs/references/functions/get-deployment-download.md', auth: [AuthType::KEY, AuthType::JWT], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_NONE, ) ], contentType: ContentType::ANY, type: MethodType::LOCATION )) ->param('functionId', '', new UID(), 'Function ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') ->inject('request') ->inject('dbForProject') ->inject('deviceForFunctions') ->action(function (string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->isEmpty()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } $path = $deployment->getAttribute('path', ''); if (!$deviceForFunctions->exists($path)) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } $response ->setContentType('application/gzip') ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days ->addHeader('X-Peak', \memory_get_peak_usage()) ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); $size = $deviceForFunctions->getFileSize($path); $rangeHeader = $request->getHeader('range'); if (!empty($rangeHeader)) { $start = $request->getRangeStart(); $end = $request->getRangeEnd(); $unit = $request->getRangeUnit(); if ($end === null) { $end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1)); } if ($unit !== 'bytes' || $start >= $end || $end >= $size) { throw new Exception(Exception::STORAGE_INVALID_RANGE); } $response ->addHeader('Accept-Ranges', 'bytes') ->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size) ->addHeader('Content-Length', $end - $start + 1) ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); $response->send($deviceForFunctions->read($path, $start, ($end - $start + 1))); } if ($size > APP_STORAGE_READ_BUFFER) { for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { $response->chunk( $deviceForFunctions->read( $path, ($i * MAX_OUTPUT_CHUNK_SIZE), min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) ), (($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size ); } } else { $response->send($deviceForFunctions->read($path)); } }); App::patch('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Update deployment') ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('event', 'functions.[functionId].deployments.[deploymentId].update') ->label('audits.event', 'deployment.update') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'updateDeployment') ->label('sdk.description', '/docs/references/functions/update-function-deployment.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_FUNCTION) ->param('functionId', '', new UID(), 'Function ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') ->inject('dbForPlatform') ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform) { $function = $dbForProject->getDocument('functions', $functionId); $deployment = $dbForProject->getDocument('deployments', $deploymentId); $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } if ($deployment->isEmpty()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } if ($build->isEmpty()) { throw new Exception(Exception::BUILD_NOT_FOUND); } if ($build->getAttribute('status') !== 'ready') { throw new Exception(Exception::BUILD_NOT_READY); } $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ 'deploymentInternalId' => $deployment->getInternalId(), 'deployment' => $deployment->getId(), ]))); // Inform scheduler if function is still active $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForEvents ->setParam('functionId', $function->getId()) ->setParam('deploymentId', $deployment->getId()); $response->dynamic($function, Response::MODEL_FUNCTION); }); App::delete('/v1/functions/:functionId') ->groups(['api', 'functions']) ->desc('Delete function') ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('event', 'functions.[functionId].delete') ->label('audits.event', 'function.delete') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( namespace: 'functions', name: 'delete', description: '/docs/references/functions/delete-function.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_NOCONTENT, model: Response::MODEL_NONE, ) ], contentType: ContentType::NONE )) ->param('functionId', '', new UID(), 'Function ID.') ->inject('response') ->inject('dbForProject') ->inject('queueForDeletes') ->inject('queueForEvents') ->inject('dbForPlatform') ->action(function (string $functionId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Database $dbForPlatform) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } if (!$dbForProject->deleteDocument('functions', $function->getId())) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove function from DB'); } // Inform scheduler to no longer run function $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($function); $queueForEvents->setParam('functionId', $function->getId()); $response->noContent(); }); App::get('/v1/functions/:functionId/deployments') ->groups(['api', 'functions']) ->desc('List deployments') ->label('scope', 'functions.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'listDeployments', description: '/docs/references/functions/list-deployments.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_DEPLOYMENT_LIST, ) ] )) ->param('functionId', '', new UID(), 'Function ID.') ->param('queries', [], new Deployments(), '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(', ', Deployments::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') ->inject('dbForProject') ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } if (!empty($search)) { $queries[] = Query::search('search', $search); } // Set resource queries $queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]); $queries[] = Query::equal('resourceType', ['functions']); /** * 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()); } $deploymentId = $cursor->getValue(); $cursorDocument = $dbForProject->getDocument('deployments', $deploymentId); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found."); } $cursor->setValue($cursorDocument); } $filterQueries = Query::groupByType($queries)['filters']; $results = $dbForProject->find('deployments', $queries); $total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT); foreach ($results as $result) { $build = $dbForProject->getDocument('builds', $result->getAttribute('buildId', '')); $result->setAttribute('status', $build->getAttribute('status', 'processing')); $result->setAttribute('buildLogs', $build->getAttribute('logs', '')); $result->setAttribute('buildTime', $build->getAttribute('duration', 0)); $result->setAttribute('buildSize', $build->getAttribute('size', 0)); $result->setAttribute('size', $result->getAttribute('size', 0)); } $response->dynamic(new Document([ 'deployments' => $results, 'total' => $total, ]), Response::MODEL_DEPLOYMENT_LIST); }); App::get('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Get deployment') ->label('scope', 'functions.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'getDeployment', description: '/docs/references/functions/get-deployment.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_DEPLOYMENT, ) ] )) ->param('functionId', '', new UID(), 'Function ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') ->inject('dbForProject') ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } if ($deployment->isEmpty()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')); $deployment->setAttribute('status', $build->getAttribute('status', 'waiting')); $deployment->setAttribute('buildLogs', $build->getAttribute('logs', '')); $deployment->setAttribute('buildTime', $build->getAttribute('duration', 0)); $deployment->setAttribute('buildSize', $build->getAttribute('size', 0)); $deployment->setAttribute('size', $deployment->getAttribute('size', 0)); $response->dynamic($deployment, Response::MODEL_DEPLOYMENT); }); App::delete('/v1/functions/:functionId/deployments/:deploymentId') ->groups(['api', 'functions']) ->desc('Delete deployment') ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('event', 'functions.[functionId].deployments.[deploymentId].delete') ->label('audits.event', 'deployment.delete') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( namespace: 'functions', name: 'deleteDeployment', description: '/docs/references/functions/delete-deployment.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_NOCONTENT, model: Response::MODEL_NONE, ) ], contentType: ContentType::NONE )) ->param('functionId', '', new UID(), 'Function ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') ->inject('dbForProject') ->inject('queueForDeletes') ->inject('queueForEvents') ->inject('deviceForFunctions') ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Device $deviceForFunctions) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->isEmpty()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } if (!$dbForProject->deleteDocument('deployments', $deployment->getId())) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB'); } if (!empty($deployment->getAttribute('path', ''))) { if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage'); } } if ($function->getAttribute('deployment') === $deployment->getId()) { // Reset function deployment $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ 'deployment' => '', 'deploymentInternalId' => '', ]))); } $queueForEvents ->setParam('functionId', $function->getId()) ->setParam('deploymentId', $deployment->getId()); $queueForDeletes ->setType(DELETE_TYPE_DOCUMENT) ->setDocument($deployment); $response->noContent(); }); App::post('/v1/functions/:functionId/deployments/:deploymentId/build') ->alias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') ->groups(['api', 'functions']) ->desc('Rebuild deployment') ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('event', 'functions.[functionId].deployments.[deploymentId].update') ->label('audits.event', 'deployment.update') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( namespace: 'functions', name: 'createBuild', description: '/docs/references/functions/create-build.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_NOCONTENT, model: Response::MODEL_NONE, ) ] )) ->param('functionId', '', new UID(), 'Function ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility ->inject('request') ->inject('response') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') ->inject('queueForBuilds') ->inject('deviceForFunctions') ->action(function (string $functionId, string $deploymentId, string $buildId, Request $request, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->isEmpty()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } $path = $deployment->getAttribute('path'); if (empty($path) || !$deviceForFunctions->exists($path)) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } $deploymentId = ID::unique(); $destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); $deviceForFunctions->transfer($path, $destination, $deviceForFunctions); $deployment->removeAttribute('$internalId'); $deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([ '$internalId' => '', '$id' => $deploymentId, 'buildId' => '', 'buildInternalId' => '', 'path' => $destination, 'entrypoint' => $function->getAttribute('entrypoint'), 'commands' => $function->getAttribute('commands', ''), 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]), ])); $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) ->setResource($function) ->setDeployment($deployment); $queueForEvents ->setParam('functionId', $function->getId()) ->setParam('deploymentId', $deployment->getId()); $response->noContent(); }); App::patch('/v1/functions/:functionId/deployments/:deploymentId/build') ->groups(['api', 'functions']) ->desc('Cancel deployment') ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('audits.event', 'deployment.update') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'updateDeploymentBuild') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_BUILD) ->param('functionId', '', new UID(), 'Function ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') ->inject('response') ->inject('dbForProject') ->inject('project') ->inject('queueForEvents') ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $deployment = $dbForProject->getDocument('deployments', $deploymentId); if ($deployment->isEmpty()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { $buildId = ID::unique(); $build = $dbForProject->createDocument('builds', new Document([ '$id' => $buildId, '$permissions' => [], 'startTime' => DateTime::now(), 'deploymentInternalId' => $deployment->getInternalId(), 'deploymentId' => $deployment->getId(), 'status' => 'canceled', 'path' => '', 'runtime' => $function->getAttribute('runtime'), 'source' => $deployment->getAttribute('path', ''), 'sourceType' => '', 'logs' => '', 'duration' => 0, 'size' => 0 ])); $deployment->setAttribute('buildId', $build->getId()); $deployment->setAttribute('buildInternalId', $build->getInternalId()); $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); } else { if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) { throw new Exception(Exception::BUILD_ALREADY_COMPLETED); } $startTime = new \DateTime($build->getAttribute('startTime')); $endTime = new \DateTime('now'); $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([ 'endTime' => DateTime::now(), 'duration' => $duration, 'status' => 'canceled' ])); } $dbForProject->purgeCachedDocument('deployments', $deployment->getId()); try { $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); } catch (\Throwable $th) { // Don't throw if the deployment doesn't exist if ($th->getCode() !== 404) { throw $th; } } $queueForEvents ->setParam('functionId', $function->getId()) ->setParam('deploymentId', $deployment->getId()); $response->dynamic($build, Response::MODEL_BUILD); }); App::post('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('Create execution') ->label('scope', 'execution.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('event', 'functions.[functionId].executions.[executionId].create') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'createExecution', description: '/docs/references/functions/create-execution.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], responses: [ new SDKResponse( code: Response::STATUS_CODE_CREATED, model: Response::MODEL_EXECUTION, ) ], contentType: ContentType::MULTIPART, requestType: 'application/json', )) ->param('functionId', '', new UID(), 'Function ID.') ->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true) ->param('async', false, new Boolean(true), 'Execute code in the background. Default value is false.', true) ->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true) ->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'HTTP method of execution. Default value is GET.', true) ->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_MINUTES, offset: 60), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true) ->inject('response') ->inject('request') ->inject('project') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('user') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('queueForFunctions') ->inject('geodb') ->action(function (string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, Usage $queueForUsage, Func $queueForFunctions, Reader $geodb) { $async = \strval($async) === 'true' || \strval($async) === '1'; if (!$async && !is_null($scheduledAt)) { throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Scheduled executions must run asynchronously. Set scheduledAt to a future date, or set async to true.'); } /** * @var array $headers */ $assocParams = ['headers']; foreach ($assocParams as $assocParam) { if (!empty('headers') && !is_array($$assocParam)) { $$assocParam = \json_decode($$assocParam, true); } } $booleanParams = ['async']; foreach ($booleanParams as $booleamParam) { if (!empty($$booleamParam) && !is_bool($$booleamParam)) { $$booleamParam = $$booleamParam === "true" ? true : false; } } // 'headers' validator $validator = new Headers(); if (!$validator->isValid($headers)) { throw new Exception($validator->getDescription(), 400); } $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $version = $function->getAttribute('version', 'v2'); $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); $spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; if (\is_null($runtime)) { throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); } $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); if ($deployment->getAttribute('resourceId') !== $function->getId()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); } if ($deployment->isEmpty()) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); } /** Check if build has completed */ $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); if ($build->isEmpty()) { throw new Exception(Exception::BUILD_NOT_FOUND); } if ($build->getAttribute('status') !== 'ready') { throw new Exception(Exception::BUILD_NOT_READY); } $validator = new Authorization('execute'); if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription()); } $jwt = ''; // initialize if (!$user->isEmpty()) { // If userId exists, generate a JWT for function $sessions = $user->getAttribute('sessions', []); $current = new Document(); foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $current = $session; } } if (!$current->isEmpty()) { $jwtExpiry = $function->getAttribute('timeout', 900); $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); $jwt = $jwtObj->encode([ 'userId' => $user->getId(), 'sessionId' => $current->getId(), ]); } } $jwtExpiry = $function->getAttribute('timeout', 900); $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); $apiKey = $jwtObj->encode([ 'projectId' => $project->getId(), 'scopes' => $function->getAttribute('scopes', []) ]); $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-id'] = $user->getId() ?? ''; $headers['x-appwrite-user-jwt'] = $jwt ?? ''; $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { $record = $geodb->get($ip); if ($record) { $eu = Config::getParam('locale-eu'); $headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? ''; $headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? ''; $headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false'; } } $headersFiltered = []; foreach ($headers as $key => $value) { if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { $headersFiltered[] = ['name' => $key, 'value' => $value]; } } $executionId = ID::unique(); $status = $async ? 'waiting' : 'processing'; if (!is_null($scheduledAt)) { $status = 'scheduled'; } $execution = new Document([ '$id' => $executionId, '$permissions' => !$user->isEmpty() ? [Permission::read(Role::user($user->getId()))] : [], 'resourceInternalId' => $function->getInternalId(), 'resourceId' => $function->getId(), 'resourceType' => 'functions', 'deploymentInternalId' => $deployment->getInternalId(), 'deploymentId' => $deployment->getId(), 'trigger' => (!is_null($scheduledAt)) ? 'schedule' : 'http', 'status' => $status, // waiting / processing / completed / failed / scheduled 'responseStatusCode' => 0, 'responseHeaders' => [], 'requestPath' => $path, 'requestMethod' => $method, 'requestHeaders' => $headersFiltered, 'errors' => '', 'logs' => '', 'duration' => 0.0, 'search' => implode(' ', [$functionId, $executionId]), ]); $queueForEvents ->setParam('functionId', $function->getId()) ->setParam('executionId', $execution->getId()) ->setContext('function', $function); if ($async) { if (is_null($scheduledAt)) { $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); $queueForFunctions ->setType('http') ->setExecution($execution) ->setFunction($function) ->setBody($body) ->setHeaders($headers) ->setPath($path) ->setMethod($method) ->setJWT($jwt) ->setProject($project) ->setUser($user) ->setParam('functionId', $function->getId()) ->setParam('executionId', $execution->getId()) ->trigger(); } else { $data = [ 'headers' => $headers, 'path' => $path, 'method' => $method, 'body' => $body, 'userId' => $user->getId() ]; $schedule = $dbForPlatform->createDocument('schedules', new Document([ 'region' => System::getEnv('_APP_REGION', 'default'), 'resourceType' => ScheduleExecutions::getSupportedResource(), 'resourceId' => $execution->getId(), 'resourceInternalId' => $execution->getInternalId(), 'resourceUpdatedAt' => DateTime::now(), 'projectId' => $project->getId(), 'schedule' => $scheduledAt, 'data' => $data, 'active' => true, ])); $execution = $execution ->setAttribute('scheduleId', $schedule->getId()) ->setAttribute('scheduleInternalId', $schedule->getInternalId()) ->setAttribute('scheduledAt', $scheduledAt); $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); } return $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) ->dynamic($execution, Response::MODEL_EXECUTION); } $durationStart = \microtime(true); $vars = []; // V2 vars if ($version === 'v2') { $vars = \array_merge($vars, [ 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', 'APPWRITE_FUNCTION_DATA' => $body ?? '', 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' ]); } // Shared vars foreach ($function->getAttribute('varsProject', []) as $var) { $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); } // Function vars foreach ($function->getAttribute('vars', []) as $var) { $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); } $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https'; $hostname = System::getEnv('_APP_DOMAIN'); $endpoint = $protocol . '://' . $hostname . "/v1"; // Appwrite vars $vars = \array_merge($vars, [ 'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint, 'APPWRITE_FUNCTION_ID' => $functionId, 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'), 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', 'APPWRITE_COMPUTE_CPUS' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, 'APPWRITE_COMPUTE_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, 'APPWRITE_VERSION' => APP_VERSION_STABLE, 'APPWRITE_REGION' => $project->getAttribute('region'), 'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''), 'APPWRITE_VCS_REPOSITORY_ID' => $deployment->getAttribute('providerRepositoryId', ''), 'APPWRITE_VCS_REPOSITORY_NAME' => $deployment->getAttribute('providerRepositoryName', ''), 'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment->getAttribute('providerRepositoryOwner', ''), 'APPWRITE_VCS_REPOSITORY_URL' => $deployment->getAttribute('providerRepositoryUrl', ''), 'APPWRITE_VCS_REPOSITORY_BRANCH' => $deployment->getAttribute('providerBranch', ''), 'APPWRITE_VCS_REPOSITORY_BRANCH_URL' => $deployment->getAttribute('providerBranchUrl', ''), 'APPWRITE_VCS_COMMIT_HASH' => $deployment->getAttribute('providerCommitHash', ''), 'APPWRITE_VCS_COMMIT_MESSAGE' => $deployment->getAttribute('providerCommitMessage', ''), 'APPWRITE_VCS_COMMIT_URL' => $deployment->getAttribute('providerCommitUrl', ''), 'APPWRITE_VCS_COMMIT_AUTHOR_NAME' => $deployment->getAttribute('providerCommitAuthor', ''), 'APPWRITE_VCS_COMMIT_AUTHOR_URL' => $deployment->getAttribute('providerCommitAuthorUrl', ''), 'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''), ]); /** Execute function */ $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); try { $version = $function->getAttribute('version', 'v2'); $command = $runtime['startCommand']; $command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'; $executionResponse = $executor->createExecution( projectId: $project->getId(), deploymentId: $deployment->getId(), body: \strlen($body) > 0 ? $body : null, variables: $vars, timeout: $function->getAttribute('timeout', 0), image: $runtime['image'], source: $build->getAttribute('path', ''), entrypoint: $deployment->getAttribute('entrypoint', ''), version: $version, path: $path, method: $method, headers: $headers, runtimeEntrypoint: $command, cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT, memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, logging: $function->getAttribute('logging', true), requestTimeout: 30 ); $headersFiltered = []; foreach ($executionResponse['headers'] as $key => $value) { if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { $headersFiltered[] = ['name' => $key, 'value' => $value]; } } /** Update execution status */ $status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed'; $execution->setAttribute('status', $status); $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); $execution->setAttribute('responseHeaders', $headersFiltered); $execution->setAttribute('logs', $executionResponse['logs']); $execution->setAttribute('errors', $executionResponse['errors']); $execution->setAttribute('duration', $executionResponse['duration']); } catch (\Throwable $th) { $durationEnd = \microtime(true); $execution ->setAttribute('duration', $durationEnd - $durationStart) ->setAttribute('status', 'failed') ->setAttribute('responseStatusCode', 500) ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); Console::error($th->getMessage()); if ($th instanceof AppwriteException) { throw $th; } } finally { $queueForUsage ->addMetric(METRIC_EXECUTIONS, 1) ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1) ->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function ->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) ; $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); } $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); if (!$isPrivilegedUser && !$isAppUser) { $execution->setAttribute('logs', ''); $execution->setAttribute('errors', ''); } $headers = []; foreach (($executionResponse['headers'] ?? []) as $key => $value) { $headers[] = ['name' => $key, 'value' => $value]; } $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); $execution->setAttribute('responseHeaders', $headers); $acceptTypes = \explode(', ', $request->getHeader('accept')); foreach ($acceptTypes as $acceptType) { if (\str_starts_with($acceptType, 'application/json') || \str_starts_with($acceptType, 'application/*')) { $response->setContentType(Response::CONTENT_TYPE_JSON); break; } elseif (\str_starts_with($acceptType, 'multipart/form-data') || \str_starts_with($acceptType, 'multipart/*')) { $response->setContentType(Response::CONTENT_TYPE_MULTIPART); break; } } $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($execution, Response::MODEL_EXECUTION); }); App::get('/v1/functions/:functionId/executions') ->groups(['api', 'functions']) ->desc('List executions') ->label('scope', 'execution.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'listExecutions', description: '/docs/references/functions/list-executions.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_EXECUTION_LIST, ) ] )) ->param('functionId', '', new UID(), 'Function ID.') ->param('queries', [], new Executions(), '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(', ', Executions::ALLOWED_ATTRIBUTES), true) ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') ->inject('dbForProject') ->inject('mode') ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } try { $queries = Query::parseQueries($queries); } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } if (!empty($search)) { $queries[] = Query::search('search', $search); } // Set internal queries $queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]); $queries[] = Query::equal('resourceType', ['functions']); /** * 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()); } $executionId = $cursor->getValue(); $cursorDocument = $dbForProject->getDocument('executions', $executionId); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found."); } $cursor->setValue($cursorDocument); } $filterQueries = Query::groupByType($queries)['filters']; $results = $dbForProject->find('executions', $queries); $total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT); $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); if (!$isPrivilegedUser && !$isAppUser) { $results = array_map(function ($execution) { $execution->setAttribute('logs', ''); $execution->setAttribute('errors', ''); return $execution; }, $results); } $response->dynamic(new Document([ 'executions' => $results, 'total' => $total, ]), Response::MODEL_EXECUTION_LIST); }); App::get('/v1/functions/:functionId/executions/:executionId') ->groups(['api', 'functions']) ->desc('Get execution') ->label('scope', 'execution.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'getExecution', description: '/docs/references/functions/get-execution.md', auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_EXECUTION, ) ] )) ->param('functionId', '', new UID(), 'Function ID.') ->param('executionId', '', new UID(), 'Execution ID.') ->inject('response') ->inject('dbForProject') ->inject('mode') ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode) { $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $execution = $dbForProject->getDocument('executions', $executionId); if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { throw new Exception(Exception::EXECUTION_NOT_FOUND); } if ($execution->isEmpty()) { throw new Exception(Exception::EXECUTION_NOT_FOUND); } $roles = Authorization::getRoles(); $isPrivilegedUser = Auth::isPrivilegedUser($roles); $isAppUser = Auth::isAppUser($roles); if (!$isPrivilegedUser && !$isAppUser) { $execution->setAttribute('logs', ''); $execution->setAttribute('errors', ''); } $response->dynamic($execution, Response::MODEL_EXECUTION); }); App::delete('/v1/functions/:functionId/executions/:executionId') ->groups(['api', 'functions']) ->desc('Delete execution') ->label('scope', 'execution.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('event', 'functions.[functionId].executions.[executionId].delete') ->label('audits.event', 'executions.delete') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( namespace: 'functions', name: 'deleteExecution', description: '/docs/references/functions/delete-execution.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_NOCONTENT, model: Response::MODEL_NONE, ) ], contentType: ContentType::NONE )) ->param('functionId', '', new UID(), 'Function ID.') ->param('executionId', '', new UID(), 'Execution ID.') ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') ->inject('queueForEvents') ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $execution = $dbForProject->getDocument('executions', $executionId); if ($execution->isEmpty()) { throw new Exception(Exception::EXECUTION_NOT_FOUND); } if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) { throw new Exception(Exception::EXECUTION_NOT_FOUND); } $status = $execution->getAttribute('status'); if (!in_array($status, ['completed', 'failed', 'scheduled'])) { throw new Exception(Exception::EXECUTION_IN_PROGRESS); } if (!$dbForProject->deleteDocument('executions', $execution->getId())) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove execution from DB'); } if ($status === 'scheduled') { $schedule = $dbForPlatform->findOne('schedules', [ Query::equal('resourceId', [$execution->getId()]), Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]), Query::equal('active', [true]), ]); if (!$schedule->isEmpty()) { $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); } } $queueForEvents ->setParam('functionId', $function->getId()) ->setParam('executionId', $execution->getId()) ->setPayload($response->output($execution, Response::MODEL_EXECUTION)); $response->noContent(); }); // Variables App::post('/v1/functions/:functionId/variables') ->desc('Create variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('audits.event', 'variable.create') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk', new Method( namespace: 'functions', name: 'createVariable', description: '/docs/references/functions/create-variable.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_CREATED, model: Response::MODEL_VARIABLE, ) ] )) ->param('functionId', '', new UID(), 'Function unique ID.', false) ->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false) ->param('secret', false, new Boolean(), 'Is secret? Secret variables can only be updated or deleted, they cannot be read.', true) ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') ->action(function (string $functionId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $variableId = ID::unique(); $variable = new Document([ '$id' => $variableId, '$permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], 'resourceInternalId' => $function->getInternalId(), 'resourceId' => $function->getId(), 'resourceType' => 'function', 'key' => $key, 'value' => $value, 'secret' => $secret, 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']), ]); try { $variable = $dbForProject->createDocument('variables', $variable); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); // Inform scheduler to pull the latest changes $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($variable, Response::MODEL_VARIABLE); }); App::get('/v1/functions/:functionId/variables') ->desc('List variables') ->groups(['api', 'functions']) ->label('scope', 'functions.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label( 'sdk', new Method( namespace: 'functions', name: 'listVariables', description: '/docs/references/functions/list-variables.md', auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_VARIABLE_LIST, ) ], ) ) ->param('functionId', '', new UID(), 'Function unique ID.', false) ->inject('response') ->inject('dbForProject') ->action(function (string $functionId, Response $response, Database $dbForProject) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $response->dynamic(new Document([ 'variables' => $function->getAttribute('vars', []), 'total' => \count($function->getAttribute('vars', [])), ]), Response::MODEL_VARIABLE_LIST); }); App::get('/v1/functions/:functionId/variables/:variableId') ->desc('Get variable') ->groups(['api', 'functions']) ->label('scope', 'functions.read') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'getVariable') ->label('sdk.description', '/docs/references/functions/get-variable.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_VARIABLE) ->param('functionId', '', new UID(), 'Function unique ID.', false) ->param('variableId', '', new UID(), 'Variable unique ID.', false) ->inject('response') ->inject('dbForProject') ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $variable = $dbForProject->getDocument('variables', $variableId); if ( $variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function' ) { throw new Exception(Exception::VARIABLE_NOT_FOUND); } if ($variable === false || $variable->isEmpty()) { throw new Exception(Exception::VARIABLE_NOT_FOUND); } $response->dynamic($variable, Response::MODEL_VARIABLE); }); App::put('/v1/functions/:functionId/variables/:variableId') ->desc('Update variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('audits.event', 'variable.update') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'updateVariable') ->label('sdk.description', '/docs/references/functions/update-variable.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_VARIABLE) ->param('functionId', '', new UID(), 'Function unique ID.', false) ->param('variableId', '', new UID(), 'Variable unique ID.', false) ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false) ->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true) ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') ->action(function (string $functionId, string $variableId, string $key, ?string $value, Response $response, Database $dbForProject, Database $dbForPlatform) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $variable = $dbForProject->getDocument('variables', $variableId); if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { throw new Exception(Exception::VARIABLE_NOT_FOUND); } if ($variable === false || $variable->isEmpty()) { throw new Exception(Exception::VARIABLE_NOT_FOUND); } $variable ->setAttribute('key', $key) ->setAttribute('value', $value ?? $variable->getAttribute('value')) ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); try { $dbForProject->updateDocument('variables', $variable->getId(), $variable); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); // Inform scheduler to pull the latest changes $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $response->dynamic($variable, Response::MODEL_VARIABLE); }); App::delete('/v1/functions/:functionId/variables/:variableId') ->desc('Delete variable') ->groups(['api', 'functions']) ->label('scope', 'functions.write') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('audits.event', 'variable.delete') ->label('audits.resource', 'function/{request.functionId}') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'functions') ->label('sdk.method', 'deleteVariable') ->label('sdk.description', '/docs/references/functions/delete-variable.md') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) ->param('functionId', '', new UID(), 'Function unique ID.', false) ->param('variableId', '', new UID(), 'Variable unique ID.', false) ->inject('response') ->inject('dbForProject') ->inject('dbForPlatform') ->action(function (string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForPlatform) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } $variable = $dbForProject->getDocument('variables', $variableId); if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') { throw new Exception(Exception::VARIABLE_NOT_FOUND); } if ($variable === false || $variable->isEmpty()) { throw new Exception(Exception::VARIABLE_NOT_FOUND); } $dbForProject->deleteDocument('variables', $variable->getId()); $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); // Inform scheduler to pull the latest changes $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment'))); Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); $response->noContent(); }); App::get('/v1/functions/templates') ->groups(['api']) ->desc('List function templates') ->label('scope', 'public') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'listTemplates', description: '/docs/references/functions/list-templates.md', auth: [AuthType::ADMIN], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_TEMPLATE_FUNCTION_LIST, ) ] )) ->param('runtimes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('runtimes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of runtimes allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' runtimes are allowed.', true) ->param('useCases', [], new ArrayList(new WhiteList(['dev-tools','starter','databases','ai','messaging','utilities']), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true) ->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true) ->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true) ->inject('response') ->action(function (array $runtimes, array $usecases, int $limit, int $offset, Response $response) { $templates = Config::getParam('function-templates', []); if (!empty($runtimes)) { $templates = \array_filter($templates, function ($template) use ($runtimes) { return \count(\array_intersect($runtimes, \array_column($template['runtimes'], 'name'))) > 0; }); } if (!empty($usecases)) { $templates = \array_filter($templates, function ($template) use ($usecases) { return \count(\array_intersect($usecases, $template['useCases'])) > 0; }); } $responseTemplates = \array_slice($templates, $offset, $limit); $response->dynamic(new Document([ 'templates' => $responseTemplates, 'total' => \count($responseTemplates), ]), Response::MODEL_TEMPLATE_FUNCTION_LIST); }); App::get('/v1/functions/templates/:templateId') ->desc('Get function template') ->label('scope', 'public') ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) ->label('sdk', new Method( namespace: 'functions', name: 'getTemplate', description: '/docs/references/functions/get-template.md', auth: [AuthType::ADMIN], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, model: Response::MODEL_TEMPLATE_FUNCTION, ) ] )) ->param('templateId', '', new Text(128), 'Template ID.') ->inject('response') ->action(function (string $templateId, Response $response) { $templates = Config::getParam('function-templates', []); $filtered = \array_filter($templates, function ($template) use ($templateId) { return $template['id'] === $templateId; }); $template = array_shift($filtered); if (empty($template)) { throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND); } $response->dynamic(new Document($template), Response::MODEL_TEMPLATE_FUNCTION); });