diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9e3e6fcd81..8d8ddadda6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -497,6 +497,18 @@ If you are in PHP Storm you don't need any plugin. Below are the settings requir
2. If needed edit the **dev/xdebug.ini** file to your needs.
3. Launch your Appwrite instance while your debugger is listening for connections.
+## Profiling
+Appwrite uses XDebug [Profiler](https://xdebug.org/docs/profiler) for generating **CacheGrind** files. The generated file would be located in each of the `appwrite` containers inside the `/tmp/xdebug` folder.
+
+To disable the profiler while debugging remove the `,profiler` mode from the `xdebug.ini` file
+```diff
+zend_extension=xdebug
+
+[xdebug]
+-xdebug.mode=develop,debug,profile
++xdebug.mode=develop,debug
+```
+
### VS Code Launch Configuration
```json
diff --git a/Dockerfile b/Dockerfile
index 6f0fdcb7b7..fc501701ba 100755
--- a/Dockerfile
+++ b/Dockerfile
@@ -91,9 +91,10 @@ RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
# Enable Extensions
RUN if [ "$DEBUG" == "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi
+RUN if [ "$DEBUG" == "true" ]; then mkdir -p /tmp/xdebug; fi
RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi
RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xdebug.so; fi
EXPOSE 80
-CMD [ "php", "app/http.php" ]
\ No newline at end of file
+CMD [ "php", "app/http.php" ]
diff --git a/app/config/collections.php b/app/config/collections.php
index d0c3df165e..b8667d0b8d 100644
--- a/app/config/collections.php
+++ b/app/config/collections.php
@@ -3848,6 +3848,39 @@ $projectCollections = array_merge([
'array' => false,
'filters' => [],
],
+ [
+ '$id' => ID::custom('scheduledAt'),
+ 'type' => Database::VAR_DATETIME,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => false,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['datetime'],
+ ],
+ [
+ '$id' => ID::custom('scheduleInternalId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('scheduleId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
],
'indexes' => [
[
@@ -4751,8 +4784,8 @@ $consoleCollections = array_merge([
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
- 'required' => true,
- 'default' => null,
+ 'required' => false,
+ 'default' => [],
'array' => true,
'filters' => [],
],
diff --git a/app/config/errors.php b/app/config/errors.php
index 6337c3205a..dc6dcd5daf 100644
--- a/app/config/errors.php
+++ b/app/config/errors.php
@@ -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.',
'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 */
Exception::BUILD_NOT_FOUND => [
diff --git a/app/config/function-templates.php b/app/config/function-templates.php
new file mode 100644
index 0000000000..dddd8596f4
--- /dev/null
+++ b/app/config/function-templates.php
@@ -0,0 +1,2048 @@
+ [
+ 'name' => 'node',
+ 'versions' => ['21.0', '20.0', '19.0', '18.0', '16.0', '14.5']
+ ],
+ 'PHP' => [
+ 'name' => 'php',
+ 'versions' => ['8.3', '8.2', '8.1', '8.0']
+ ],
+ 'RUBY' => [
+ 'name' => 'ruby',
+ 'versions' => ['3.3', '3.2', '3.1', '3.0']
+ ],
+ 'PYTHON' => [
+ 'name' => 'python',
+ 'versions' => ['3.12', '3.11', '3.10', '3.9', '3.8']
+ ],
+ 'DART' => [
+ 'name' => 'dart',
+ 'versions' => ['3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16']
+ ],
+ 'BUN' => [
+ 'name' => 'bun',
+ 'versions' => ['1.0']
+ ],
+ 'GO' => [
+ 'name' => 'go',
+ 'versions' => ['1.22']
+ ]
+];
+
+function getRuntimes($runtime, $commands, $entrypoint, $providerRootDirectory, $versionsDenyList = [])
+{
+ return array_map(function ($version) use ($runtime, $commands, $entrypoint, $providerRootDirectory) {
+ return [
+ 'name' => $runtime['name'] . '-' . $version,
+ 'commands' => $commands,
+ 'entrypoint' => $entrypoint,
+ 'providerRootDirectory' => $providerRootDirectory
+ ];
+ }, array_filter($runtime['versions'], function ($version) use ($versionsDenyList) {
+ return !in_array($version, $versionsDenyList);
+ }));
+}
+
+return [
+ [
+ 'icon' => 'icon-lightning-bolt',
+ 'id' => 'starter',
+ 'name' => 'Starter function',
+ 'tagline' =>
+ 'A simple function to get started. Edit this function to explore endless possibilities with Appwrite Functions.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['starter'],
+ 'runtimes' => [
+ ...getRuntimes(TEMPLATE_RUNTIMES['NODE'], 'npm install', 'src/main.js', 'node/starter'),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PHP'],
+ 'composer install',
+ 'src/index.php',
+ 'php/starter'
+ ),
+ ...getRuntimes(TEMPLATE_RUNTIMES['RUBY'], 'bundle install', 'lib/main.rb', 'ruby/starter'),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PYTHON'],
+ 'pip install -r requirements.txt',
+ 'src/main.py',
+ 'python/starter'
+ ),
+ ...getRuntimes(TEMPLATE_RUNTIMES['DART'], 'dart pub get', 'lib/main.dart', 'dart/starter'),
+ ...getRuntimes(TEMPLATE_RUNTIMES['BUN'], 'bun install', 'src/main.ts', 'bun/starter'),
+ ...getRuntimes(TEMPLATE_RUNTIMES['GO'], '', 'main.go', 'go/starter')
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [],
+ 'scopes' => ["users.read"]
+ ],
+ [
+ 'icon' => 'icon-upstash',
+ 'id' => 'query-upstash-vector',
+ 'name' => 'Query Upstash Vector',
+ 'tagline' => 'Vector database that stores text embeddings and context retrieval for LLMs',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['databases'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/query-upstash-vector'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'UPSTASH_URL',
+ 'description' => 'The endpoint to connect to your Upstash Vector database. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'https://resolved-mallard-84564-eu1-vector.upstash.io',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'UPSTASH_TOKEN',
+ 'description' => 'Authentication token to access your Upstash Vector database. Learn more.',
+ 'value' => '',
+ 'placeholder' =>
+ 'oe4wNTbwHVLcDNa6oceZfhBEABsCNYh43ii6Xdq4bKBH7mq7qJkUmc4cs3ABbYyuVKWZTxVQjiNjYgydn2dkhABNes4NAuDpj7qxUAmZYqGJT78',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-redis',
+ 'id' => 'query-redis-labs',
+ 'name' => 'Query Redis Labs',
+ 'tagline' => 'Key-value database with advanced caching capabilities.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['databases'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/query-redis-labs'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'REDIS_HOST',
+ 'description' => 'The endpoint to connect to your Redis database. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'redis-13258.c35.eu-central-1-1.ec2.redns.redis-cloud.com',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'REDIS_PASSWORD',
+ 'description' => 'Authentication password to access your Redis database. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'efNNehiACfcZiwsTAjcK6xiwPyu6Dpdq',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-neo4j',
+ 'id' => 'query-neo4j-auradb',
+ 'name' => 'Query Neo4j AuraDB',
+ 'tagline' => 'Graph database with focus on relations between data.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['databases'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/query-neo4j-auradb'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'NEO4J_URI',
+ 'description' => 'The endpoint to connect to your Neo4j database. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'neo4j+s://4tg4mddo.databases.neo4j.io',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'NEO4J_USER',
+ 'description' => 'Authentication user to access your Neo4j database. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'neo4j',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'NEO4J_PASSWORD',
+ 'description' => 'Authentication password to access your Neo4j database. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'mCUc4PbVUQN-_NkTLJLisb6ccnwzQKKhrkF77YMctzx',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-mongodb',
+ 'id' => 'query-mongo-atlas',
+ 'name' => 'Query MongoDB Atlas',
+ 'tagline' =>
+ 'Realtime NoSQL document database with geospecial, graph, search, and vector suport.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['databases'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/query-mongo-atlas'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'MONGO_URI',
+ 'description' => 'The endpoint to connect to your Mongo database. Learn more.',
+ 'value' => '',
+ 'placeholder' =>
+ 'mongodb+srv://appwrite:Yx42hafg7Q4fgkxe@cluster0.7mslfog.mongodb.net/?retryWrites=true&w=majority&appName=Appwrite',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-neon',
+ 'id' => 'query-neon-postgres',
+ 'name' => 'Query Neon Postgres',
+ 'tagline' =>
+ 'Reliable SQL database with replication, point-in-time recovery, and pgvector support.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['databases'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/query-neon-postgres'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'PGHOST',
+ 'description' => 'The endpoint to connect to your Postgres database. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'ep-still-sea-a792sh84.eu-central-1.aws.neon.tech',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'PGDATABASE',
+ 'description' => 'Name of our Postgres database. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'main',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'PGUSER',
+ 'description' => 'Name of our Postgres user for authentication. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'main_owner',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'PGPASSWORD',
+ 'description' => 'Password of our Postgres user for authentication. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'iQCfaUaaWB3B',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'ENDPOINT_ID',
+ 'description' => 'Endpoint ID provided for your Postgres database. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'ep-still-sea-a792sh84',
+ 'required' => true,
+ 'type' => 'text'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-open-ai',
+ 'id' => 'prompt-chatgpt',
+ 'name' => 'Prompt ChatGPT',
+ 'tagline' => 'Ask questions and let OpenAI GPT-3.5-turbo answer.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/prompt-chatgpt'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PYTHON'],
+ 'pip install -r requirements.txt',
+ 'src/main.py',
+ 'python/prompt_chatgpt'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PHP'],
+ 'composer install',
+ 'src/index.php',
+ 'php/prompt-chatgpt'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['DART'],
+ 'dart pub get',
+ 'lib/main.dart',
+ 'dart/prompt_chatgpt'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'OPENAI_API_KEY',
+ 'description' => 'A unique key used to authenticate with the OpenAI API. This is a paid service and you will be charged for each request made to the API. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'sk-wzG...vcy',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'OPENAI_MAX_TOKENS',
+ 'description' => 'The maximum number of tokens that the OpenAI response should contain. Be aware that OpenAI models read and write a maximum number of tokens per API call, which varies depending on the model. For GPT-3.5-turbo, the limit is 4096 tokens. Learn more.',
+ 'value' => '512',
+ 'placeholder' => '512',
+ 'required' => false,
+ 'type' => 'number'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-discord',
+ 'id' => 'discord-command-bot',
+ 'name' => 'Discord Command Bot',
+ 'tagline' => 'Simple command using Discord Interactions.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['messaging'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install && npm run setup',
+ 'src/main.js',
+ 'node/discord-command-bot'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PYTHON'],
+ 'pip install -r requirements.txt && python src/setup.py',
+ 'src/main.py',
+ 'python/discord_command_bot'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['GO'],
+ '',
+ 'main.go',
+ 'go/discord-command-bot'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'DISCORD_PUBLIC_KEY',
+ 'description' => 'Public Key of your application in Discord Developer Portal. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'db9...980',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'DISCORD_APPLICATION_ID',
+ 'description' => 'ID of your application in Discord Developer Portal. Learn more.',
+ 'value' => '',
+ 'placeholder' => '427...169',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'DISCORD_TOKEN',
+ 'description' => 'Bot token of your application in Discord Developer Portal. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'NDI...LUfg',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-perspective-api',
+ 'id' => 'analyze-with-perspectiveapi',
+ 'name' => 'Analyze with PerspectiveAPI',
+ 'tagline' => 'Automate moderation by getting toxicity of messages.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/analyze-with-perspectiveapi'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'PERSPECTIVE_API_KEY',
+ 'description' => 'Google Perspective API key. It authenticates your function, allowing it to interact with the API. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'AIzaS...fk-fuM',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-pangea',
+ 'id' => 'censor-with-redact',
+ 'name' => 'Censor with Redact',
+ 'tagline' =>
+ 'Censor sensitive information from a provided text string using Redact API by Pangea.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/censor-with-redact'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PYTHON'],
+ 'pip install -r requirements.txt',
+ 'src/main.py',
+ 'python/censor_with_redact'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['DART'],
+ 'dart pub get',
+ 'lib/main.dart',
+ 'dart/censor_with_redact'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'PANGEA_REDACT_TOKEN',
+ 'description' => 'Access token for the Pangea Redact API. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'pts_7p4...5wl4',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-document',
+ 'id' => 'generate-pdf',
+ 'name' => 'Generate PDF',
+ 'tagline' => 'Document containing sample invoice in PDF format.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['utilities'],
+ 'runtimes' => [
+ ...getRuntimes(TEMPLATE_RUNTIMES['NODE'], 'npm install', 'src/main.js', 'node/generate-pdf')
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => []
+ ],
+ [
+ 'icon' => 'icon-github',
+ 'id' => 'github-issue-bot',
+ 'name' => 'GitHub issue bot',
+ 'tagline' =>
+ 'Automate the process of responding to newly opened issues in a GitHub repository.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['dev-tools'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/github-issue-bot'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'GITHUB_TOKEN',
+ 'description' => 'A personal access token from GitHub with the necessary permissions to post comments on issues. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'ghp_1...',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'GITHUB_WEBHOOK_SECRET',
+ 'description' => 'The secret used to verify that the webhook request comes from GitHub. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'd1efb...aec35',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-bookmark',
+ 'id' => 'url-shortener',
+ 'name' => 'URL shortener',
+ 'tagline' => 'Generate URL with short ID and redirect to the original URL when visited.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['utilities'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/url-shortener'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database to store the short URLs. Learn more.',
+ 'value' => 'urlShortener',
+ 'placeholder' => 'urlShortener',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection to store the short URLs. Learn more.',
+ 'value' => 'urls',
+ 'placeholder' => 'urls',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'SHORT_BASE_URL',
+ 'description' => 'The domain to use for the short URLs. You can use your functions subdomain or a custom domain.',
+ 'value' => '',
+ 'placeholder' => 'https://shortdomain.io',
+ 'required' => true,
+ 'type' => 'url'
+ ]
+ ],
+ 'scopes' => ["databases.read", "databases.write", "collections.write", "attributes.write", "documents.read", "documents.write"]
+ ],
+ [
+ 'icon' => 'icon-algolia',
+ 'id' => 'sync-with-algolia',
+ 'name' => 'Sync with Algolia',
+ 'tagline' => 'Intuitive search bar for any data in Appwrite Databases.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['databases'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/sync-with-algolia'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PYTHON'],
+ 'pip install -r requirements.txt',
+ 'src/main.py',
+ 'python/sync_with_algolia'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PHP'],
+ 'composer install',
+ 'src/index.php',
+ 'php/sync-with-algolia'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the Appwrite database that contains the collection to sync. Learn more.',
+ 'placeholder' => '64a55...7b912',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection in the Appwrite database to sync. Learn more.',
+ 'placeholder' => '7c3e8...2a9f1',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'ALGOLIA_APP_ID',
+ 'description' => 'The ID of the application in Algolia. Learn more.',
+ 'placeholder' => 'OFCNCOG2CU',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'ALGOLIA_ADMIN_API_KEY',
+ 'description' => 'The admin API Key for your Algolia service. Learn more.',
+ 'placeholder' => 'fd0aa...136a8',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'ALGOLIA_INDEX_ID',
+ 'description' => 'The ID of the index in Algolia where the documents are to be synced. Learn more.',
+ 'placeholder' => 'my_index',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'ALGOLIA_SEARCH_API_KEY',
+ 'description' => 'The search API Key for your Algolia service. This key is used for searching the synced index. Learn more.',
+ 'placeholder' => 'bf2f5...df733',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ ],
+ 'scopes' => ["databases.read", "collections.read", "documents.read"]
+ ],
+ [
+ 'icon' => 'icon-meilisearch',
+ 'id' => 'sync-with-meilisearch',
+ 'name' => 'Sync with Meilisearch',
+ 'tagline' => 'Intuitive search bar for any data in Appwrite Databases.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['databases'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/sync-with-meilisearch'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PYTHON'],
+ 'pip install -r requirements.txt',
+ 'src/main.py',
+ 'python/sync-with-meilisearch'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PHP'],
+ 'composer install',
+ 'src/index.php',
+ 'php/sync-with-meilisearch'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['BUN'],
+ 'bun install',
+ 'src/main.ts',
+ 'bun/sync-with-meilisearch'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['RUBY'],
+ 'bundle install',
+ 'lib/main.rb',
+ 'ruby/sync-with-meilisearch'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the Appwrite database that contains the collection to sync. Learn more.',
+ 'placeholder' => '64a55...7b912',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection in the Appwrite database to sync. Learn more.',
+ 'placeholder' => '7c3e8...2a9f1',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'MEILISEARCH_ENDPOINT',
+ 'description' => 'The host URL of the Meilisearch server. Learn more.',
+ 'placeholder' => 'http://127.0.0.1:7700',
+ 'required' => true,
+ 'type' => 'url'
+ ],
+ [
+ 'name' => 'MEILISEARCH_ADMIN_API_KEY',
+ 'description' => 'The admin API key for Meilisearch. Learn more.',
+ 'placeholder' => 'masterKey1234',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'MEILISEARCH_SEARCH_API_KEY',
+ 'description' => 'API Key for Meilisearch search operations. Learn more.',
+ 'placeholder' => 'searchKey1234',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'MEILISEARCH_INDEX_NAME',
+ 'description' => 'Name of the Meilisearch index to which the documents will be synchronized. Learn more.',
+ 'placeholder' => 'appwrite_index',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ ],
+ 'scopes' => ["databases.read", "collections.read", "documents.read"]
+ ],
+ [
+ 'icon' => 'icon-vonage',
+ 'id' => 'whatsapp-with-vonage',
+ 'name' => 'WhatsApp with Vonage',
+ 'tagline' => 'Simple bot to answer WhatsApp messages.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['messaging'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/whatsapp-with-vonage'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PYTHON'],
+ 'pip install -r requirements.txt',
+ 'src/main.py',
+ 'python/whatsapp_with_vonage'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PHP'],
+ 'composer install',
+ 'src/index.php',
+ 'php/whatsapp-with-vonage'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['DART'],
+ 'dart pub get',
+ 'lib/main.dart',
+ 'dart/whatsapp-with-vonage'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['RUBY'],
+ 'bundle install',
+ 'lib/main.rb',
+ 'ruby/whatsapp-with-vonage'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['BUN'],
+ 'bun install',
+ 'src/main.ts',
+ 'bun/whatsapp-with-vonage'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'VONAGE_API_KEY',
+ 'description' => 'API Key to use the Vonage API. Learn more.',
+ 'value' => '',
+ 'placeholder' => '62...97',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'VONAGE_API_SECRET',
+ 'description' => 'Secret to use the Vonage API. Learn more.',
+ 'placeholder' => 'Zjc...5PH',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'VONAGE_API_SIGNATURE_SECRET',
+ 'description' => 'Secret to verify the JWT token sent by Vonage. Learn more.',
+ 'placeholder' => 'NXOi3...IBHDa',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'VONAGE_WHATSAPP_NUMBER',
+ 'description' => 'Vonage WhatsApp number to send messages from. Learn more.',
+ 'placeholder' => '+14000000102',
+ 'required' => true,
+ 'type' => 'phone'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-bell',
+ 'id' => 'push-notification-with-fcm',
+ 'name' => 'Push notification with FCM',
+ 'tagline' => 'Send push notifications to your users using Firebase Cloud Messaging (FCM).',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['messaging'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/push-notification-with-fcm'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'FCM_PROJECT_ID',
+ 'description' => 'A unique identifier for your FCM project. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'mywebapp-f6e57',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'FCM_CLIENT_EMAIL',
+ 'description' => 'Your FCM service account email. Learn more.',
+ 'placeholder' => 'fcm-adminsdk-2f0de@test-f7q57.iam.gserviceaccount.com',
+ 'required' => true,
+ 'type' => 'email'
+ ],
+ [
+ 'name' => 'FCM_PRIVATE_KEY',
+ 'description' => 'A unique private key used to authenticate with FCM. Learn more.',
+ 'placeholder' => '0b683...75675',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'FCM_DATABASE_URL',
+ 'description' => 'URL of your FCM database. Learn more.',
+ 'placeholder' => 'https://my-app-f298e.firebaseio.com',
+ 'required' => true,
+ 'type' => 'url'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-mail',
+ 'id' => 'email-contact-form',
+ 'name' => 'Email contact form',
+ 'tagline' => 'Sends an email with the contents of a HTML form.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['utilities'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/email-contact-form'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PYTHON'],
+ 'pip install -r requirements.txt',
+ 'src/main.py',
+ 'python/email_contact_form'
+ ),
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['PHP'],
+ 'composer install',
+ 'src/index.php',
+ 'php/email-contact-form'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'SMTP_HOST',
+ 'description' => 'The address of your SMTP server. Many STMP providers will provide this information in their documentation. Some popular providers include: Mailgun, SendGrid, and Gmail.',
+ 'value' => '',
+ 'placeholder' => 'smtp.mailgun.org',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'SMTP_PORT',
+ 'description' => 'The port of your STMP server. Commnly used ports include 25, 465, and 587.',
+ 'placeholder' => '25',
+ 'required' => true,
+ 'type' => 'number'
+ ],
+ [
+ 'name' => 'SMTP_USERNAME',
+ 'description' => 'The username for your SMTP server. This is commonly your email address.',
+ 'placeholder' => 'no-reply@mywebapp.org',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'SMTP_PASSWORD',
+ 'description' => 'The password for your SMTP server.',
+ 'placeholder' => '5up3r5tr0ngP4ssw0rd',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'SUBMIT_EMAIL',
+ 'description' => 'The email address to send form submissions to.',
+ 'placeholder' => 'me@mywebapp.org',
+ 'required' => true,
+ 'type' => 'email'
+ ],
+ [
+ 'name' => 'ALLOWED_ORIGINS',
+ 'description' => 'An optional comma-separated list of allowed origins for CORS (defaults to *). This is an important security measure to prevent malicious users from abusing your function.',
+ 'value' => '',
+ 'placeholder' => 'https://mywebapp.org,https://mywebapp.com',
+ 'required' => false,
+ 'type' => 'text'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-stripe',
+ 'id' => 'subscriptions-with-stripe',
+ 'name' => 'Subscriptions with Stripe',
+ 'tagline' => 'Receive recurring card payments and grant subscribers extra permissions.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['utilities'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/subscriptions-with-stripe'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'STRIPE_SECRET_KEY',
+ 'description' => 'Secret for sending requests to the Stripe API. Learn more.',
+ 'placeholder' => 'sk_test_51J...',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'STRIPE_WEBHOOK_SECRET',
+ 'description' => 'Secret used to validate the Stripe Webhook signature. Learn more.',
+ 'placeholder' => 'whsec_...',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ],
+ 'scopes' => ["users.read", "sessions.write", "users.write"]
+ ],
+ [
+ 'icon' => 'icon-stripe',
+ 'id' => 'payments-with-stripe',
+ 'name' => 'Payments with Stripe',
+ 'tagline' => 'Receive card payments and store paid orders.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['utilities'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/payments-with-stripe'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'STRIPE_SECRET_KEY',
+ 'description' => 'Secret for sending requests to the Stripe API. Learn more.',
+ 'placeholder' => 'sk_test_51J...',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'STRIPE_WEBHOOK_SECRET',
+ 'description' => 'Secret used to validate the Stripe Webhook signature. Learn more.',
+ 'placeholder' => 'whsec_...',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database to store paid orders. Learn more.',
+ 'value' => 'orders',
+ 'placeholder' => 'orders',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection to store paid orders. Learn more.',
+ 'value' => 'orders',
+ 'placeholder' => 'orders',
+ 'required' => false,
+ 'type' => 'text'
+ ]
+ ],
+ 'scopes' => ["databases.read", "databases.write", "collections.write", "attributes.write", "documents.read", "documents.write"]
+ ],
+ [
+ 'icon' => 'icon-chat',
+ 'id' => 'text-generation-with-huggingface',
+ 'name' => 'Text generation',
+ 'tagline' => 'Generate text using the Hugging Face inference API.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 30,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/text-generation-with-huggingface'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'HUGGINGFACE_ACCESS_TOKEN',
+ 'description' => 'Secret for sending requests to the Hugging Face API. Learn more.',
+ 'placeholder' => 'hf_MUvn...',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-translate',
+ 'id' => 'language-translation-with-huggingface',
+ 'name' => 'Language translation',
+ 'tagline' => 'Translate text using the Hugging Face inference API.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 30,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/language-translation-with-huggingface'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'HUGGINGFACE_ACCESS_TOKEN',
+ 'description' => 'Secret for sending requests to the Hugging Face API. Learn more.',
+ 'placeholder' => 'hf_MUvn...',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-eye',
+ 'id' => 'image-classification-with-huggingface',
+ 'name' => 'Image classification',
+ 'tagline' => 'Classify images using the Hugging Face inference API.',
+ 'permissions' => ['any'],
+ 'events' => ['buckets.*.files.*.create'],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install && npm run setup',
+ 'src/main.js',
+ 'node/image-classification-with-huggingface'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database where the responses are stored. Learn more.',
+ 'value' => 'ai',
+ 'placeholder' => 'ai',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection where the responses are stored. Learn more.',
+ 'value' => 'image_classification',
+ 'placeholder' => 'image_classification',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_BUCKET_ID',
+ 'description' => 'The ID of the bucket where the images are stored. Learn more.',
+ 'value' => 'image_classification',
+ 'placeholder' => 'image_classification',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'HUGGINGFACE_ACCESS_TOKEN',
+ 'description' => 'Secret for sending requests to the Hugging Face API. Learn more.',
+ 'placeholder' => 'hf_MUvn...',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ],
+ 'scopes' => ["databases.read", "databases.write", "collections.read", "collections.write", "attributes.write", "documents.read", "documents.write", "buckets.read", "buckets.write", "files.read"]
+ ],
+ [
+ 'icon' => 'icon-eye',
+ 'id' => 'object-detection-with-huggingface',
+ 'name' => 'Object detection',
+ 'tagline' => 'Detect objects in images using the Hugging Face inference API.',
+ 'permissions' => ['any'],
+ 'events' => ['buckets.*.files.*.create'],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install && npm run setup',
+ 'src/main.js',
+ 'node/object-detection-with-huggingface'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database where the responses are stored. Learn more.',
+ 'value' => 'ai',
+ 'placeholder' => 'ai',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection where the responses are stored. Learn more.',
+ 'value' => 'object_detection',
+ 'placeholder' => 'object_detection',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_BUCKET_ID',
+ 'description' => 'The ID of the bucket where the images are stored. Learn more.',
+ 'value' => 'object_detection',
+ 'placeholder' => 'object_detection',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'HUGGINGFACE_ACCESS_TOKEN',
+ 'description' => 'Secret for sending requests to the Hugging Face API. Learn more.',
+ 'placeholder' => 'hf_MUvn...',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ],
+ "scopes" => ["databases.read", "databases.write", "collections.read", "collections.write", "attributes.write", "documents.read", "documents.write", "buckets.read", "buckets.write", "files.read"]
+ ],
+ [
+ 'icon' => 'icon-text',
+ 'id' => 'speech-recognition-with-huggingface',
+ 'name' => 'Speech recognition',
+ 'tagline' => 'Transcribe audio to text using the Hugging Face inference API.',
+ 'permissions' => ['any'],
+ 'events' => ['buckets.*.files.*.create'],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install && npm run setup',
+ 'src/main.js',
+ 'node/speech-recognition-with-huggingface'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database where the responses are stored. Learn more.',
+ 'value' => 'ai',
+ 'placeholder' => 'ai',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection where the responses are stored. Learn more.',
+ 'value' => 'speech_recognition',
+ 'placeholder' => 'speech_recognition',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_BUCKET_ID',
+ 'description' => 'The ID of the bucket where audio is stored. Learn more.',
+ 'value' => 'speech_recognition',
+ 'placeholder' => 'speech_recognition',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'HUGGINGFACE_ACCESS_TOKEN',
+ 'description' => 'Secret for sending requests to the Hugging Face API. Learn more.',
+ 'placeholder' => 'hf_MUvn...',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ],
+ "scopes" => ["databases.read", "databases.write", "collections.read", "collections.write", "attributes.write", "documents.read", "documents.write", "buckets.read", "buckets.write", "files.read"]
+ ],
+ [
+ 'icon' => 'icon-chat',
+ 'id' => 'text-to-speech-with-huggingface',
+ 'name' => 'Text to speech',
+ 'tagline' => 'Convert text to speech using the Hugging Face inference API.',
+ 'permissions' => ['any'],
+ 'events' => ['databases.*.collections.*.documents.*.create'],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install && npm run setup',
+ 'src/main.js',
+ 'node/text-to-speech-with-huggingface'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database where the responses are stored. Learn more.',
+ 'value' => 'ai',
+ 'placeholder' => 'ai',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection where the responses are stored. Learn more.',
+ 'value' => 'speech_recognition',
+ 'placeholder' => 'speech_recognition',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_BUCKET_ID',
+ 'description' => 'The ID of the bucket where audio is stored. Learn more.',
+ 'value' => 'speech_recognition',
+ 'placeholder' => 'speech_recognition',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'HUGGINGFACE_ACCESS_TOKEN',
+ 'description' => 'Secret for sending requests to the Hugging Face API. Learn more.',
+ 'placeholder' => 'hf_MUvn...',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ],
+ "scopes" => ["buckets.read", "buckets.write", "files.read", "files.write"]
+ ],
+ [
+ 'icon' => 'icon-chip',
+ 'id' => 'generate-with-replicate',
+ 'name' => 'Generate with Replicate',
+ 'tagline' => "Generate text, audio and images using Replicate's API.",
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 300,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/generate-with-replicate'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'REPLICATE_API_KEY',
+ 'description' => 'A unique key used to authenticate with the Replicate API. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'd1efb...aec35',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-chip',
+ 'id' => 'generate-with-together-ai',
+ 'name' => 'Generate with Together AI',
+ 'tagline' => "Generate text and images using Together AI's API.",
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 300,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/generate-with-together-ai'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'TOGETHER_API_KEY',
+ 'description' => 'A unique key used to authenticate with the Together AI API. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'd1efb...aec35',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'APPWRITE_BUCKET_ID',
+ 'description' => 'The ID of the bucket where audio is stored. Learn more.',
+ 'placeholder' => 'generated_speech',
+ 'required' => true,
+ 'type' => 'text'
+ ]
+ ],
+ "scopes" => ["buckets.write", "files.read", "files.write"]
+ ],
+ [
+ 'icon' => 'icon-chip',
+ 'id' => 'chat-with-perplexity-ai',
+ 'name' => 'Chat with Perplexity AI',
+ 'tagline' => 'Create a chatbot using the Perplexity AI API.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/chat-with-perplexity-ai'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'PERPLEXITY_API_KEY',
+ 'description' => 'A unique key used to authenticate with the Perplexity API. Learn more.',
+ 'placeholder' => 'pplex-68...999',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'PERPLEXITY_MAX_TOKENS',
+ 'description' => 'The maximum number of tokens to generate. Learn more.',
+ 'placeholder' => '512',
+ 'required' => false,
+ 'type' => 'number'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-chip',
+ 'id' => 'generate-with-replicate',
+ 'name' => 'Generate with Replicate',
+ 'tagline' => "Generate text, audio and images using Replicate's API.",
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 300,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/generate-with-replicate'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'REPLICATE_API_KEY',
+ 'description' => 'A unique key used to authenticate with the Replicate API. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'd1efb...aec35',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-document-search',
+ 'id' => 'sync-with-pinecone',
+ 'name' => 'Sync with Pinecone',
+ 'tagline' => "Sync your Appwrite database with Pinecone's vector database.",
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 30,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/sync-with-pinecone'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'OPENAI_API_KEY',
+ 'description' => 'A unique key used to authenticate with the OpenAI API. This is a paid service and you will be charged for each request made to the API. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'sk-wzG...vcy',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'PINECONE_API_KEY',
+ 'description' => 'A unique key used to authenticate with the Pinecone API. Learn more.',
+ 'placeholder' => 'd1efb...aec35',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'PINECONE_INDEX_NAME',
+ 'description' => 'The name of the index in Pinecone. Learn more.',
+ 'placeholder' => 'my-index',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database where the documents are stored. Learn more.',
+ 'placeholder' => 'my-database',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection where the documents are stored. Learn more.',
+ 'placeholder' => 'my-collection',
+ 'required' => true,
+ 'type' => 'text'
+ ]
+ ],
+ "scopes" => ["databases.read", "collections.read", "documents.read"]
+ ],
+ [
+ 'icon' => 'icon-chip',
+ 'id' => 'rag-with-langchain',
+ 'name' => 'RAG with LangChain',
+ 'tagline' => 'Generate text using a LangChain RAG model',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 30,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/rag-with-langchain'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'OPENAI_API_KEY',
+ 'description' => 'A unique key used to authenticate with the OpenAI API. This is a paid service and you will be charged for each request made to the API. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'sk-wzG...vcy',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'PINECONE_API_KEY',
+ 'description' => 'A unique key used to authenticate with the Pinecone API. Learn more.',
+ 'placeholder' => 'd1efb...aec35',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'PINECONE_INDEX_NAME',
+ 'description' => 'The name of the index in Pinecone. Learn more.',
+ 'placeholder' => 'my-index',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database where the documents are stored. Learn more.',
+ 'placeholder' => 'my-database',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection where the documents are stored. Learn more.',
+ 'placeholder' => 'my-collection',
+ 'required' => true,
+ 'type' => 'text'
+ ]
+ ],
+ "scopes" => ["databases.read", "collections.read", "documents.read"]
+ ],
+ [
+ 'icon' => 'icon-chat',
+ 'id' => 'speak-with-elevenlabs',
+ 'name' => 'Speak with ElevenLabs',
+ 'tagline' => 'Convert text to speech using the ElevenLabs API.',
+ 'permissions' => ['any'],
+ 'cron' => '',
+ 'events' => [],
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/speak-with-elevenlabs'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'ELEVENLABS_API_KEY',
+ 'description' => 'A unique key used to authenticate with the ElevenLabs API. Learn more.',
+ 'placeholder' => 'd03xxxxxxxx26',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database where the responses are stored. Learn more.',
+ 'placeholder' => 'my-database',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection where the responses are stored. Learn more.',
+ 'placeholder' => 'my-collection',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_BUCKET_ID',
+ 'description' => 'The ID of the bucket where audio is stored. Learn more.',
+ 'placeholder' => 'generated_speech',
+ 'required' => true,
+ 'type' => 'text'
+ ]
+ ],
+ "scopes" => ["buckets.read", "buckets.write", "files.read", "files.write"]
+ ],
+ [
+ 'icon' => 'icon-chip',
+ 'id' => 'speak-with-lmnt',
+ 'name' => 'Speak with LMNT',
+ 'tagline' => 'Convert text to speech using the LMNT API.',
+ 'permissions' => ['any'],
+ 'cron' => '',
+ 'events' => [],
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/speak-with-lmnt'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'LMNT_API_KEY',
+ 'description' => 'A unique key used to authenticate with the LMNT API. Learn more.',
+ 'placeholder' => 'd03xxxxxxxx26',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'APPWRITE_BUCKET_ID',
+ 'description' => 'The ID of the bucket where audio is stored. Learn more.',
+ 'placeholder' => 'generated_speech',
+ 'required' => true,
+ 'type' => 'text'
+ ]
+ ],
+ "scopes" => ["buckets.read", "buckets.write", "files.read", "files.write"]
+ ],
+ [
+ 'icon' => 'icon-chip',
+ 'id' => 'chat-with-anyscale',
+ 'name' => 'Chat with AnyScale',
+ 'tagline' => 'Create a chatbot using the AnyScale API.',
+ 'permissions' => ['any'],
+ 'cron' => '',
+ 'events' => [],
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/chat-with-anyscale'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'ANYSCALE_API_KEY',
+ 'description' => 'A unique key used to authenticate with the AnyScale API. Learn more.',
+ 'placeholder' => 'd03xxxxxxxx26',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'ANYSCALE_MAX_TOKENS',
+ 'description' => 'The maximum number of tokens that Anyscale responses should contain. Learn more.',
+ 'placeholder' => '',
+ 'required' => false,
+ 'type' => 'number'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-music-note',
+ 'id' => 'music-generation-with-huggingface',
+ 'name' => 'Music generation',
+ 'tagline' => 'Generate music from a text prompt using the Hugging Face inference API.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install && npm run setup',
+ 'src/main.js',
+ 'node/music-generation-with-huggingface'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'APPWRITE_BUCKET_ID',
+ 'description' => 'The ID of the bucket where generated music is stored. Learn more.',
+ 'value' => 'generated_music',
+ 'placeholder' => 'generated_music',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'HUGGINGFACE_ACCESS_TOKEN',
+ 'description' => 'Secret for sending requests to the Hugging Face API. Learn more.',
+ 'placeholder' => 'hf_MUvn...',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ],
+ "scopes" => ["buckets.read", "buckets.write", "files.read", "files.write"]
+ ],
+ [
+ 'icon' => 'icon-chip',
+ 'id' => 'generate-with-fal-ai',
+ 'name' => 'Generate with fal.ai',
+ 'tagline' => "Generate images using fal.ai's API.",
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 300,
+ 'useCases' => ['ai'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/generate-with-fal-ai'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'FAL_API_KEY',
+ 'description' => 'A unique key used to authenticate with the fal.ai API. Learn more.',
+ 'value' => '',
+ 'placeholder' => 'd1efb...aec35',
+ 'required' => true,
+ 'type' => 'password'
+ ]
+ ]
+ ],
+ [
+ 'icon' => 'icon-currency-dollar',
+ 'id' => 'subscriptions-with-lemon-squeezy',
+ 'name' => 'Subscriptions with Lemon Squeezy',
+ 'tagline' => 'Receive recurring card payments and grant subscribers extra permissions.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['utilities'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/subscriptions-with-lemon-squeezy'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'LEMON_SQUEEZY_API_KEY',
+ 'description' => 'API key for sending requests to the Lemon Squeezy API. Learn more.',
+ 'placeholder' => 'eyJ0eXAiOiJ...',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'LEMON_SQUEEZY_WEBHOOK_SECRET',
+ 'description' => 'Secret used to validate the Lemon Squuezy Webhook signature. Learn more.',
+ 'placeholder' => 'abcd...',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'LEMON_SQUEEZY_STORE_ID',
+ 'description' => 'Store ID required to create a checkout using the Lemon Squeezy API. Learn more.',
+ 'placeholder' => '123456',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'LEMON_SQUEEZY_VARIANT_ID',
+ 'description' => 'Variant ID of a product required to create a checkout using the Lemon Squeezy API. Learn more.',
+ 'placeholder' => 'abcd...',
+ 'required' => true,
+ 'type' => 'text'
+ ]
+ ],
+ "scopes" => ["users.read", "users.write"]
+ ],
+ [
+ 'icon' => 'icon-currency-dollar',
+ 'id' => 'payments-with-lemon-squeezy',
+ 'name' => 'Payments with Lemon Squeezy',
+ 'tagline' => 'Receive card payments and store paid orders.',
+ 'permissions' => ['any'],
+ 'events' => [],
+ 'cron' => '',
+ 'timeout' => 15,
+ 'useCases' => ['utilities'],
+ 'runtimes' => [
+ ...getRuntimes(
+ TEMPLATE_RUNTIMES['NODE'],
+ 'npm install',
+ 'src/main.js',
+ 'node/payments-with-lemon-squeezy'
+ )
+ ],
+ 'instructions' => 'For documentation and instructions check out file.',
+ 'vcsProvider' => 'github',
+ 'providerRepositoryId' => 'templates',
+ 'providerOwner' => 'appwrite',
+ 'providerBranch' => 'main',
+ 'variables' => [
+ [
+ 'name' => 'APPWRITE_DATABASE_ID',
+ 'description' => 'The ID of the database to store paid orders. Learn more.',
+ 'value' => 'orders',
+ 'placeholder' => 'orders',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'APPWRITE_COLLECTION_ID',
+ 'description' => 'The ID of the collection to store paid orders. Learn more.',
+ 'value' => 'orders',
+ 'placeholder' => 'orders',
+ 'required' => false,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'LEMON_SQUEEZY_API_KEY',
+ 'description' => 'API key for sending requests to the Lemon Squeezy API. Learn more.',
+ 'placeholder' => 'eyJ0eXAiOiJ...',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'LEMON_SQUEEZY_WEBHOOK_SECRET',
+ 'description' => 'Secret used to validate the Lemon Squuezy Webhook signature. Learn more.',
+ 'placeholder' => 'abcd...',
+ 'required' => true,
+ 'type' => 'password'
+ ],
+ [
+ 'name' => 'LEMON_SQUEEZY_STORE_ID',
+ 'description' => 'Store ID required to create a checkout using the Lemon Squeezy API. Learn more.',
+ 'placeholder' => '123456',
+ 'required' => true,
+ 'type' => 'text'
+ ],
+ [
+ 'name' => 'LEMON_SQUEEZY_VARIANT_ID',
+ 'description' => 'Variant ID of a product required to create a checkout using the Lemon Squeezy API. Learn more.',
+ 'placeholder' => 'abcd...',
+ 'required' => true,
+ 'type' => 'text'
+ ]
+ ],
+ "scopes" => ["users.read", "users.write"]
+ ]
+];
diff --git a/app/config/locale/translations/ar-ma.json b/app/config/locale/translations/ar-ma.json
new file mode 100644
index 0000000000..453de25c80
--- /dev/null
+++ b/app/config/locale/translations/ar-ma.json
@@ -0,0 +1,238 @@
+{
+ "settings.inspire": "\"الفن ديال الحكمة هو الفن ديال أنك تعرف أش تنخّل.\"",
+ "settings.locale": "ar-ma",
+ "settings.direction": "rtl",
+ "emails.sender": "فرقة %s",
+ "emails.verification.subject": "التيْقان ديال الحساب",
+ "emails.verification.hello": "السلام {{user}}",
+ "emails.verification.body": "تبّع هاد الوصلة باش تيقّن لادريسة تاع ليميل ديالك.",
+ "emails.verification.footer": "إلا ماشي نتا اللي طلبتي تيقّن هاد لادريسة تاع ليميل، ممكن تنخّل هاد البرية.",
+ "emails.verification.thanks": "شكرا",
+ "emails.verification.signature": "فرقة {{project}}",
+ "emails.magicSession.subject": "تكونيكطا",
+ "emails.magicSession.hello": "السلام,",
+ "emails.magicSession.body": "تبّع هاد الوصلة باش تتكونيكطا.",
+ "emails.magicSession.footer": "إلا ماشي نتا اللي طلبتي تتكونيكطا بهاد ليميل، ممكن تنخّل هاد البرية.",
+ "emails.magicSession.thanks": "شكرا",
+ "emails.magicSession.signature": "فرقة {{project}}",
+ "emails.recovery.subject": "تبدال كلمة السر",
+ "emails.recovery.hello": "السلام {{user}}",
+ "emails.recovery.body": "تبّع هاد الوصلة باش تبدّل كلمة السر تاع {{project}}.",
+ "emails.recovery.footer": "إلا ماشي نتا اللي طلبتي تبدّل كلمة السر، ممكن تنخّل هاد البرية.",
+ "emails.recovery.thanks": "شكرا",
+ "emails.recovery.signature": "فرقة {{project}}",
+ "emails.invitation.subject": "عراضة ل فرقة %s ف %s",
+ "emails.invitation.hello": "السلام",
+ "emails.invitation.body": "هاد البرية تصيفطات ليك حيت {{owner}} بغى يعرض عليك تولّي عضو ف فرقة {{team}} عند {{project}}.",
+ "emails.invitation.footer": "إلا كنتي ما مسوّقش, ممكن تنخّل هاد البرية.",
+ "emails.invitation.thanks": "شكرا",
+ "emails.invitation.signature": "فرقة {{project}}",
+ "emails.certificate.subject": "السرتافيكة فشلات ل %s",
+ "emails.certificate.hello": "السلام",
+ "emails.certificate.body": "السرتافيكة ديال الضومين ديالك '{{domain}}' ما قدّاتش تجينيرا. هادي هي المحاولة نمرة {{attempt}}, السبب ديال هاد الفشل هو: {{error}}",
+ "emails.certificate.footer": "السرتافيكة الفايتة ديالك غاتبقى مزيانة لمدة 30 يوم من عند أول فشل. كانشجعوك بزاف أنك تبقشش فهاد الموضوع, وا إلّا الضومين ديالك ما غايبقاش خدّام فيه الـ SSL.",
+ "emails.certificate.thanks": "شكرا",
+ "emails.certificate.signature": "فرقة {{project}}",
+ "locale.country.unknown": "ما معروفش",
+ "countries.af": "أفغانستان",
+ "countries.ao": "أنڭولا",
+ "countries.al": "ألبانيا",
+ "countries.ad": "أندورا",
+ "countries.ae": "الإمارات العربية المتّاحدة",
+ "countries.ar": "الأرجنتين",
+ "countries.am": "أرمينيا",
+ "countries.ag": "أنتيڭوا وبربودا",
+ "countries.au": "ؤسطراليا",
+ "countries.at": "النامسا",
+ "countries.az": "أديربيجان",
+ "countries.bi": "بوروندي",
+ "countries.be": "بلجيكا",
+ "countries.bj": "بينين",
+ "countries.bf": "بوركينا فاصو",
+ "countries.bd": "بنڭلاديش",
+ "countries.bg": "بلڭاريا",
+ "countries.bh": "البحرين",
+ "countries.bs": "دزيرات البهاما",
+ "countries.ba": "البوسنة ؤ الهرسك",
+ "countries.by": "بيلاروسيا",
+ "countries.bz": "بيليز",
+ "countries.bo": "بوليڤيا",
+ "countries.br": "البرازيل",
+ "countries.bb": "باربادوس",
+ "countries.bn": "بروناي",
+ "countries.bt": "بوتان",
+ "countries.bw": "بوتسوانا",
+ "countries.cf": "جمهورية إفريقيا الوسطانية",
+ "countries.ca": "كانادا",
+ "countries.ch": "سويسرا",
+ "countries.cl": "تشيلي",
+ "countries.cn": "الشينوا",
+ "countries.ci": "ساحل العاج",
+ "countries.cm": "الكاميرون",
+ "countries.cd": "جمهورية الكونڭو الديمقراطية",
+ "countries.cg": "جمهورية الكونڭو",
+ "countries.co": "كولومبيا",
+ "countries.km": "دزيرات القومور",
+ "countries.cv": "الراس الخضر",
+ "countries.cr": "كوسطاريكا",
+ "countries.cu": "كوبا",
+ "countries.cy": "قوبروص",
+ "countries.cz": "التشيك",
+ "countries.de": "ألمانيا",
+ "countries.dj": "دجيبوتي",
+ "countries.dm": "ضومينيكا",
+ "countries.dk": "الدنمارك",
+ "countries.do": "جمهورية الضومينيكان",
+ "countries.dz": "الدزاير",
+ "countries.ec": "إكوادور",
+ "countries.eg": "مصر",
+ "countries.er": "إريتريا",
+ "countries.es": "سبانيا",
+ "countries.ee": "إسطونيا",
+ "countries.et": "إتيوپيا",
+ "countries.fi": "فينلاندا",
+ "countries.fj": "فيدجي",
+ "countries.fr": "فرانسا",
+ "countries.fm": "ميكرونيزيا",
+ "countries.ga": "الڭابون",
+ "countries.gb": "المملكة المتّاحدة",
+ "countries.ge": "تجورجيا",
+ "countries.gh": "غانا",
+ "countries.gn": "غينيا",
+ "countries.gm": "ڭامبيا",
+ "countries.gw": "غينيا بيساو",
+ "countries.gq": "غينيا الستوائية",
+ "countries.gr": "اليونان",
+ "countries.gd": "ڭرينادا",
+ "countries.gt": "ڭواتيمالا",
+ "countries.gy": "ڭيانا",
+ "countries.hn": "هوندوراس",
+ "countries.hr": "كرواتيا",
+ "countries.ht": "هايتي",
+ "countries.hu": "الماجر",
+ "countries.id": "إندونيسيا",
+ "countries.in": "الهند",
+ "countries.ie": "إرلاندا",
+ "countries.ir": "إران",
+ "countries.iq": "العراق",
+ "countries.is": "إسلاندا",
+ "countries.il": "إسرائيل",
+ "countries.it": "الطاليان",
+ "countries.jm": "جامايكا",
+ "countries.jo": "الأردن",
+ "countries.jp": "الجاپون",
+ "countries.kz": "كازاخستان",
+ "countries.ke": "كينيا",
+ "countries.kg": "قيرغيزستان",
+ "countries.kh": "كمبوديا",
+ "countries.ki": "كيريباتي",
+ "countries.kn": "سانت كيتس ؤ نيفيس",
+ "countries.kr": "كوريا الجنوبية",
+ "countries.kw": "الكويت",
+ "countries.la": "لاوس",
+ "countries.lb": "لبنان",
+ "countries.lr": "ليبيريا",
+ "countries.ly": "ليبيا",
+ "countries.lc": "سانت لوسيا",
+ "countries.li": "ليختنشتاين",
+ "countries.lk": "سري لانكا",
+ "countries.ls": "ليسوتو",
+ "countries.lt": "ليتوانيا",
+ "countries.lu": "لوكسمبورڭ",
+ "countries.lv": "لاتفيا",
+ "countries.ma": "المغريب",
+ "countries.mc": "موناكو",
+ "countries.md": "مولضوڤا",
+ "countries.mg": "ماداغشقار",
+ "countries.mv": "دزيرات المالديڤ",
+ "countries.mx": "الميكسيك",
+ "countries.mh": "دزيرات مارشال",
+ "countries.mk": "مقدونيا",
+ "countries.ml": "مالي",
+ "countries.mt": "مالطا",
+ "countries.mm": "ميانمار",
+ "countries.me": "مونطينيڭرو",
+ "countries.mn": "منغوليا",
+ "countries.mz": "الموزمبيق",
+ "countries.mr": "موريتانيا",
+ "countries.mu": "موريشيوس",
+ "countries.mw": "مالاوي",
+ "countries.my": "ماليزيا",
+ "countries.na": "ناميبيا",
+ "countries.ne": "النيجر",
+ "countries.ng": "نيجيريا",
+ "countries.ni": "نيكاراڭوا",
+ "countries.nl": "هولاندا",
+ "countries.no": "النرويج",
+ "countries.np": "نيپال",
+ "countries.nr": "ناورو",
+ "countries.nz": "نيوزيلاندا",
+ "countries.om": "عمّان",
+ "countries.pk": "پاكيستان",
+ "countries.pa": "پاناما",
+ "countries.pe": "الپيرو",
+ "countries.ph": "الفيليپين",
+ "countries.pw": "پالاو",
+ "countries.pg": "پاپوا غينيا الجديدة",
+ "countries.pl": "پولاندا",
+ "countries.kp": "كوريا الشمالية",
+ "countries.pt": "البرطقيز",
+ "countries.py": "الپاراڭواي",
+ "countries.qa": "قطر",
+ "countries.ro": "رومانيا",
+ "countries.ru": "روسيا",
+ "countries.rw": "روّاندا",
+ "countries.sa": "المملكة العربية السعودية",
+ "countries.sd": "السودان",
+ "countries.sn": "السينيڭال",
+ "countries.sg": "سنغافورة",
+ "countries.sb": "دزيرات سليمان",
+ "countries.sl": "صييراليون",
+ "countries.sv": "السالڤاضور",
+ "countries.sm": "سان مارينو",
+ "countries.so": "الصومال",
+ "countries.rs": "صيربيا",
+ "countries.ss": "جنوب السودان",
+ "countries.st": "صاو طومي ؤ پرينسيپي",
+ "countries.sr": "سورينام",
+ "countries.sk": "صلوڤاكيا",
+ "countries.si": "صلوڤينيا",
+ "countries.se": "السويد",
+ "countries.sz": "سوازيلاند",
+ "countries.sc": "السيشيل",
+ "countries.sy": "سوريا",
+ "countries.td": "تشاد",
+ "countries.tg": "الطوڭو",
+ "countries.th": "الطايلوند",
+ "countries.tj": "طادجيكيستان",
+ "countries.tm": "تركمانيستان",
+ "countries.tl": "تيمور الشرقية",
+ "countries.to": "تونڭا",
+ "countries.tt": "ترينيداد ؤ طوباڭو",
+ "countries.tn": "تونس",
+ "countries.tr": "توركيا",
+ "countries.tv": "توڤالو",
+ "countries.tz": "طنزانيا",
+ "countries.ug": "ؤڭاندا",
+ "countries.ua": "ؤكرانيا",
+ "countries.uy": "ؤروڭواي",
+ "countries.us": "ميريكان",
+ "countries.uz": "ؤزباكيستان",
+ "countries.va": "مدينة الڤاتيكان",
+ "countries.vc": "سانت ڤانسون ؤ دزيرات ڭرينادين",
+ "countries.ve": "ڤينيزويلا",
+ "countries.vn": "ڤيطنام",
+ "countries.vu": "ڤانواتو",
+ "countries.ws": "ساموا",
+ "countries.ye": "اليمن",
+ "countries.za": "جنوب إفريقيا",
+ "countries.zm": "زامبيا",
+ "countries.zw": "زيمبابوي",
+ "continents.af": "أفريقيا",
+ "continents.an": "القارة القطبية الجنوبية",
+ "continents.as": "أسيا",
+ "continents.eu": "ؤروپا",
+ "continents.na": "ميريكان الشمالية",
+ "continents.oc": "ؤقيانوسيا",
+ "continents.sa": "ميريكان الجنوبية"
+}
diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php
index d2a1790d94..a61db99bdf 100644
--- a/app/controllers/api/functions.php
+++ b/app/controllers/api/functions.php
@@ -1744,9 +1744,8 @@ App::post('/v1/functions/:functionId/executions')
->setContext('function', $function);
if ($async) {
- $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
-
if(is_null($scheduledAt)) {
+ $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
$queueForFunctions
->setType('http')
->setExecution($execution)
@@ -1770,7 +1769,7 @@ App::post('/v1/functions/:functionId/executions')
'jwt' => $jwt,
];
- $dbForConsole->createDocument('schedules', new Document([
+ $schedule = $dbForConsole->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => ScheduleExecutions::getSupportedResource(),
'resourceId' => $execution->getId(),
@@ -1781,6 +1780,13 @@ App::post('/v1/functions/:functionId/executions')
'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
@@ -1825,7 +1831,8 @@ App::post('/v1/functions/:functionId/executions')
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
- 'APPWRITE_VERSION' => APP_VERSION_STABLE
+ 'APPWRITE_VERSION' => APP_VERSION_STABLE,
+ 'APPWRITE_REGION' => $project->getAttribute('region'),
]);
/** Execute function */
@@ -2349,3 +2356,64 @@ App::delete('/v1/functions/:functionId/variables/:variableId')
$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);
+ });
diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php
index 652644d44b..b080066653 100644
--- a/app/controllers/api/storage.php
+++ b/app/controllers/api/storage.php
@@ -63,7 +63,7 @@ App::post('/v1/storage/buckets')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
- ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
+ ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
@@ -240,7 +240,7 @@ App::put('/v1/storage/buckets/:bucketId')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('fileSecurity', false, new Boolean(true), 'Enables configuring permissions for individual file. A user needs one of file or bucket level permissions to access a file. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(true), 'Is bucket enabled? When set to \'disabled\', users cannot access the files in this bucket but Server SDKs with and API key can still access the bucket. No files are lost when this is toggled.', true)
- ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1024 * 1024), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
+ ->param('maximumFileSize', fn (array $plan) => empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000, fn (array $plan) => new Range(1, empty($plan['fileSize']) ? (int) System::getEnv('_APP_STORAGE_LIMIT', 0) : $plan['fileSize'] * 1000 * 1000), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(System::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '.', true, ['plan'])
->param('allowedFileExtensions', [], new ArrayList(new Text(64), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Allowed file extensions. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' extensions are allowed, each 64 characters long.', true)
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD]), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php
index 8b32349957..78988f525b 100644
--- a/app/controllers/api/users.php
+++ b/app/controllers/api/users.php
@@ -2109,7 +2109,7 @@ App::post('/v1/users/:userId/jwts')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_JWT)
->param('userId', '', new UID(), 'User ID.')
- ->param('sessionId', 'recent', new UID(), 'Session ID. Use the string \'recent\' to use the most recent session. Defaults to the most recent session.', true)
+ ->param('sessionId', '', new UID(), 'Session ID. Use the string \'recent\' to use the most recent session. Defaults to the most recent session.', true)
->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true)
->inject('response')
->inject('dbForProject')
@@ -2137,17 +2137,13 @@ App::post('/v1/users/:userId/jwts')
}
}
- if ($session->isEmpty()) {
- throw new Exception(Exception::USER_SESSION_NOT_FOUND);
- }
-
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic(new Document(['jwt' => $jwt->encode([
'userId' => $user->getId(),
- 'sessionId' => $session->getId()
+ 'sessionId' => $session->isEmpty() ? '' : $session->getId()
])]), Response::MODEL_JWT);
});
diff --git a/app/controllers/general.php b/app/controllers/general.php
index 10c9eb8e18..d532110df3 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -2,6 +2,7 @@
require_once __DIR__ . '/../init.php';
+use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
@@ -11,9 +12,11 @@ use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
+use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
+use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
@@ -163,7 +166,15 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"');
}
+ $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 = \array_merge([], $requestHeaders);
+ $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-id'] = '';
$headers['x-appwrite-user-jwt'] = '';
@@ -242,15 +253,21 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo
$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_VERSION' => APP_VERSION_STABLE
+ 'APPWRITE_VERSION' => APP_VERSION_STABLE,
+ 'APPWRITE_REGION' => $project->getAttribute('region'),
]);
/** Execute function */
@@ -434,6 +451,9 @@ App::init()
if (version_compare($requestFormat, '1.5.0', '<')) {
$request->addFilter(new RequestV17());
}
+ if (version_compare($requestFormat, '1.6.0', '<')) {
+ $request->addFilter(new RequestV18());
+ }
}
$domain = $request->getHostname();
@@ -550,6 +570,9 @@ App::init()
if (version_compare($responseFormat, '1.5.0', '<')) {
$response->addFilter(new ResponseV17());
}
+ if (version_compare($responseFormat, '1.6.0', '<')) {
+ $response->addFilter(new ResponseV18());
+ }
if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) {
$response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is ". APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks");
}
diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php
index 583a0160a1..672366fa01 100644
--- a/app/controllers/shared/api.php
+++ b/app/controllers/shared/api.php
@@ -206,6 +206,7 @@ App::init()
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
+ // Remove after migration
if(!\str_contains($apiKey, '_')) {
$keyType = API_KEY_STANDARD;
$authKey = $apiKey;
diff --git a/app/init.php b/app/init.php
index ef7742956f..5ecd78cf00 100644
--- a/app/init.php
+++ b/app/init.php
@@ -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-inputs', __DIR__ . '/config/storage/inputs.php');
Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php');
+Config::load('function-templates', __DIR__ . '/config/function-templates.php');
/**
* New DB Filters
@@ -1242,14 +1243,15 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
}
$jwtUserId = $payload['userId'] ?? '';
- $jwtSessionId = $payload['sessionId'] ?? '';
-
- if ($jwtUserId && $jwtSessionId) {
+ if (!empty($jwtUserId)) {
$user = $dbForProject->getDocument('users', $jwtUserId);
}
- if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
- $user = new Document([]);
+ $jwtSessionId = $payload['sessionId'] ?? '';
+ if(!empty($jwtSessionId)) {
+ if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
+ $user = new Document([]);
+ }
}
}
diff --git a/dev/xdebug.ini b/dev/xdebug.ini
index e29c8bd46e..f9c535019f 100644
--- a/dev/xdebug.ini
+++ b/dev/xdebug.ini
@@ -1,6 +1,8 @@
zend_extension=xdebug
[xdebug]
-xdebug.mode=develop,debug
+xdebug.mode=develop,debug,profile
xdebug.client_host=host.docker.internal
-xdebug.start_with_request=yes
\ No newline at end of file
+xdebug.start_with_request=yes
+xdebug.output_dir=/tmp/xdebug
+xdebug.use_compression=false
diff --git a/docs/references/avatars/get-favicon.md b/docs/references/avatars/get-favicon.md
index b571e0af93..c3a63dd83f 100644
--- a/docs/references/avatars/get-favicon.md
+++ b/docs/references/avatars/get-favicon.md
@@ -1 +1,3 @@
Use this endpoint to fetch the favorite icon (AKA favicon) of any remote website URL.
+
+This endpoint does not follow HTTP redirects.
\ No newline at end of file
diff --git a/docs/references/avatars/get-image.md b/docs/references/avatars/get-image.md
index efd654b362..ca9cc5e418 100644
--- a/docs/references/avatars/get-image.md
+++ b/docs/references/avatars/get-image.md
@@ -1,3 +1,5 @@
Use this endpoint to fetch a remote image URL and crop it to any image size you want. This endpoint is very useful if you need to crop and display remote images in your app or in case you want to make sure a 3rd party image is properly served using a TLS protocol.
When one dimension is specified and the other is 0, the image is scaled with preserved aspect ratio. If both dimensions are 0, the API provides an image at source quality. If dimensions are not specified, the default size of image returned is 400x400px.
+
+This endpoint does not follow HTTP redirects.
\ No newline at end of file
diff --git a/docs/references/functions/get-template.md b/docs/references/functions/get-template.md
new file mode 100644
index 0000000000..ccdcce7352
--- /dev/null
+++ b/docs/references/functions/get-template.md
@@ -0,0 +1 @@
+Get a function template using ID. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
\ No newline at end of file
diff --git a/docs/references/functions/list-templates.md b/docs/references/functions/list-templates.md
new file mode 100644
index 0000000000..ed43b9cbf4
--- /dev/null
+++ b/docs/references/functions/list-templates.md
@@ -0,0 +1 @@
+List available function templates. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
\ No newline at end of file
diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php
index dbc7d9425e..884296ff67 100644
--- a/src/Appwrite/Extend/Exception.php
+++ b/src/Appwrite/Extend/Exception.php
@@ -156,6 +156,7 @@ class Exception extends \Exception
public const FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
public const FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
+ public const FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found';
/** Deployments */
public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found';
diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php
index 597943f842..9075b85702 100644
--- a/src/Appwrite/Migration/Migration.php
+++ b/src/Appwrite/Migration/Migration.php
@@ -86,7 +86,7 @@ abstract class Migration
'1.5.5' => 'V20',
'1.5.6' => 'V20',
'1.5.7' => 'V20',
- '1.6.0' => 'V21',
+ '1.6.0' => 'V21'
];
/**
diff --git a/src/Appwrite/Migration/Version/V21.php b/src/Appwrite/Migration/Version/V21.php
index 75d083d87d..7dd2912234 100644
--- a/src/Appwrite/Migration/Version/V21.php
+++ b/src/Appwrite/Migration/Version/V21.php
@@ -7,6 +7,8 @@ use Exception;
use Throwable;
use Utopia\CLI\Console;
use Utopia\Database\Database;
+use Utopia\Database\DateTime;
+use Utopia\Database\Document;
class V21 extends Migration
{
@@ -31,6 +33,9 @@ class V21 extends Migration
Console::info('Migrating Collections');
$this->migrateCollections();
+
+ Console::info('Migrating Documents');
+ $this->forEachDocument([$this, 'fixDocument']);
}
/**
@@ -57,16 +62,119 @@ class V21 extends Migration
switch ($id) {
case 'projects':
-
// Create accessedAt attribute
try {
$this->createAttributeFromCollection($this->projectDB, $id, 'accessedAt');
} catch (Throwable $th) {
Console::warning("'accessedAt' from {$id}: {$th->getMessage()}");
}
+ break;
+ case 'schedules':
+ // Create data attribute
+ try {
+ $this->createAttributeFromCollection($this->projectDB, $id, 'data');
+ } catch (Throwable $th) {
+ Console::warning("'data' from {$id}: {$th->getMessage()}");
+ }
break;
+
+ case 'functions':
+ // Create scopes attribute
+ try {
+ $this->createAttributeFromCollection($this->projectDB, $id, 'scopes');
+ } catch (Throwable $th) {
+ Console::warning("'scopes' from {$id}: {$th->getMessage()}");
+ }
+
+ // Create size attribute
+ try {
+ $this->createAttributeFromCollection($this->projectDB, $id, 'size');
+ } catch (Throwable $th) {
+ Console::warning("'size' from {$id}: {$th->getMessage()}");
+ }
+
+ break;
+ case 'executions':
+ // Create requestMethod index
+ try {
+ $this->createIndexFromCollection($this->projectDB, $id, '_key_requestMethod');
+ } catch (\Throwable $th) {
+ Console::warning("'_key_requestMethod' from {$id}: {$th->getMessage()}");
+ }
+
+ // Create requestPath index
+ try {
+ $this->createIndexFromCollection($this->projectDB, $id, '_key_requestPath');
+ } catch (\Throwable $th) {
+ Console::warning("'_key_requestPath' from {$id}: {$th->getMessage()}");
+ }
+
+ // Create deployment index
+ try {
+ $this->createIndexFromCollection($this->projectDB, $id, '_key_deployment');
+ } catch (\Throwable $th) {
+ Console::warning("'_key_deployment' from {$id}: {$th->getMessage()}");
+ }
+
+ try {
+ /**
+ * Create 'scheduledAt' attribute
+ */
+ $this->createAttributeFromCollection($this->projectDB, $id, 'scheduledAt');
+ } catch (\Throwable $th) {
+ Console::warning("'scheduledAt' from {$id}: {$th->getMessage()}");
+ }
+
+ try {
+ /**
+ * Create 'scheduleInternalId' attribute
+ */
+ $this->createAttributeFromCollection($this->projectDB, $id, 'scheduleInternalId');
+ } catch (\Throwable $th) {
+ Console::warning("'scheduleInternalId' from {$id}: {$th->getMessage()}");
+ }
+
+ try {
+ /**
+ * Create 'scheduleId' attribute
+ */
+ $this->createAttributeFromCollection($this->projectDB, $id, 'scheduleId');
+ } catch (\Throwable $th) {
+ Console::warning("'scheduleId' from {$id}: {$th->getMessage()}");
+ }
}
+
+ usleep(50000);
}
}
+
+ /**
+ * Fix run on each document
+ *
+ * @param Document $document
+ * @return Document
+ */
+ protected function fixDocument(Document $document): Document
+ {
+ switch ($document->getCollection()) {
+ case 'projects':
+ /**
+ * Bump version number.
+ */
+ $document->setAttribute('version', '1.6.0');
+
+ // Add accessedAt attribute
+ $document->setAttribute('accessedAt', DateTime::now());
+ break;
+ case 'functions':
+ // Add scopes attribute
+ $document->setAttribute('scopes', []);
+
+ // Add size attribute
+ $document->setAttribute('size', 's-1vcpu-512m');
+ }
+
+ return $document;
+ }
}
diff --git a/src/Appwrite/Platform/Workers/Builds.php b/src/Appwrite/Platform/Workers/Builds.php
index 922498c3fa..b712edd402 100644
--- a/src/Appwrite/Platform/Workers/Builds.php
+++ b/src/Appwrite/Platform/Workers/Builds.php
@@ -400,7 +400,8 @@ class Builds extends Action
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
- 'APPWRITE_VERSION' => APP_VERSION_STABLE
+ 'APPWRITE_VERSION' => APP_VERSION_STABLE,
+ 'APPWRITE_REGION' => $project->getAttribute('region'),
]);
$command = $deployment->getAttribute('commands', '');
diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php
index 5dfdc0a63a..78671bfeb0 100644
--- a/src/Appwrite/Platform/Workers/Functions.php
+++ b/src/Appwrite/Platform/Workers/Functions.php
@@ -373,6 +373,9 @@ class Functions extends Action
$headers['x-appwrite-event'] = $event ?? '';
$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';
/** Create execution or update execution status */
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
@@ -464,7 +467,8 @@ class Functions extends Action
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
- 'APPWRITE_VERSION' => APP_VERSION_STABLE
+ 'APPWRITE_VERSION' => APP_VERSION_STABLE,
+ 'APPWRITE_REGION' => $project->getAttribute('region'),
]);
/** Execute function */
diff --git a/src/Appwrite/Utopia/Request/Filters/V18.php b/src/Appwrite/Utopia/Request/Filters/V18.php
new file mode 100644
index 0000000000..4f889ce66b
--- /dev/null
+++ b/src/Appwrite/Utopia/Request/Filters/V18.php
@@ -0,0 +1,21 @@
+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('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('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))
@@ -409,6 +417,9 @@ class Response extends SwooleResponse
->setModel(new Team())
->setModel(new Membership())
->setModel(new Func())
+ ->setModel(new TemplateFunction())
+ ->setModel(new TemplateRuntime())
+ ->setModel(new TemplateVariable())
->setModel(new Installation())
->setModel(new ProviderRepository())
->setModel(new Detection())
diff --git a/src/Appwrite/Utopia/Response/Filters/V18.php b/src/Appwrite/Utopia/Response/Filters/V18.php
new file mode 100644
index 0000000000..d0aa680e3b
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Filters/V18.php
@@ -0,0 +1,43 @@
+ $this->parseFunction($content),
+ Response::MODEL_EXECUTION => $this->parseExecution($content),
+ Response::MODEL_PROJECT => $this->parseProject($content),
+ default => $parsedResponse,
+ };
+
+ return $parsedResponse;
+ }
+
+ protected function parseExecution(array $content)
+ {
+ unset($content['scheduledAt']);
+ return $content;
+ }
+
+ protected function parseFunction(array $content)
+ {
+ unset($content['scopes']);
+ return $content;
+ }
+
+ protected function parseProject(array $content)
+ {
+ unset($content['authMockNumbers']);
+ unset($content['authSessionAlerts']);
+ return $content;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Execution.php b/src/Appwrite/Utopia/Response/Model/Execution.php
index fbd9619a40..90fbdc9689 100644
--- a/src/Appwrite/Utopia/Response/Model/Execution.php
+++ b/src/Appwrite/Utopia/Response/Model/Execution.php
@@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
+use Utopia\Database\DateTime;
use Utopia\Database\Helpers\Role;
class Execution extends Model
@@ -110,6 +111,13 @@ class Execution extends Model
'default' => 0,
'example' => 0.400,
])
+ ->addRule('scheduledAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'The scheduled time for execution. If left empty, execution will be queued immediately.',
+ 'required' => false,
+ 'default' => DateTime::now(),
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
;
}
diff --git a/src/Appwrite/Utopia/Response/Model/TemplateFunction.php b/src/Appwrite/Utopia/Response/Model/TemplateFunction.php
new file mode 100644
index 0000000000..7a3a4cfbd5
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/TemplateFunction.php
@@ -0,0 +1,136 @@
+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 .',
+ ])
+ ->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;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/TemplateRuntime.php b/src/Appwrite/Utopia/Response/Model/TemplateRuntime.php
new file mode 100644
index 0000000000..c08ea9b32a
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/TemplateRuntime.php
@@ -0,0 +1,59 @@
+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;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/TemplateVariable.php b/src/Appwrite/Utopia/Response/Model/TemplateVariable.php
new file mode 100644
index 0000000000..b0fd919dbf
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/TemplateVariable.php
@@ -0,0 +1,65 @@
+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;
+ }
+}
diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php
index 29ed756932..3206381e95 100644
--- a/src/Executor/Executor.php
+++ b/src/Executor/Executor.php
@@ -75,6 +75,12 @@ class Executor
$runtimeId = "$projectId-$deploymentId-build";
$route = "/runtimes";
$timeout = (int) System::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
+
+ // Remove after migration
+ if ($version == 'v3') {
+ $version = 'v4';
+ }
+
$params = [
'runtimeId' => $runtimeId,
'source' => $source,
@@ -188,6 +194,13 @@ class Executor
$runtimeId = "$projectId-$deploymentId";
$route = '/runtimes/' . $runtimeId . '/execution';
+
+
+ // Remove after migration
+ if ($version == 'v3') {
+ $version = 'v4';
+ }
+
$params = [
'runtimeId' => $runtimeId,
'variables' => $variables,
diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php
index c45ebbe068..abefb5c9a3 100644
--- a/tests/e2e/Services/Functions/FunctionsBase.php
+++ b/tests/e2e/Services/Functions/FunctionsBase.php
@@ -15,6 +15,31 @@ trait FunctionsBase
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
}
+ protected function awaitDeploymentIsBuilt($functionId, $deploymentId, $checkForSuccess = true): void
+ {
+ while (true) {
+ $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ if (
+ $deployment['headers']['status-code'] >= 400
+ || \in_array($deployment['body']['status'], ['ready', 'failed'])
+ ) {
+ break;
+ }
+
+ \sleep(1);
+ }
+
+ if($checkForSuccess) {
+ $this->assertEquals(200, $deployment['headers']['status-code']);
+ $this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body']));
+ }
+ }
+
// /**
// * @depends testCreateTeam
// */
diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php
index bf969d388a..3aaa18594e 100644
--- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php
+++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php
@@ -8,6 +8,8 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
+use Utopia\Config\Config;
+use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
@@ -119,25 +121,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
-
- $this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body']));
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId);
$function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
'content-type' => 'application/json',
@@ -238,25 +222,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
-
- $this->assertEquals('ready', $deployment['body']['status']);
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId);
$function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
'content-type' => 'application/json',
@@ -269,6 +235,7 @@ class FunctionsCustomClientTest extends Scope
// Schedule execution for the future
\date_default_timezone_set('UTC');
$futureTime = (new \DateTime())->add(new \DateInterval('PT10S'))->format('Y-m-d H:i:s');
+ $futureTimeIso = DateTime::formatTz($futureTime);
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $function['body']['$id'] . '/executions', array_merge([
'content-type' => 'application/json',
@@ -286,9 +253,23 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals(202, $execution['headers']['status-code']);
$this->assertEquals('scheduled', $execution['body']['status']);
+ $this->assertEquals($futureTimeIso, $execution['body']['scheduledAt']);
$executionId = $execution['body']['$id'];
+ // List executions and ensure it has schedule date
+ $response = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertGreaterThan(0, \count($response['body']['executions']));
+ $recentExecution = $response['body']['executions'][0];
+ $this->assertEquals($executionId, $recentExecution['$id']);
+ $this->assertEquals($futureTimeIso, $recentExecution['scheduledAt']);
+
sleep(20);
$execution = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/executions/' . $executionId, [
@@ -303,6 +284,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals('/custom', $execution['body']['requestPath']);
$this->assertEquals('GET', $execution['body']['requestMethod']);
$this->assertGreaterThan(0, $execution['body']['duration']);
+ $this->assertEquals($futureTimeIso, $execution['body']['scheduledAt']);
/* Test for FAILURE */
@@ -401,25 +383,7 @@ class FunctionsCustomClientTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
-
- $this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body']));
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId);
$function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
'content-type' => 'application/json',
@@ -449,6 +413,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals(APP_VERSION_STABLE, $output['APPWRITE_VERSION']);
+ $this->assertEquals('default', $output['APPWRITE_REGION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
$this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']);
$this->assertEquals($this->getUser()['$id'], $output['APPWRITE_FUNCTION_USER_ID']);
@@ -530,25 +495,7 @@ class FunctionsCustomClientTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
-
- $this->assertEquals('ready', $deployment['body']['status']);
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId);
// Why do we have to do this?
$function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
@@ -787,25 +734,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
-
- $this->assertEquals('ready', $deployment['body']['status']);
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId);
$function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
'content-type' => 'application/json',
@@ -834,6 +763,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals(APP_VERSION_STABLE, $output['APPWRITE_VERSION']);
+ $this->assertEquals('default', $output['APPWRITE_REGION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
$this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']);
$this->assertEquals($this->getUser()['$id'], $output['APPWRITE_FUNCTION_USER_ID']);
@@ -897,25 +827,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
-
- $this->assertEquals('ready', $deployment['body']['status']);
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId);
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'application/json',
@@ -944,4 +856,131 @@ class FunctionsCustomClientTest extends Scope
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']);
+ }
}
diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
index c9f9e4443f..e3148752c8 100644
--- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
+++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
@@ -426,25 +426,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
- $this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body']));
-
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId);
$functionDetails = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
'content-type' => 'application/json',
@@ -483,26 +465,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt']));
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
-
- $this->assertEquals(200, $deployment['headers']['status-code']);
- $this->assertEquals('ready', $deployment['body']['status']);
+ $this->awaitDeploymentIsBuilt($data['functionId'], $deploymentId);
return array_merge($data, ['deploymentId' => $deploymentId]);
}
@@ -1167,25 +1130,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
-
- $this->assertEquals('ready', $deployment['body']['status']);
+ $this->awaitDeploymentIsBuilt($functionId, $deploymentId);
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'application/json',
@@ -1264,7 +1209,7 @@ class FunctionsCustomServerTest extends Scope
* @param string $entrypoint
*
* @dataProvider provideCustomExecutions
- * @depends testTimeout
+ * @depends testTimeout
*/
public function testCreateCustomExecution(string $folder, string $name, string $entrypoint, string $runtimeName, string $runtimeVersion)
{
@@ -1310,23 +1255,7 @@ class FunctionsCustomServerTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false);
$deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
@@ -1435,23 +1364,7 @@ class FunctionsCustomServerTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false);
$deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
@@ -1543,23 +1456,7 @@ class FunctionsCustomServerTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false);
$deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
@@ -1654,23 +1551,7 @@ class FunctionsCustomServerTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false);
$deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
@@ -1738,23 +1619,7 @@ class FunctionsCustomServerTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false);
$deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
@@ -1845,23 +1710,7 @@ class FunctionsCustomServerTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false);
$deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
@@ -1948,23 +1797,7 @@ class FunctionsCustomServerTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false);
$deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
@@ -2050,23 +1883,7 @@ class FunctionsCustomServerTest extends Scope
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
- // Poll until deployment is built
- while (true) {
- $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]);
-
- if (
- $deployment['headers']['status-code'] >= 400
- || \in_array($deployment['body']['status'], ['ready', 'failed'])
- ) {
- break;
- }
-
- \sleep(1);
- }
+ $this->awaitDeploymentIsBuilt($function['body']['$id'], $deploymentId, checkForSuccess: false);
$deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
@@ -2081,9 +1898,9 @@ class FunctionsCustomServerTest extends Scope
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
- $bytes = pack('C*', ...[0,20,255]);
+ $bytes = pack('C*', ...[0, 20, 255]);
- $response = $proxyClient->call(Client::METHOD_POST, '/', [ 'content-type' => 'text/plain' ], $bytes, false);
+ $response = $proxyClient->call(Client::METHOD_POST, '/', ['content-type' => 'text/plain'], $bytes, false);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(\md5($bytes), $response['body']);
diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php
index c34227e4de..c06bc6f4a8 100644
--- a/tests/e2e/Services/Users/UsersBase.php
+++ b/tests/e2e/Services/Users/UsersBase.php
@@ -1589,6 +1589,27 @@ trait UsersBase
], false);
$this->assertEquals($user['headers']['status-code'], 201);
+ // Create JWT 0, with no session available
+ $response = $this->client->call(Client::METHOD_POST, '/users/' . $userId . '/jwts', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), []);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']['jwt']);
+ $jwt0 = $response['body']['jwt'];
+
+ // Ensure JWT 0 works
+ $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-jwt' => $jwt0,
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($userId, $response['body']['$id']);
+
// Create two sessions
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
@@ -1641,12 +1662,13 @@ trait UsersBase
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($userId, $response['body']['$id']);
- // Create JWT 2 for latest session using default param
+ // Create JWT 2 for latest session using 'current' param
$response = $this->client->call(Client::METHOD_POST, '/users/' . $userId . '/jwts', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
- 'duration' => 5
+ 'duration' => 5,
+ 'sessionId' => 'current'
]);
$this->assertEquals(201, $response['headers']['status-code']);
@@ -1696,6 +1718,27 @@ trait UsersBase
$this->assertEquals(401, $response['headers']['status-code']);
+ // Ensure JWT 0 works still even with no sessions
+
+ $response = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId . '/sessions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'sessionId' => $session2Id
+ ]);
+
+ $this->assertEquals(204, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-jwt' => $jwt0,
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($userId, $response['body']['$id']);
+
// Cleanup after test
$response = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, array_merge([
diff --git a/tests/resources/functions/php-fn/index.php b/tests/resources/functions/php-fn/index.php
index b353ad2ca4..0b6e3d206c 100644
--- a/tests/resources/functions/php-fn/index.php
+++ b/tests/resources/functions/php-fn/index.php
@@ -11,6 +11,7 @@ return function ($context) {
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
'APPWRITE_VERSION' => \getenv('APPWRITE_VERSION') ?: '',
+ 'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'APPWRITE_FUNCTION_EVENT' => $context->req->headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' => $context->req->bodyRaw ?? '',
'APPWRITE_FUNCTION_DATA' => $context->req->bodyRaw ?? '',
diff --git a/tests/resources/functions/php-large/index.php b/tests/resources/functions/php-large/index.php
index 5a9666488e..ab3d050245 100644
--- a/tests/resources/functions/php-large/index.php
+++ b/tests/resources/functions/php-large/index.php
@@ -8,6 +8,7 @@ return function ($context) {
'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
+ 'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'UNICODE_TEST' => "êä"
]);
};
diff --git a/tests/resources/functions/php/index.php b/tests/resources/functions/php/index.php
index ac7b85a43a..d5328c40e1 100644
--- a/tests/resources/functions/php/index.php
+++ b/tests/resources/functions/php/index.php
@@ -8,6 +8,7 @@ return function ($context) {
'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
+ 'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'UNICODE_TEST' => "êä",
'GLOBAL_VARIABLE' => \getenv('GLOBAL_VARIABLE') ?: ''
]);
diff --git a/tests/unit/Utopia/Request/Filters/V18Test.php b/tests/unit/Utopia/Request/Filters/V18Test.php
new file mode 100644
index 0000000000..4e1f81573a
--- /dev/null
+++ b/tests/unit/Utopia/Request/Filters/V18Test.php
@@ -0,0 +1,51 @@
+filter = new V18();
+ }
+
+ public function tearDown(): void
+ {
+ }
+
+ public function deleteMfaAuthenticatorProvider()
+ {
+ return [
+ 'remove otp' => [
+ [
+ 'type' => 'totp',
+ 'otp' => 1230
+ ],
+ [
+ 'type' => 'totp'
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider deleteMfaAuthenticatorProvider
+ */
+ public function testdeleteMfaAuthenticator(array $content, array $expected): void
+ {
+ $model = 'account.deleteMfaAuthenticator';
+
+ $result = $this->filter->parse($content, $model);
+
+ $this->assertEquals($expected, $result);
+ }
+}
diff --git a/tests/unit/Utopia/Response/Filters/V18Test.php b/tests/unit/Utopia/Response/Filters/V18Test.php
new file mode 100644
index 0000000000..c4011c08a1
--- /dev/null
+++ b/tests/unit/Utopia/Response/Filters/V18Test.php
@@ -0,0 +1,110 @@
+filter = new V18();
+ }
+
+ public function tearDown(): void
+ {
+ }
+
+ public function functionProvider(): array
+ {
+ return [
+ 'remove scopes' => [
+ [
+ 'scopes' => [
+ 'example_scope',
+ 'example_scope2',
+ ],
+ ],
+ [
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider functionProvider
+ */
+ public function testFunction(array $content, array $expected): void
+ {
+ $model = Response::MODEL_FUNCTION;
+
+ $result = $this->filter->parse($content, $model);
+
+ $this->assertEquals($expected, $result);
+ }
+
+
+ public function executionProvider(): array
+ {
+ return [
+ 'remove scheduledAt' => [
+ [
+ 'scheduledAt' => '2024-07-13T09:00:00.000Z',
+ ],
+ [
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider executionProvider
+ */
+ public function testExecution(array $content, array $expected): void
+ {
+ $model = Response::MODEL_EXECUTION;
+
+ $result = $this->filter->parse($content, $model);
+
+ $this->assertEquals($expected, $result);
+ }
+
+ public function projectProvider(): array
+ {
+ return [
+ 'remove authMockNumbers and authSessionAlerts' => [
+ [
+ 'authMockNumbers' => [
+ 'example_mock_number',
+ 'example_mock_number2',
+ ],
+ 'authSessionAlerts' => [
+ 'example_alert',
+ 'example_alert2',
+ ],
+ ],
+ [
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider projectProvider
+ */
+ public function testProject(array $content, array $expected): void
+ {
+ $model = Response::MODEL_PROJECT;
+
+ $result = $this->filter->parse($content, $model);
+
+ $this->assertEquals($expected, $result);
+ }
+}