1
0
Fork 0
mirror of synced 2024-09-20 03:17:30 +12:00

Merge pull request #8427 from appwrite/feat-move-functions-marketplace-to-appwrite

Move functions marketplace to appwrite
This commit is contained in:
Christy Jacob 2024-07-30 17:07:15 +04:00 committed by GitHub
commit c857f2ddf9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 2517 additions and 0 deletions

View file

@ -529,6 +529,11 @@ return [
'description' => 'Synchronous function execution timed out. Use asynchronous execution instead, or ensure the execution duration doesn\'t exceed 30 seconds.', 'description' => 'Synchronous function execution timed out. Use asynchronous execution instead, or ensure the execution duration doesn\'t exceed 30 seconds.',
'code' => 408, 'code' => 408,
], ],
Exception::FUNCTION_TEMPLATE_NOT_FOUND => [
'name' => Exception::FUNCTION_TEMPLATE_NOT_FOUND,
'description' => 'Function Template with the requested ID could not be found.',
'code' => 404,
],
/** Builds */ /** Builds */
Exception::BUILD_NOT_FOUND => [ Exception::BUILD_NOT_FOUND => [

File diff suppressed because it is too large Load diff

View file

@ -2356,3 +2356,64 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
$response->noContent(); $response->noContent();
}); });
App::get('/v1/functions/templates')
->desc('List function templates')
->label('scope', 'public')
->label('sdk.namespace', 'functions')
->label('sdk.method', 'listTemplates')
->label('sdk.description', '/docs/references/functions/list-templates.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.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('sdk.namespace', 'functions')
->label('sdk.method', 'getTemplate')
->label('sdk.description', '/docs/references/functions/get-template.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.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', []);
$template = array_shift(\array_filter($templates, function ($template) use ($templateId) {
return $template['id'] === $templateId;
}));
if (empty($template)) {
throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND);
}
$response->dynamic(new Document($template), Response::MODEL_TEMPLATE_FUNCTION);
});

View file

@ -303,6 +303,7 @@ Config::load('storage-logos', __DIR__ . '/config/storage/logos.php');
Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php'); Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php');
Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php'); Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php'); Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php');
Config::load('function-templates', __DIR__ . '/config/function-templates.php');
/** /**
* New DB Filters * New DB Filters

View file

@ -0,0 +1 @@
Get a function template using ID. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.

View file

@ -0,0 +1 @@
List available function templates. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.

View file

@ -156,6 +156,7 @@ class Exception extends \Exception
public const FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported'; public const FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
public const FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing'; public const FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout'; public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
public const FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found';
/** Deployments */ /** Deployments */
public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found'; public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found';

View file

@ -87,7 +87,10 @@ use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Target; use Appwrite\Utopia\Response\Model\Target;
use Appwrite\Utopia\Response\Model\Team; use Appwrite\Utopia\Response\Model\Team;
use Appwrite\Utopia\Response\Model\TemplateEmail; use Appwrite\Utopia\Response\Model\TemplateEmail;
use Appwrite\Utopia\Response\Model\TemplateFunction;
use Appwrite\Utopia\Response\Model\TemplateRuntime;
use Appwrite\Utopia\Response\Model\TemplateSMS; use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\TemplateVariable;
use Appwrite\Utopia\Response\Model\Token; use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Topic; use Appwrite\Utopia\Response\Model\Topic;
use Appwrite\Utopia\Response\Model\UsageBuckets; use Appwrite\Utopia\Response\Model\UsageBuckets;
@ -251,6 +254,10 @@ class Response extends SwooleResponse
public const MODEL_BUILD_LIST = 'buildList'; // Not used anywhere yet public const MODEL_BUILD_LIST = 'buildList'; // Not used anywhere yet
public const MODEL_FUNC_PERMISSIONS = 'funcPermissions'; public const MODEL_FUNC_PERMISSIONS = 'funcPermissions';
public const MODEL_HEADERS = 'headers'; public const MODEL_HEADERS = 'headers';
public const MODEL_TEMPLATE_FUNCTION = 'templateFunction';
public const MODEL_TEMPLATE_FUNCTION_LIST = 'templateFunctionList';
public const MODEL_TEMPLATE_RUNTIME = 'templateRuntime';
public const MODEL_TEMPLATE_VARIABLE = 'templateVariable';
// Proxy // Proxy
public const MODEL_PROXY_RULE = 'proxyRule'; public const MODEL_PROXY_RULE = 'proxyRule';
@ -340,6 +347,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM)) ->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM))
->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP)) ->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP))
->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION)) ->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION))
->setModel(new BaseList('Function Templates List', self::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', self::MODEL_TEMPLATE_FUNCTION))
->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION)) ->setModel(new BaseList('Installations List', self::MODEL_INSTALLATION_LIST, 'installations', self::MODEL_INSTALLATION))
->setModel(new BaseList('Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', self::MODEL_PROVIDER_REPOSITORY)) ->setModel(new BaseList('Provider Repositories List', self::MODEL_PROVIDER_REPOSITORY_LIST, 'providerRepositories', self::MODEL_PROVIDER_REPOSITORY))
->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH)) ->setModel(new BaseList('Branches List', self::MODEL_BRANCH_LIST, 'branches', self::MODEL_BRANCH))
@ -409,6 +417,9 @@ class Response extends SwooleResponse
->setModel(new Team()) ->setModel(new Team())
->setModel(new Membership()) ->setModel(new Membership())
->setModel(new Func()) ->setModel(new Func())
->setModel(new TemplateFunction())
->setModel(new TemplateRuntime())
->setModel(new TemplateVariable())
->setModel(new Installation()) ->setModel(new Installation())
->setModel(new ProviderRepository()) ->setModel(new ProviderRepository())
->setModel(new Detection()) ->setModel(new Detection())

View file

@ -0,0 +1,136 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class TemplateFunction extends Model
{
public function __construct()
{
$this
->addRule('icon', [
'type' => self::TYPE_STRING,
'description' => 'Function Template Icon.',
'default' => '',
'example' => 'icon-lightning-bolt',
])
->addRule('id', [
'type' => self::TYPE_STRING,
'description' => 'Function Template ID.',
'default' => '',
'example' => 'starter',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Function Template Name.',
'default' => '',
'example' => 'Starter function',
])
->addRule('tagline', [
'type' => self::TYPE_STRING,
'description' => 'Function Template Tagline.',
'default' => '',
'example' => 'A simple function to get started.',
])
->addRule('permissions', [
'type' => self::TYPE_STRING,
'description' => 'Execution permissions.',
'default' => [],
'example' => 'any',
'array' => true,
])
->addRule('events', [
'type' => self::TYPE_STRING,
'description' => 'Function trigger events.',
'default' => [],
'example' => 'account.create',
'array' => true,
])
->addRule('cron', [
'type' => self::TYPE_STRING,
'description' => 'Function execution schedult in CRON format.',
'default' => '',
'example' => '0 0 * * *',
])
->addRule('timeout', [
'type' => self::TYPE_INTEGER,
'description' => 'Function execution timeout in seconds.',
'default' => 15,
'example' => 300,
])
->addRule('useCases', [
'type' => self::TYPE_STRING,
'description' => 'Function use cases.',
'default' => [],
'example' => 'Starter',
'array' => true,
])
->addRule('runtimes', [
'type' => Response::MODEL_TEMPLATE_RUNTIME,
'description' => 'List of runtimes that can be used with this template.',
'default' => [],
'example' => [],
'array' => true
])
->addRule('instructions', [
'type' => self::TYPE_STRING,
'description' => 'Function Template Instructions.',
'default' => '',
'example' => 'For documentation and instructions check out <link>.',
])
->addRule('vcsProvider', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Provider.',
'default' => '',
'example' => 'github',
])
->addRule('providerRepositoryId', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Repository ID',
'default' => '',
'example' => 'templates',
])
->addRule('providerOwner', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) Owner.',
'default' => '',
'example' => 'appwrite',
])
->addRule('providerBranch', [
'type' => self::TYPE_STRING,
'description' => 'VCS (Version Control System) branch name',
'default' => '',
'example' => 'main',
])
->addRule('variables', [
'type' => Response::MODEL_TEMPLATE_VARIABLE,
'description' => 'Function variables.',
'default' => [],
'example' => [],
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Template Function';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TEMPLATE_FUNCTION;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class TemplateRuntime extends Model
{
public function __construct()
{
$this
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Runtime Name.',
'default' => '',
'example' => 'node-19.0',
])
->addRule('commands', [
'type' => self::TYPE_STRING,
'description' => 'The build command used to build the deployment.',
'default' => '',
'example' => 'npm install',
])
->addRule('entrypoint', [
'type' => self::TYPE_STRING,
'description' => 'The entrypoint file used to execute the deployment.',
'default' => '',
'example' => 'index.js',
])
->addRule('providerRootDirectory', [
'type' => self::TYPE_STRING,
'description' => 'Path to function in VCS (Version Control System) repository',
'default' => '',
'example' => 'node/starter',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Template Runtime';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TEMPLATE_RUNTIME;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class TemplateVariable extends Model
{
public function __construct()
{
$this
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Variable Name.',
'default' => '',
'example' => 'APPWRITE_DATABASE_ID',
])
->addRule('description', [
'type' => self::TYPE_STRING,
'description' => 'Variable Description.',
'default' => '',
'example' => 'The ID of the Appwrite database that contains the collection to sync.',
])
->addRule('placeholder', [
'type' => self::TYPE_STRING,
'description' => 'Variable Placeholder.',
'default' => '',
'example' => '64a55...7b912',
])
->addRule('required', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is the variable required?',
'default' => false,
'example' => false,
])
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Variable Type.',
'default' => '',
'example' => 'password',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Template Variable';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TEMPLATE_VARIABLE;
}
}

View file

@ -8,6 +8,7 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient; use Tests\E2E\Scopes\SideClient;
use Utopia\Config\Config;
use Utopia\Database\DateTime; use Utopia\Database\DateTime;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\ID;
@ -963,4 +964,131 @@ class FunctionsCustomClientTest extends Scope
return []; return [];
} }
public function testListTemplates()
{
/**
* Test for SUCCESS
*/
$expectedTemplates = array_slice(Config::getParam('function-templates', []), 0, 25);
$templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([
'content-type' => 'application/json',
], $this->getHeaders()));
$this->assertEquals(200, $templates['headers']['status-code']);
$this->assertGreaterThan(0, $templates['body']['total']);
$this->assertIsArray($templates['body']['templates']);
$this->assertArrayHasKey('runtimes', $templates['body']['templates'][0]);
$this->assertArrayHasKey('useCases', $templates['body']['templates'][0]);
for ($i = 0; $i < 25; $i++) {
$this->assertEquals($expectedTemplates[$i]['name'], $templates['body']['templates'][$i]['name']);
$this->assertEquals($expectedTemplates[$i]['id'], $templates['body']['templates'][$i]['id']);
$this->assertEquals($expectedTemplates[$i]['icon'], $templates['body']['templates'][$i]['icon']);
$this->assertEquals($expectedTemplates[$i]['tagline'], $templates['body']['templates'][$i]['tagline']);
$this->assertEquals($expectedTemplates[$i]['useCases'], $templates['body']['templates'][$i]['useCases']);
$this->assertEquals($expectedTemplates[$i]['vcsProvider'], $templates['body']['templates'][$i]['vcsProvider']);
$this->assertEquals($expectedTemplates[$i]['runtimes'], $templates['body']['templates'][$i]['runtimes']);
$this->assertEquals($expectedTemplates[$i]['variables'], $templates['body']['templates'][$i]['variables']);
}
$templates_offset = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([
'content-type' => 'application/json',
], $this->getHeaders()), [
'limit' => 1,
'offset' => 2
]);
$this->assertEquals(200, $templates_offset['headers']['status-code']);
$this->assertEquals(1, $templates_offset['body']['total']);
// assert that offset works as expected
$this->assertEquals($templates['body']['templates'][2]['id'], $templates_offset['body']['templates'][0]['id']);
$templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([
'content-type' => 'application/json',
], $this->getHeaders()), [
'useCases' => ['starter', 'ai'],
'runtimes' => ['bun-1.0', 'dart-2.16']
]);
$this->assertEquals(200, $templates['headers']['status-code']);
$this->assertGreaterThanOrEqual(3, $templates['body']['total']);
$this->assertIsArray($templates['body']['templates']);
foreach ($templates['body']['templates'] as $template) {
$this->assertContains($template['useCases'][0], ['starter', 'ai']);
}
$this->assertArrayHasKey('runtimes', $templates['body']['templates'][0]);
$this->assertContains('bun-1.0', array_column($templates['body']['templates'][0]['runtimes'], 'name'));
$templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 5,
'offset' => 2,
'useCases' => ['databases'],
'runtimes' => ['node-16.0']
]);
$this->assertEquals(200, $templates['headers']['status-code']);
$this->assertEquals(5, $templates['body']['total']);
$this->assertIsArray($templates['body']['templates']);
$this->assertArrayHasKey('runtimes', $templates['body']['templates'][0]);
foreach ($templates['body']['templates'] as $template) {
$this->assertContains($template['useCases'][0], ['databases']);
}
$this->assertContains('node-16.0', array_column($templates['body']['templates'][0]['runtimes'], 'name'));
/**
* Test for FAILURE
*/
$templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([
'content-type' => 'application/json',
], $this->getHeaders()), [
'limit' => 5001,
'offset' => 10,
]);
$this->assertEquals(400, $templates['headers']['status-code']);
$this->assertEquals('Invalid `limit` param: Value must be a valid range between 1 and 5,000', $templates['body']['message']);
$templates = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'limit' => 5,
'offset' => 5001,
]);
$this->assertEquals(400, $templates['headers']['status-code']);
$this->assertEquals('Invalid `offset` param: Value must be a valid range between 0 and 5,000', $templates['body']['message']);
}
public function testGetTemplate()
{
/**
* Test for SUCCESS
*/
$template = $this->client->call(Client::METHOD_GET, '/functions/templates/query-neo4j-auradb', array_merge([
'content-type' => 'application/json',
], $this->getHeaders()), []);
$this->assertEquals(200, $template['headers']['status-code']);
$this->assertIsArray($template['body']);
$this->assertEquals('query-neo4j-auradb', $template['body']['id']);
$this->assertEquals('Query Neo4j AuraDB', $template['body']['name']);
$this->assertEquals('icon-neo4j', $template['body']['icon']);
$this->assertEquals('Graph database with focus on relations between data.', $template['body']['tagline']);
$this->assertEquals(['databases'], $template['body']['useCases']);
$this->assertEquals('github', $template['body']['vcsProvider']);
/**
* Test for FAILURE
*/
$template = $this->client->call(Client::METHOD_GET, '/functions/templates/invalid-template-id', array_merge([
'content-type' => 'application/json',
], $this->getHeaders()), []);
$this->assertEquals(404, $template['headers']['status-code']);
$this->assertEquals('Function Template with the requested ID could not be found.', $template['body']['message']);
}
} }