Resolve merge conflicts

This commit is contained in:
Khushboo Verma
2025-02-28 16:20:24 +05:30
84 changed files with 5492 additions and 2473 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ _APP_OPENSSL_KEY_V1=your-secret-key
_APP_DOMAIN=traefik
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_SITES=sites.localhost
_APP_DOMAIN_TARGET=localhost
_APP_DOMAIN_TARGET=test.appwrite.io
_APP_RULES_FORMAT=md5
_APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379
+2
View File
@@ -146,6 +146,7 @@ jobs:
Projects,
Realtime,
Sites,
Proxy,
Storage,
Teams,
Users,
@@ -212,6 +213,7 @@ jobs:
Projects,
Realtime,
Sites,
Proxy,
Storage,
Teams,
Users,
+39 -17
View File
@@ -1013,10 +1013,10 @@ return [
'filters' => [],
],
[
'$id' => ID::custom('resourceType'),
'$id' => ID::custom('type'), // 'api', 'redirect', 'deployment' (site or function)
'type' => Database::VAR_STRING,
'format' => '',
'size' => 100,
'size' => 32,
'signed' => true,
'required' => true,
'default' => null,
@@ -1024,24 +1024,28 @@ return [
'filters' => [],
],
[
'$id' => ID::custom('resourceInternalId'),
// If 'api', then (empty)
// If 'redirect', then URL
// If 'deployment', then deployment ID
'$id' => ID::custom('value'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'size' => 512,
'signed' => true,
'required' => false,
'default' => null,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('resourceId'),
// Examples: branch=main
'$id' => ID::custom('automation'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'default' => '',
'array' => false,
'filters' => [],
],
@@ -1066,9 +1070,27 @@ return [
'default' => null,
'array' => false,
'filters' => [],
]
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_domain'),
'type' => Database::INDEX_UNIQUE,
@@ -1091,24 +1113,24 @@ return [
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_resourceInternalId',
'$id' => '_key_type',
'type' => Database::INDEX_KEY,
'attributes' => ['resourceInternalId'],
'lengths' => [Database::LENGTH_KEY],
'attributes' => ['type'],
'lengths' => [32],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_resourceId',
'$id' => '_key_value',
'type' => Database::INDEX_KEY,
'attributes' => ['resourceId'],
'lengths' => [Database::LENGTH_KEY],
'attributes' => ['value'],
'lengths' => [512],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_resourceType',
'$id' => '_key_automation',
'type' => Database::INDEX_KEY,
'attributes' => ['resourceType'],
'lengths' => [],
'attributes' => ['automation'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
+1 -1
View File
@@ -191,7 +191,7 @@ return [
'name' => 'Functions',
'subtitle' => 'The Functions Service allows you view, create and manage your Cloud Functions.',
'description' => '/docs/services/functions.md',
'controller' => 'api/functions.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/functions',
+269 -41
View File
@@ -4398,7 +4398,7 @@
"enum": [
"rules"
],
"x-enum-name": null,
"x-enum-name": "ConsoleResourceType",
"x-enum-keys": []
},
"in": "query"
@@ -23683,7 +23683,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, resourceType, resourceId, url",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, type, value, automation, url",
"required": false,
"schema": {
"type": "array",
@@ -23706,10 +23706,12 @@
"in": "query"
}
]
},
}
},
"\/proxy\/rules\/api": {
"post": {
"summary": "Create rule",
"operationId": "proxyCreateRule",
"summary": "Create API rule",
"operationId": "proxyCreateAPIRule",
"tags": [
"proxy"
],
@@ -23727,13 +23729,79 @@
}
},
"x-appwrite": {
"method": "createRule",
"method": "createAPIRule",
"weight": 423,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "proxy\/create-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule.",
"demo": "proxy\/create-a-p-i-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite's API on custom domain.",
"rate-limit": 10,
"rate-time": 60,
"rate-key": "userId:{userId}, url:{url}",
"scope": "rules.write",
"platforms": [
"console"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"requestBody": {
"content": {
"application\/json": {
"schema": {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Domain name.",
"x-example": null
}
},
"required": [
"domain"
]
}
}
}
}
}
},
"\/proxy\/rules\/function": {
"post": {
"summary": "Create function rule",
"operationId": "proxyCreateFunctionRule",
"tags": [
"proxy"
],
"description": "",
"responses": {
"201": {
"description": "Rule",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/proxyRule"
}
}
}
}
},
"x-appwrite": {
"method": "createFunctionRule",
"weight": 425,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "proxy\/create-function-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for executing Appwrite Function on custom domain.",
"rate-limit": 10,
"rate-time": 60,
"rate-key": "userId:{userId}, url:{url}",
@@ -23762,27 +23830,169 @@
"description": "Domain name.",
"x-example": null
},
"resourceType": {
"functionId": {
"type": "string",
"description": "Action definition for the rule. Possible values are \"api\", \"function\" and \"site\"",
"x-example": "api",
"enum": [
"api",
"function",
"site"
],
"x-enum-name": null,
"x-enum-keys": []
"description": "ID of function to be executed.",
"x-example": "<FUNCTION_ID>"
},
"resourceId": {
"branch": {
"type": "string",
"description": "ID of resource for the action type. If resourceType is \"api\", leave empty. If resourceType is \"function\", provide ID of the function.",
"x-example": "<RESOURCE_ID>"
"description": "Name of VCS branch to deploy changes automatically",
"x-example": "<BRANCH>"
}
},
"required": [
"domain",
"resourceType"
"functionId"
]
}
}
}
}
}
},
"\/proxy\/rules\/redirect": {
"post": {
"summary": "Create Redirect rule",
"operationId": "proxyCreateRedirectRule",
"tags": [
"proxy"
],
"description": "",
"responses": {
"201": {
"description": "Rule",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/proxyRule"
}
}
}
}
},
"x-appwrite": {
"method": "createRedirectRule",
"weight": 426,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "proxy\/create-redirect-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for to redirect from custom domain to another domain.",
"rate-limit": 10,
"rate-time": 60,
"rate-key": "userId:{userId}, url:{url}",
"scope": "rules.write",
"platforms": [
"console"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"requestBody": {
"content": {
"application\/json": {
"schema": {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Domain name.",
"x-example": null
},
"target": {
"type": "string",
"description": "Target domain (hostname) of redirection",
"x-example": null
}
},
"required": [
"domain",
"target"
]
}
}
}
}
}
},
"\/proxy\/rules\/site": {
"post": {
"summary": "Create site rule",
"operationId": "proxyCreateSiteRule",
"tags": [
"proxy"
],
"description": "",
"responses": {
"201": {
"description": "Rule",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/proxyRule"
}
}
}
}
},
"x-appwrite": {
"method": "createSiteRule",
"weight": 424,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "proxy\/create-site-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite Site on custom domain.",
"rate-limit": 10,
"rate-time": 60,
"rate-key": "userId:{userId}, url:{url}",
"scope": "rules.write",
"platforms": [
"console"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"requestBody": {
"content": {
"application\/json": {
"schema": {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Domain name.",
"x-example": null
},
"siteId": {
"type": "string",
"description": "ID of site to be executed.",
"x-example": "<SITE_ID>"
},
"branch": {
"type": "string",
"description": "Name of VCS branch to deploy changes automatically",
"x-example": "<BRANCH>"
}
},
"required": [
"domain",
"siteId"
]
}
}
@@ -24205,8 +24415,14 @@
},
"adapter": {
"type": "string",
"description": "Framework adapter. Allows: static, ssr",
"x-example": "<ADAPTER>"
"description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr",
"x-example": "static",
"enum": [
"static",
"ssr"
],
"x-enum-name": null,
"x-enum-keys": []
},
"installationId": {
"type": "string",
@@ -24772,8 +24988,14 @@
},
"adapter": {
"type": "string",
"description": "Framework adapter. Usuallly allows: static, ssr",
"x-example": "<ADAPTER>"
"description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr",
"x-example": "static",
"enum": [
"static",
"ssr"
],
"x-enum-name": null,
"x-enum-keys": []
},
"fallbackFile": {
"type": "string",
@@ -37167,11 +37389,6 @@
"x-example": 128,
"format": "int32"
},
"domain": {
"type": "string",
"description": "Preview domain.",
"x-example": "deploy1-project1.appwrite.site"
},
"providerRepositoryName": {
"type": "string",
"description": "The name of the vcs provider repository",
@@ -37240,7 +37457,6 @@
"status",
"buildLogs",
"buildTime",
"domain",
"providerRepositoryName",
"providerRepositoryOwner",
"providerRepositoryUrl",
@@ -39780,15 +39996,20 @@
"description": "Domain name.",
"x-example": "appwrite.company.com"
},
"resourceType": {
"type": {
"type": "string",
"description": "Action definition for the rule. Possible values are \"api\", \"function\", or \"redirect\"",
"x-example": "function"
"description": "Action definition for the rule. Possible values are \"api\", \"deployment\", or \"redirect\"",
"x-example": "deployment"
},
"resourceId": {
"value": {
"type": "string",
"description": "ID of resource for the action type. If resourceType is \"api\" or \"url\", it is empty. If resourceType is \"function\", it is ID of the function.",
"x-example": "myAwesomeFunction"
"description": "Detail specification for the type. If type is \"api\", this is empty. If type is \"redirect\", this is URL. If type is \"deployment\", this is deployment ID.",
"x-example": "67a9cf1a00150ee93abd"
},
"automation": {
"type": "string",
"description": "Action that results in a rule update. If VCS branch, value can be of syntax \"branch=[name]\"",
"x-example": "branch=dev"
},
"status": {
"type": "string",
@@ -39811,8 +40032,9 @@
"$createdAt",
"$updatedAt",
"domain",
"resourceType",
"resourceId",
"type",
"value",
"automation",
"status",
"logs",
"renewAt"
@@ -39944,6 +40166,11 @@
"type": "string",
"description": "Defines if HTTPS is enforced for all requests.",
"x-example": "enabled"
},
"_APP_DOMAINS_NAMESERVERS": {
"type": "string",
"description": "Comma-separated list of nameservers.",
"x-example": "ns1.example.com,ns2.example.com"
}
},
"required": [
@@ -39955,7 +40182,8 @@
"_APP_DOMAIN_ENABLED",
"_APP_ASSISTANT_ENABLED",
"_APP_DOMAIN_SITES",
"_APP_OPTIONS_FORCE_HTTPS"
"_APP_OPTIONS_FORCE_HTTPS",
"_APP_DOMAINS_NAMESERVERS"
]
},
"mfaChallenge": {
+16 -10
View File
@@ -16526,8 +16526,14 @@
},
"adapter": {
"type": "string",
"description": "Framework adapter. Allows: static, ssr",
"x-example": "<ADAPTER>"
"description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr",
"x-example": "static",
"enum": [
"static",
"ssr"
],
"x-enum-name": null,
"x-enum-keys": []
},
"installationId": {
"type": "string",
@@ -16871,8 +16877,14 @@
},
"adapter": {
"type": "string",
"description": "Framework adapter. Usuallly allows: static, ssr",
"x-example": "<ADAPTER>"
"description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr",
"x-example": "static",
"enum": [
"static",
"ssr"
],
"x-enum-name": null,
"x-enum-keys": []
},
"fallbackFile": {
"type": "string",
@@ -27488,11 +27500,6 @@
"x-example": 128,
"format": "int32"
},
"domain": {
"type": "string",
"description": "Preview domain.",
"x-example": "deploy1-project1.appwrite.site"
},
"providerRepositoryName": {
"type": "string",
"description": "The name of the vcs provider repository",
@@ -27561,7 +27568,6 @@
"status",
"buildLogs",
"buildTime",
"domain",
"providerRepositoryName",
"providerRepositoryOwner",
"providerRepositoryUrl",
+281 -41
View File
@@ -4604,7 +4604,7 @@
"enum": [
"rules"
],
"x-enum-name": null,
"x-enum-name": "ConsoleResourceType",
"x-enum-keys": [],
"in": "query"
}
@@ -24168,7 +24168,7 @@
"parameters": [
{
"name": "queries",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, resourceType, resourceId, url",
"description": "Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https:\/\/appwrite.io\/docs\/databases#querying-documents). Maximum of 100 queries are allowed, each 4096 characters long. You may filter on the following attributes: domain, type, value, automation, url",
"required": false,
"type": "array",
"collectionFormat": "multi",
@@ -24188,10 +24188,12 @@
"in": "query"
}
]
},
}
},
"\/proxy\/rules\/api": {
"post": {
"summary": "Create rule",
"operationId": "proxyCreateRule",
"summary": "Create API rule",
"operationId": "proxyCreateAPIRule",
"consumes": [
"application\/json"
],
@@ -24211,13 +24213,82 @@
}
},
"x-appwrite": {
"method": "createRule",
"method": "createAPIRule",
"weight": 423,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "proxy\/create-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule.",
"demo": "proxy\/create-a-p-i-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite's API on custom domain.",
"rate-limit": 10,
"rate-time": 60,
"rate-key": "userId:{userId}, url:{url}",
"scope": "rules.write",
"platforms": [
"console"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "payload",
"in": "body",
"schema": {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Domain name.",
"default": null,
"x-example": null
}
},
"required": [
"domain"
]
}
}
]
}
},
"\/proxy\/rules\/function": {
"post": {
"summary": "Create function rule",
"operationId": "proxyCreateFunctionRule",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"proxy"
],
"description": "",
"responses": {
"201": {
"description": "Rule",
"schema": {
"$ref": "#\/definitions\/proxyRule"
}
}
},
"x-appwrite": {
"method": "createFunctionRule",
"weight": 425,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "proxy\/create-function-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for executing Appwrite Function on custom domain.",
"rate-limit": 10,
"rate-time": 60,
"rate-key": "userId:{userId}, url:{url}",
@@ -24248,29 +24319,180 @@
"default": null,
"x-example": null
},
"resourceType": {
"functionId": {
"type": "string",
"description": "Action definition for the rule. Possible values are \"api\", \"function\" and \"site\"",
"description": "ID of function to be executed.",
"default": null,
"x-example": "api",
"enum": [
"api",
"function",
"site"
],
"x-enum-name": null,
"x-enum-keys": []
"x-example": "<FUNCTION_ID>"
},
"resourceId": {
"branch": {
"type": "string",
"description": "ID of resource for the action type. If resourceType is \"api\", leave empty. If resourceType is \"function\", provide ID of the function.",
"description": "Name of VCS branch to deploy changes automatically",
"default": "",
"x-example": "<RESOURCE_ID>"
"x-example": "<BRANCH>"
}
},
"required": [
"domain",
"resourceType"
"functionId"
]
}
}
]
}
},
"\/proxy\/rules\/redirect": {
"post": {
"summary": "Create Redirect rule",
"operationId": "proxyCreateRedirectRule",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"proxy"
],
"description": "",
"responses": {
"201": {
"description": "Rule",
"schema": {
"$ref": "#\/definitions\/proxyRule"
}
}
},
"x-appwrite": {
"method": "createRedirectRule",
"weight": 426,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "proxy\/create-redirect-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for to redirect from custom domain to another domain.",
"rate-limit": 10,
"rate-time": 60,
"rate-key": "userId:{userId}, url:{url}",
"scope": "rules.write",
"platforms": [
"console"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "payload",
"in": "body",
"schema": {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Domain name.",
"default": null,
"x-example": null
},
"target": {
"type": "string",
"description": "Target domain (hostname) of redirection",
"default": null,
"x-example": null
}
},
"required": [
"domain",
"target"
]
}
}
]
}
},
"\/proxy\/rules\/site": {
"post": {
"summary": "Create site rule",
"operationId": "proxyCreateSiteRule",
"consumes": [
"application\/json"
],
"produces": [
"application\/json"
],
"tags": [
"proxy"
],
"description": "",
"responses": {
"201": {
"description": "Rule",
"schema": {
"$ref": "#\/definitions\/proxyRule"
}
}
},
"x-appwrite": {
"method": "createSiteRule",
"weight": 424,
"cookies": false,
"type": "",
"deprecated": false,
"demo": "proxy\/create-site-rule.md",
"edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a new proxy rule for serving Appwrite Site on custom domain.",
"rate-limit": 10,
"rate-time": 60,
"rate-key": "userId:{userId}, url:{url}",
"scope": "rules.write",
"platforms": [
"console"
],
"packaging": false,
"auth": {
"Project": []
}
},
"security": [
{
"Project": []
}
],
"parameters": [
{
"name": "payload",
"in": "body",
"schema": {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Domain name.",
"default": null,
"x-example": null
},
"siteId": {
"type": "string",
"description": "ID of site to be executed.",
"default": null,
"x-example": "<SITE_ID>"
},
"branch": {
"type": "string",
"description": "Name of VCS branch to deploy changes automatically",
"default": "",
"x-example": "<BRANCH>"
}
},
"required": [
"domain",
"siteId"
]
}
}
@@ -24708,9 +24930,15 @@
},
"adapter": {
"type": "string",
"description": "Framework adapter. Allows: static, ssr",
"description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr",
"default": "",
"x-example": "<ADAPTER>"
"x-example": "static",
"enum": [
"static",
"ssr"
],
"x-enum-name": null,
"x-enum-keys": []
},
"installationId": {
"type": "string",
@@ -25287,9 +25515,15 @@
},
"adapter": {
"type": "string",
"description": "Framework adapter. Usuallly allows: static, ssr",
"description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr",
"default": "",
"x-example": "<ADAPTER>"
"x-example": "static",
"enum": [
"static",
"ssr"
],
"x-enum-name": null,
"x-enum-keys": []
},
"fallbackFile": {
"type": "string",
@@ -37750,11 +37984,6 @@
"x-example": 128,
"format": "int32"
},
"domain": {
"type": "string",
"description": "Preview domain.",
"x-example": "deploy1-project1.appwrite.site"
},
"providerRepositoryName": {
"type": "string",
"description": "The name of the vcs provider repository",
@@ -37823,7 +38052,6 @@
"status",
"buildLogs",
"buildTime",
"domain",
"providerRepositoryName",
"providerRepositoryOwner",
"providerRepositoryUrl",
@@ -40434,15 +40662,20 @@
"description": "Domain name.",
"x-example": "appwrite.company.com"
},
"resourceType": {
"type": {
"type": "string",
"description": "Action definition for the rule. Possible values are \"api\", \"function\", or \"redirect\"",
"x-example": "function"
"description": "Action definition for the rule. Possible values are \"api\", \"deployment\", or \"redirect\"",
"x-example": "deployment"
},
"resourceId": {
"value": {
"type": "string",
"description": "ID of resource for the action type. If resourceType is \"api\" or \"url\", it is empty. If resourceType is \"function\", it is ID of the function.",
"x-example": "myAwesomeFunction"
"description": "Detail specification for the type. If type is \"api\", this is empty. If type is \"redirect\", this is URL. If type is \"deployment\", this is deployment ID.",
"x-example": "67a9cf1a00150ee93abd"
},
"automation": {
"type": "string",
"description": "Action that results in a rule update. If VCS branch, value can be of syntax \"branch=[name]\"",
"x-example": "branch=dev"
},
"status": {
"type": "string",
@@ -40465,8 +40698,9 @@
"$createdAt",
"$updatedAt",
"domain",
"resourceType",
"resourceId",
"type",
"value",
"automation",
"status",
"logs",
"renewAt"
@@ -40598,6 +40832,11 @@
"type": "string",
"description": "Defines if HTTPS is enforced for all requests.",
"x-example": "enabled"
},
"_APP_DOMAINS_NAMESERVERS": {
"type": "string",
"description": "Comma-separated list of nameservers.",
"x-example": "ns1.example.com,ns2.example.com"
}
},
"required": [
@@ -40609,7 +40848,8 @@
"_APP_DOMAIN_ENABLED",
"_APP_ASSISTANT_ENABLED",
"_APP_DOMAIN_SITES",
"_APP_OPTIONS_FORCE_HTTPS"
"_APP_OPTIONS_FORCE_HTTPS",
"_APP_DOMAINS_NAMESERVERS"
]
},
"mfaChallenge": {
+16 -10
View File
@@ -17008,9 +17008,15 @@
},
"adapter": {
"type": "string",
"description": "Framework adapter. Allows: static, ssr",
"description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr",
"default": "",
"x-example": "<ADAPTER>"
"x-example": "static",
"enum": [
"static",
"ssr"
],
"x-enum-name": null,
"x-enum-keys": []
},
"installationId": {
"type": "string",
@@ -17369,9 +17375,15 @@
},
"adapter": {
"type": "string",
"description": "Framework adapter. Usuallly allows: static, ssr",
"description": "Framework adapter defining rendering strategy. Allowed values are: static, ssr",
"default": "",
"x-example": "<ADAPTER>"
"x-example": "static",
"enum": [
"static",
"ssr"
],
"x-enum-name": null,
"x-enum-keys": []
},
"fallbackFile": {
"type": "string",
@@ -28054,11 +28066,6 @@
"x-example": 128,
"format": "int32"
},
"domain": {
"type": "string",
"description": "Preview domain.",
"x-example": "deploy1-project1.appwrite.site"
},
"providerRepositoryName": {
"type": "string",
"description": "The name of the vcs provider repository",
@@ -28127,7 +28134,6 @@
"status",
"buildLogs",
"buildTime",
"domain",
"providerRepositoryName",
"providerRepositoryOwner",
"providerRepositoryUrl",
+2 -1
View File
@@ -62,7 +62,8 @@ App::get('/v1/console/variables')
'_APP_DOMAIN_ENABLED' => $isDomainEnabled,
'_APP_ASSISTANT_ENABLED' => $isAssistantEnabled,
'_APP_DOMAIN_SITES' => System::getEnv('_APP_DOMAIN_SITES'),
'_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS')
'_APP_OPTIONS_FORCE_HTTPS' => System::getEnv('_APP_OPTIONS_FORCE_HTTPS'),
'_APP_DOMAINS_NAMESERVERS' => System::getEnv('_APP_DOMAINS_NAMESERVERS'),
]);
$response->dynamic($variables, Response::MODEL_CONSOLE_VARIABLES);
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -857,9 +857,10 @@ App::get('/v1/health/storage')
->inject('response')
->inject('deviceForFiles')
->inject('deviceForFunctions')
->inject('deviceForSites')
->inject('deviceForBuilds')
->action(function (Response $response, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds) {
$devices = [$deviceForFiles, $deviceForFunctions, $deviceForBuilds];
->action(function (Response $response, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForSites, Device $deviceForBuilds) {
$devices = [$deviceForFiles, $deviceForFunctions, $deviceForSites, $deviceForBuilds];
$checkStart = \microtime(true);
foreach ($devices as $device) {
+53 -4
View File
@@ -19,6 +19,7 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
@@ -244,10 +245,10 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
'activate' => $activate,
]));
// Preview deployments for sites
if ($resource->getCollection() === 'sites') {
$projectId = $project->getId();
// Deployment preview
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
@@ -257,13 +258,61 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'resourceType' => 'deployment',
'resourceId' => $deploymentId,
'resourceInternalId' => $deployment->getInternalId(),
'type' => 'deployment',
'value' => $deployment->getId(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
// VCS branch preview
if (!empty($providerBranch)) {
$domain = "branch-{$providerBranch}-{$resource->getId()}-{$project->getId()}.{$sitesDomain}";
$ruleId = md5($domain);
try {
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'type' => 'deployment',
'value' => $deployment->getId(),
'automation' => 'branch=' . $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
// VCS commit preview
if (!empty($providerCommitHash)) {
$domain = "commit-{$providerCommitHash}-{$resource->getId()}-{$project->getId()}.{$sitesDomain}";
$ruleId = md5($domain);
try {
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'type' => 'deployment',
'value' => $deployment->getId(),
'automation' => 'commit=' . $providerCommitHash,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
}
if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) {
+74 -110
View File
@@ -12,10 +12,7 @@ use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Validator\Origin;
use Appwrite\Platform\Appwrite;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Transformation\Adapter\Preview;
use Appwrite\Transformation\Transformation;
use Appwrite\Utopia\Request;
@@ -123,41 +120,9 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
return false;
}
$type = $rule->getAttribute('resourceType');
if ($type === 'function' || $type === 'site' || $type === 'deployment') {
$resourceCollection = match ($type) {
'function' => 'functions',
'site' => 'sites',
'deployment' => 'deployments',
};
}
if ($type === 'function' || $type === 'site' || $type === 'deployment') {
$method = $utopia->getRoute()?->getLabel('sdk', null);
if (empty($method)) {
$utopia->getRoute()?->label('sdk', new Method(
namespace: 'functions',
name: 'createExecution',
description: '/docs/references/functions/create-execution.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_EXECUTION,
)
],
contentType: ContentType::MULTIPART,
requestType: 'application/json',
));
} else {
/** @var Method $method */
$method->setNamespace('functions');
$method->setMethodName('createExecution');
$utopia->getRoute()?->label('sdk', $method);
}
$type = $rule->getAttribute('type', '');
if ($type === 'deployment') {
if (System::getEnv('_APP_OPTIONS_COMPUTE_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https' && $request->getHostname() !== APP_HOSTNAME_INTERNAL) {
if ($request->getMethod() !== Request::METHOD_GET) {
@@ -167,8 +132,22 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
}
$resourceId = $rule->getAttribute('resourceId');
$projectId = $rule->getAttribute('projectId');
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('value')));
if ($deployment->getAttribute('resourceType', '') === 'functions') {
$type = 'function';
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
$type = 'site';
}
$resource = $type === 'function' ?
Authorization::skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
Authorization::skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
$isPreview = $type === 'function' ? false : (!\str_starts_with($rule->getAttribute('automation', ''), 'site='));
$path = ($swooleRequest->server['request_uri'] ?? '/');
$query = ($swooleRequest->server['query_string'] ?? '');
@@ -181,30 +160,17 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$requestHeaders = $request->getHeaders();
$project = Authorization::skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
if ($resourceCollection === 'deployments') {
$subResource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resource = Authorization::skip(fn () => $dbForProject->getDocument($subResource->getAttribute('resourceType'), $subResource->getAttribute('resourceId')));
} else {
$resource = Authorization::skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
}
if ($resource->isEmpty() || !$resource->getAttribute('enabled')) {
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND);
}
if ($isResourceBlocked($project, RESOURCE_TYPE_FUNCTIONS, $resourceId)) {
if ($isResourceBlocked($project, $type === 'function' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES, $resource->getId())) {
throw new AppwriteException(AppwriteException::GENERAL_RESOURCE_BLOCKED);
}
$version = match ($type) {
'function' => $resource->getAttribute('version', 'v2'),
'site' => 'v4',
'deployment' => 'v4'
};
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
@@ -213,10 +179,10 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$runtime = match ($type) {
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null,
'deployment' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null,
default => null
};
// Static site enforced runtime
if ($resource->getAttribute('adapter', '') === 'static') {
$runtime = $runtimes['static-1'] ?? null;
}
@@ -225,22 +191,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported');
}
$deploymentId = match ($type) {
'function' => $resource->getAttribute('deployment', ''),
'site' => $resource->getAttribute('deploymentId', ''),
'deployment' => $subResource->getId()
};
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $deploymentId));
if ($deployment->getAttribute('resourceId') !== $resource->getId()) {
throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
}
if ($deployment->isEmpty()) {
throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
}
/** Check if build has completed */
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
@@ -251,10 +201,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
throw new AppwriteException(AppwriteException::BUILD_NOT_READY);
}
//todo: figure out for sites/functions
if ($type === 'function') {
$permissions = $resource->getAttribute('execute');
if (!(\in_array('any', $permissions)) && !(\in_array('guests', $permissions))) {
throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"');
}
@@ -266,18 +214,15 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
//todo: check if this would work for sites
if ($type === 'function') {
$jwtExpiry = $resource->getAttribute('timeout', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$jwtKey = $jwtObj->encode([
'projectId' => $project->getId(),
'scopes' => $resource->getAttribute('scopes', [])
]);
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-jwt'] = '';
}
$jwtExpiry = $resource->getAttribute('timeout', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$jwtKey = $jwtObj->encode([
'projectId' => $project->getId(),
'scopes' => $resource->getAttribute('scopes', [])
]);
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-jwt'] = '';
$ip = $headers['x-real-ip'] ?? '';
if (!empty($ip)) {
@@ -316,21 +261,26 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
'errors' => '',
'logs' => '',
'duration' => 0.0,
'search' => implode(' ', [$resourceId, $executionId]),
'search' => implode(' ', [$resource->getId(), $executionId]),
]);
if ($type === 'function') {
$execution->setAttribute('resourceType', 'functions');
$execution->setAttribute('trigger', 'http'); // http / schedule / event
$execution->setAttribute('status', 'processing'); // waiting / processing / completed / failed
$queueForEvents
->setParam('functionId', $resource->getId())
->setParam('executionId', $execution->getId())
->setContext('function', $resource);
} elseif ($type === 'site') {
$execution->setAttribute('resourceType', 'sites');
}
$queueForEvents
->setParam('functionId', $resource->getId())
->setParam('executionId', $execution->getId())
->setContext('function', $resource);
$queueForEvents
->setParam('siteId', $resource->getId())
->setParam('executionId', $execution->getId())
->setContext('site', $resource);
}
$durationStart = \microtime(true);
@@ -363,7 +313,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
// Appwrite vars
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_ID' => $resourceId,
'APPWRITE_FUNCTION_ID' => $resource->getId(),
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
@@ -388,18 +338,21 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''),
]);
// SPA fallbackFile override
if ($resource->getAttribute('adapter', '') === 'static' && $resource->getAttribute('fallbackFile', '') !== '') {
$vars['OPEN_RUNTIMES_STATIC_FALLBACK'] = $resource->getAttribute('fallbackFile', '');
}
/** Execute function */
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
try {
$version = match ($type) {
'function' => $resource->getAttribute('version', 'v2'),
'site' => 'v4',
'deployment' => 'v4'
};
$entrypoint = match ($type) {
'function' => $deployment->getAttribute('entrypoint', ''),
'site' => '',
'deployment' => ''
};
if ($type === 'function') {
@@ -407,7 +360,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
'v2' => '',
default => 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $runtime['startCommand'] . '"'
};
} elseif ($type === 'site' || $type === 'deployment') {
} elseif ($type === 'site') {
$frameworks = Config::getParam('frameworks', []);
$framework = $frameworks[$resource->getAttribute('framework', '')] ?? null;
@@ -426,7 +379,6 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$entrypoint = match ($type) {
'function' => $deployment->getAttribute('entrypoint', ''),
'site' => '',
'deployment' => ''
};
$executionResponse = $executor->createExecution(
@@ -449,13 +401,20 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
requestTimeout: 30
);
// Branded 404 override
if ($executionResponse['statusCode'] === 404 && $resource->getAttribute('adapter', '') === 'static') {
$layout = new View(__DIR__ . '/../views/general/404.phtml');
$executionResponse['body'] = $layout->render();
$executionResponse['headers']['content-length'] = \strlen($executionResponse['body']);
}
// Branded banner for previews
if (\is_null($apiKey) || $apiKey->isBannerDisabled() === false) {
$transformation = new Transformation();
$transformation->addAdapter(new Preview());
$transformation->setInput($executionResponse['body']);
$transformation->setTraits($executionResponse['headers']);
if ($type === 'deployment' && $transformation->transform()) {
if ($isPreview && $transformation->transform()) {
$executionResponse['body'] = $transformation->getOutput();
foreach ($executionResponse['headers'] as $key => $value) {
@@ -475,9 +434,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
/** Update execution status */
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
if ($type === 'function') {
$execution->setAttribute('status', $status);
}
$execution->setAttribute('status', $status);
$execution->setAttribute('logs', $executionResponse['logs']);
$execution->setAttribute('errors', $executionResponse['errors']);
$execution->setAttribute('responseStatusCode', $executionResponse['statusCode']);
@@ -566,6 +523,17 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
} elseif ($type === 'api') {
$utopia->getRoute()?->label('error', '');
return false;
} elseif ($type === 'redirect') {
$path = ($swooleRequest->server['request_uri'] ?? '/');
$query = ($swooleRequest->server['query_string'] ?? '');
if (!empty($query)) {
$path .= '?' . $query;
}
$url = 'https://' . $rule->getAttribute('value', '') . $path;
$response->redirect($url);
return true;
} else {
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type);
}
@@ -698,14 +666,16 @@ App::init()
}
if ($domainDocument->isEmpty()) {
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$domainDocument = new Document([
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
'$id' => System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(),
'$id' => $ruleId,
'domain' => $domain->get(),
'resourceType' => 'api',
'type' => 'api',
'status' => 'verifying',
'projectId' => 'console',
'projectInternalId' => 'console'
'projectInternalId' => 'console',
'search' => implode(' ', [$ruleId, $domain->get()]),
]);
$domainDocument = $dbForPlatform->createDocument('rules', $domainDocument);
@@ -742,7 +712,7 @@ App::init()
} elseif (!empty($origin)) {
// Auto-allow domains with linked rule
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin)));
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? '')));
} else {
$rule = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
@@ -1311,13 +1281,7 @@ App::get('/v1/ping')
App::wildcard()
->groups(['api'])
->label('scope', 'global')
->inject('utopia')
->action(function (App $utopia) {
$handeledByRouter = $utopia->getRoute()?->getLabel('router', false);
if ($handeledByRouter === true) {
return;
}
->action(function () {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
});
+6 -12
View File
@@ -303,9 +303,9 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
$dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
}
if ($dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty()) {
if (Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty())) {
Console::info(" └── Creating screenshots bucket...");
$dbForPlatform->createDocument('buckets', new Document([
Authorization::skip(fn () => $dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('screenshots'),
'$collection' => ID::custom('buckets'),
'name' => 'Screenshots',
@@ -316,16 +316,11 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'encryption' => false,
'antivirus' => false,
'fileSecurity' => true,
'$permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'$permissions' => [],
'search' => 'buckets Screenshots',
]));
])));
$bucket = $dbForPlatform->getDocument('buckets', 'screenshots');
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
Console::info(" └── Creating files collection for screenshots bucket...");
$files = $collections['buckets']['files'] ?? [];
@@ -353,7 +348,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
'orders' => $index['orders'],
]), $files['indexes']);
$dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
Authorization::skip(fn () => $dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes));
}
});
@@ -519,7 +514,6 @@ $http->on('Task', function () use ($register, $domains) {
if ($lastSyncUpdate != null) {
$queries[] = Query::greaterThanEqual('$updatedAt', $lastSyncUpdate);
}
$queries[] = Query::equal('resourceType', ['function']);
$results = [];
try {
$results = Authorization::skip(fn () => $dbForPlatform->find('rules', $queries));
+185
View File
@@ -0,0 +1,185 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 Not Found</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
background-color: #FFFFFF;
}
.main {
display: flex;
min-height: 100vh;
width: 100vw;
align-items: center;
justify-content: center;
}
.content {
margin-left: auto;
margin-right: auto;
max-width: 400px;
}
span {
padding: var(--space-1, 2px) var(--space-3, 6px);
border-radius: var(--border-radius-XS, 6px);
background: var(--color-overlay-on-neutral, rgba(0, 0, 0, 0.06));
color: var(--color-fgColor-neutral-secondary, #56565C);
text-align: center;
font-family: var(--font-family-sansSerif, Inter);
font-size: var(--font-size-S, 14px);
font-style: normal;
font-weight: 400;
line-height: 140%;
letter-spacing: -0.063px;
}
h1 {
color: var(--color-fgColor-neutral-primary, #2D2D31);
text-align: center;
font-family: var(--font-family-sansSerif, Inter);
font-size: var(--font-size-XXXL, 32px);
font-style: normal;
font-weight: 400;
line-height: 140%;
letter-spacing: -0.144px;
margin-top: 8px;
margin-bottom: 32px;
}
button {
border-radius: var(--border-radius-S, 8px);
font-family: var(--font-family-sansSerif, Inter);
font-size: var(--font-size-S, 14px);
font-style: normal;
font-weight: 500;
line-height: 140%;
letter-spacing: -0.063px;
padding: var(--space-3, 6px) var(--space-5, 10px);
cursor: pointer;
border: var(--border-width-S, 1px) solid var(--color-border-neutral-strong, #D8D8DB);
background: var(--color-bgColor-neutral-primary, #FFF);
color: var(--color-fgColor-neutral-secondary, #56565C);
}
.center {
display: flex;
justify-content: center;
}
.brand {
position: absolute;
width: 100%;
bottom: 32px;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.brand p {
font-family: var(--font-family-monospace, "Aeonik Fono");
font-size: var(--font-size-XS, 12px);
font-style: normal;
font-weight: 400;
line-height: 130%;
letter-spacing: 0.96px;
text-transform: uppercase;
color: var(--color-fgColor-neutral-secondary, #56565C);
}
.brand svg {
height: 20px;
}
.logo-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #1D1D21;
}
h1 {
color: var(--color-fgColor-neutral-primary, #EDEDF0);
}
button {
border: var(--border-width-S, 1px) solid var(--color-border-neutral-strong, #414146);
background: var(--color-bgColor-neutral-primary, #1D1D21);
color: var(--color-fgColor-neutral-secondary, #C3C3C6);
}
.brand p {
color: var(--color-fgColor-neutral-secondary, #C3C3C6);
}
span {
background: var(--color-overlay-on-neutral, rgba(255, 255, 255, 0.20));
color: var(--color-fgColor-neutral-secondary, #C3C3C6);
}
.logo-light {
display: none;
}
.logo-dark {
display: block;
}
}
</style>
</head>
<body>
<div class="main">
<div class="content">
<div class="center"><span>Page not found</span></div>
<h1>The page youre looking for doesnt exist.</h1>
<div class="center"><a href="/"><button>Go to homepage</button></a></div>
</div>
</div>
<div class="brand">
<p>Powered by</p>
<svg class="logo-dark" width="110" height="20" viewBox="0 0 110 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.8649 16.2461C33.6492 16.2461 34.5511 15.3184 34.9433 14.6867H35.1197C35.1981 15.3578 35.6687 15.9895 36.5903 15.9895H38.3353V14.0156H37.8843C37.5706 14.0156 37.4138 13.838 37.4138 13.5617V5.64661H35.1001V6.90986H34.9236C34.4727 6.27823 33.5315 5.39001 31.8061 5.39001C29.0611 5.39001 27.022 7.67965 27.022 10.818C27.022 13.9564 29.1003 16.2461 31.8649 16.2461ZM32.2767 13.9959C30.6493 13.9959 29.3748 12.7919 29.3748 10.8378C29.3748 8.92316 30.6101 7.62044 32.2571 7.62044C33.8256 7.62044 35.1393 8.78499 35.1393 10.8378C35.1393 12.5945 34.0217 13.9959 32.2767 13.9959Z" fill="#EDEDF0" />
<path d="M39.7013 20H42.0149V14.6867H42.1914C42.6227 15.3184 43.5443 16.2461 45.3677 16.2461C48.1127 16.2461 50.1127 13.9169 50.1127 10.818C50.1127 7.69939 47.9755 5.39001 45.2109 5.39001C43.4462 5.39001 42.5835 6.35719 42.1718 6.89012H41.9953V5.64661H39.7013V20ZM44.8776 14.0551C43.2894 14.0551 41.9757 12.8708 41.9757 10.818C41.9757 9.06133 43.0933 7.58096 44.8383 7.58096C46.4657 7.58096 47.7402 8.86395 47.7402 10.818C47.7402 12.7326 46.5049 14.0551 44.8776 14.0551Z" fill="#EDEDF0" />
<path d="M51.3065 20H53.6202V14.6867H53.7966C54.228 15.3184 55.1495 16.2461 56.973 16.2461C59.718 16.2461 61.5273 13.9169 61.5273 10.818C61.5273 7.69939 59.5807 5.39001 56.8161 5.39001C55.0515 5.39001 54.1888 6.35719 53.777 6.89012H53.6005V5.64661H51.3065V20ZM56.4828 14.0551C54.8946 14.0551 53.5809 12.8708 53.5809 10.818C53.5809 9.06133 54.6985 7.58096 56.4436 7.58096C58.071 7.58096 59.3454 8.86395 59.3454 10.818C59.3454 12.7326 58.1102 14.0551 56.4828 14.0551Z" fill="#EDEDF0" />
<path d="M64.5857 16.2296H67.8601L69.7227 8.11721H69.8404L71.7031 16.2296H74.9579L77.5642 5.88678H75.2323L73.3697 14.0189H73.1932L71.3305 5.88678H68.2522L66.3699 14.0189H66.1935L64.3504 5.88678H61.8799L64.5857 16.2296Z" fill="#EDEDF0" />
<path d="M78.7363 16.2296H81.0499V11.1174C81.0499 9.16334 81.9519 7.9593 83.6381 7.9593H84.6576V5.63019H83.893C82.5793 5.63019 81.5793 6.53815 81.1872 7.40663H81.0303V5.88678H78.7363V16.2296Z" fill="#EDEDF0" />
<path d="M96.1391 16.2296H97.943V14.1571H96.1587C95.4529 14.1571 95.1588 13.8413 95.1588 13.111V7.93956H98.0606V5.88678H95.1588V2.98526H92.9628V5.88678H91.0413V7.93956H92.8255V13.1307C92.8255 15.3217 94.1392 16.2296 96.1391 16.2296Z" fill="#EDEDF0" />
<path d="M104.15 16.2461C106.287 16.2461 108.17 15.1802 108.836 13.0287L106.719 12.5155C106.346 13.6603 105.268 14.2525 104.13 14.2525C102.444 14.2525 101.327 13.1472 101.307 11.4102H109.091V10.7588C109.091 7.67965 107.189 5.39001 104.052 5.39001C101.287 5.39001 98.915 7.58096 98.915 10.8378C98.915 13.9959 101.013 16.2461 104.15 16.2461ZM101.327 9.71269C101.464 8.46918 102.581 7.42305 104.052 7.42305C105.464 7.42305 106.621 8.31128 106.738 9.71269H101.327Z" fill="#EDEDF0" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M90.0125 16.2296H87.6989V7.93956H85.895V5.88678H90.0125V16.2296Z" fill="#EDEDF0" />
<path d="M88.6834 4.45145C89.5265 4.45145 90.154 3.81983 90.154 2.99082C90.154 2.18155 89.5265 1.54993 88.6834 1.54993C87.8403 1.54993 87.2129 2.18155 87.2129 2.99082C87.2129 3.81983 87.8403 4.45145 88.6834 4.45145Z" fill="#EDEDF0" />
<path d="M20.2007 13.6935V18.258H8.88588C5.5894 18.258 2.71111 16.4222 1.17116 13.6935C0.947288 13.2968 0.751353 12.8806 0.586995 12.4486C0.26435 11.6021 0.0615332 10.6938 0 9.74603V8.51195C0.0133592 8.30074 0.03441 8.09119 0.0619381 7.88413C0.118209 7.45921 0.203222 7.04343 0.314953 6.63926C1.37195 2.80758 4.8089 0 8.88588 0C12.9629 0 16.3994 2.80758 17.4564 6.63926H12.6184C11.8241 5.39025 10.4493 4.5645 8.88588 4.5645C7.32245 4.5645 5.94767 5.39025 5.15341 6.63926C4.91132 7.01895 4.72349 7.43764 4.60042 7.88413C4.49112 8.27999 4.43282 8.69744 4.43282 9.12899C4.43282 10.4373 4.96962 11.6166 5.83027 12.4486C6.62778 13.2209 7.70299 13.6935 8.88588 13.6935H20.2007Z" fill="#FD366E" />
<path d="M20.2006 7.88412V12.4486H11.9414C12.8021 11.6166 13.3389 10.4373 13.3389 9.12899C13.3389 8.69744 13.2806 8.27999 13.1713 7.88412H20.2006Z" fill="#FD366E" />
</svg>
<svg class="logo-light" width="110" height="20" viewBox="0 0 110 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.8648 16.2461C33.649 16.2461 34.5509 15.3184 34.9431 14.6867H35.1195C35.198 15.3578 35.6685 15.9895 36.5901 15.9895H38.3351V14.0156H37.8841C37.5704 14.0156 37.4136 13.838 37.4136 13.5617V5.64661H35.0999V6.90986H34.9235C34.4725 6.27823 33.5314 5.39001 31.8059 5.39001C29.0609 5.39001 27.0218 7.67965 27.0218 10.818C27.0218 13.9564 29.1001 16.2461 31.8648 16.2461ZM32.2765 13.9959C30.6491 13.9959 29.3746 12.7919 29.3746 10.8378C29.3746 8.92316 30.6099 7.62044 32.2569 7.62044C33.8255 7.62044 35.1391 8.78499 35.1391 10.8378C35.1391 12.5945 34.0215 13.9959 32.2765 13.9959Z" fill="#2D2D31" />
<path d="M39.7011 20H42.0147V14.6867H42.1912C42.6226 15.3184 43.5441 16.2461 45.3676 16.2461C48.1126 16.2461 50.1125 13.9169 50.1125 10.818C50.1125 7.69939 47.9753 5.39001 45.2107 5.39001C43.4461 5.39001 42.5833 6.35719 42.1716 6.89012H41.9951V5.64661H39.7011V20ZM44.8774 14.0551C43.2892 14.0551 41.9755 12.8708 41.9755 10.818C41.9755 9.06133 43.0931 7.58096 44.8382 7.58096C46.4656 7.58096 47.74 8.86395 47.74 10.818C47.74 12.7326 46.5048 14.0551 44.8774 14.0551Z" fill="#2D2D31" />
<path d="M51.3063 20H53.62V14.6867H53.7964C54.2278 15.3184 55.1493 16.2461 56.9728 16.2461C59.7178 16.2461 61.5271 13.9169 61.5271 10.818C61.5271 7.69939 59.5805 5.39001 56.8159 5.39001C55.0513 5.39001 54.1886 6.35719 53.7768 6.89012H53.6004V5.64661H51.3063V20ZM56.4826 14.0551C54.8944 14.0551 53.5808 12.8708 53.5808 10.818C53.5808 9.06133 54.6984 7.58096 56.4434 7.58096C58.0708 7.58096 59.3453 8.86395 59.3453 10.818C59.3453 12.7326 58.11 14.0551 56.4826 14.0551Z" fill="#2D2D31" />
<path d="M64.5855 16.2296H67.8599L69.7226 8.11721H69.8402L71.7029 16.2296H74.9577L77.564 5.88678H75.2322L73.3695 14.0189H73.193L71.3303 5.88678H68.252L66.3697 14.0189H66.1933L64.3502 5.88678H61.8797L64.5855 16.2296Z" fill="#2D2D31" />
<path d="M78.7361 16.2296H81.0498V11.1174C81.0498 9.16334 81.9517 7.9593 83.6379 7.9593H84.6575V5.63019H83.8928C82.5791 5.63019 81.5791 6.53815 81.187 7.40663H81.0301V5.88678H78.7361V16.2296Z" fill="#2D2D31" />
<path d="M96.1389 16.2296H97.9428V14.1571H96.1585C95.4527 14.1571 95.1586 13.8413 95.1586 13.111V7.93956H98.0604V5.88678H95.1586V2.98526H92.9626V5.88678H91.0411V7.93956H92.8253V13.1307C92.8253 15.3217 94.139 16.2296 96.1389 16.2296Z" fill="#2D2D31" />
<path d="M104.15 16.2461C106.287 16.2461 108.169 15.1802 108.836 13.0287L106.718 12.5155C106.346 13.6603 105.268 14.2525 104.13 14.2525C102.444 14.2525 101.326 13.1472 101.307 11.4102H109.091V10.7588C109.091 7.67965 107.189 5.39001 104.052 5.39001C101.287 5.39001 98.9148 7.58096 98.9148 10.8378C98.9148 13.9959 101.013 16.2461 104.15 16.2461ZM101.326 9.71269C101.464 8.46918 102.581 7.42305 104.052 7.42305C105.464 7.42305 106.62 8.31128 106.738 9.71269H101.326Z" fill="#2D2D31" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M90.0123 16.2296H87.6987V7.93956H85.8948V5.88678H90.0123V16.2296Z" fill="#2D2D31" />
<path d="M88.6835 4.45145C89.5266 4.45145 90.154 3.81983 90.154 2.99082C90.154 2.18155 89.5266 1.54993 88.6835 1.54993C87.8404 1.54993 87.213 2.18155 87.213 2.99082C87.213 3.81983 87.8404 4.45145 88.6835 4.45145Z" fill="#2D2D31" />
<path d="M20.2007 13.6935V18.258H8.88588C5.5894 18.258 2.71111 16.4222 1.17116 13.6935C0.947288 13.2968 0.751353 12.8806 0.586995 12.4486C0.26435 11.6021 0.0615332 10.6938 0 9.74603V8.51195C0.0133592 8.30074 0.03441 8.09119 0.0619381 7.88413C0.118209 7.45921 0.203222 7.04343 0.314953 6.63926C1.37195 2.80758 4.8089 0 8.88588 0C12.9629 0 16.3994 2.80758 17.4564 6.63926H12.6184C11.8241 5.39025 10.4493 4.5645 8.88588 4.5645C7.32245 4.5645 5.94767 5.39025 5.15341 6.63926C4.91132 7.01895 4.72349 7.43764 4.60042 7.88413C4.49112 8.27999 4.43282 8.69744 4.43282 9.12899C4.43282 10.4373 4.96962 11.6166 5.83027 12.4486C6.62778 13.2209 7.70299 13.6935 8.88588 13.6935H20.2007Z" fill="#FD366E" />
<path d="M20.2007 7.88412V12.4486H11.9415C12.8022 11.6166 13.339 10.4373 13.339 9.12899C13.339 8.69744 13.2807 8.27999 13.1714 7.88412H20.2007Z" fill="#FD366E" />
</svg>
</div>
</body>
</html>
+1 -1
View File
@@ -56,7 +56,7 @@
"utopia-php/detector": "dev-feat-pseudocode-draft2 as 0.1.99",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "dev-fix-prevent-duplicate-compression as 0.33.99",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.3.*",
"utopia-php/image": "0.7.*",
"utopia-php/locale": "0.4.*",
Generated
+33 -40
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "81769c65caf560ea69f2c13fc8b04e7d",
"content-hash": "6d3952c126526006ed5a7d5a4741b755",
"packages": [
{
"name": "adhocore/jwt",
@@ -279,16 +279,16 @@
},
{
"name": "brick/math",
"version": "0.12.1",
"version": "0.12.2",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
"reference": "f510c0a40911935b77b86859eb5223d58d660df1"
"reference": "901eddb1e45a8e0f689302e40af871c181ecbe40"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1",
"reference": "f510c0a40911935b77b86859eb5223d58d660df1",
"url": "https://api.github.com/repos/brick/math/zipball/901eddb1e45a8e0f689302e40af871c181ecbe40",
"reference": "901eddb1e45a8e0f689302e40af871c181ecbe40",
"shasum": ""
},
"require": {
@@ -297,7 +297,7 @@
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^10.1",
"vimeo/psalm": "5.16.0"
"vimeo/psalm": "6.8.8"
},
"type": "library",
"autoload": {
@@ -327,7 +327,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.12.1"
"source": "https://github.com/brick/math/tree/0.12.2"
},
"funding": [
{
@@ -335,7 +335,7 @@
"type": "github"
}
],
"time": "2023-11-29T23:19:16+00:00"
"time": "2025-02-26T10:21:45+00:00"
},
{
"name": "chillerlan/php-qrcode",
@@ -2694,16 +2694,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.2.3",
"version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "7ce6078c79a4a7afff931c413d2959d3bffbfb8d"
"reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/7ce6078c79a4a7afff931c413d2959d3bffbfb8d",
"reference": "7ce6078c79a4a7afff931c413d2959d3bffbfb8d",
"url": "https://api.github.com/repos/symfony/http-client/zipball/78981a2ffef6437ed92d4d7e2a86a82f256c6dc6",
"reference": "78981a2ffef6437ed92d4d7e2a86a82f256c6dc6",
"shasum": ""
},
"require": {
@@ -2769,7 +2769,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.2.3"
"source": "https://github.com/symfony/http-client/tree/v7.2.4"
},
"funding": [
{
@@ -2785,7 +2785,7 @@
"type": "tidelift"
}
],
"time": "2025-01-28T15:51:35+00:00"
"time": "2025-02-13T10:27:23+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -3982,16 +3982,16 @@
},
{
"name": "utopia-php/framework",
"version": "dev-fix-prevent-duplicate-compression",
"version": "0.33.17",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "a1efe3e10038afe4109af833ce7a25a8ec4b5ed2"
"reference": "73fac6fbce9f56282dba4e52a58cf836ec434644"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/a1efe3e10038afe4109af833ce7a25a8ec4b5ed2",
"reference": "a1efe3e10038afe4109af833ce7a25a8ec4b5ed2",
"url": "https://api.github.com/repos/utopia-php/http/zipball/73fac6fbce9f56282dba4e52a58cf836ec434644",
"reference": "73fac6fbce9f56282dba4e52a58cf836ec434644",
"shasum": ""
},
"require": {
@@ -4023,9 +4023,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/fix-prevent-duplicate-compression"
"source": "https://github.com/utopia-php/http/tree/0.33.17"
},
"time": "2025-02-03T12:02:35+00:00"
"time": "2025-02-24T17:35:48+00:00"
},
{
"name": "utopia-php/image",
@@ -5114,16 +5114,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.40.0",
"version": "0.40.1",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "d2880132c900f64108d3e4484a6c1ed1bed2303c"
"reference": "df180676b6fbde7832ae1495af3e2f3e8f700837"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/d2880132c900f64108d3e4484a6c1ed1bed2303c",
"reference": "d2880132c900f64108d3e4484a6c1ed1bed2303c",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/df180676b6fbde7832ae1495af3e2f3e8f700837",
"reference": "df180676b6fbde7832ae1495af3e2f3e8f700837",
"shasum": ""
},
"require": {
@@ -5159,9 +5159,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.40.0"
"source": "https://github.com/appwrite/sdk-generator/tree/0.40.1"
},
"time": "2025-02-04T12:47:33+00:00"
"time": "2025-02-26T07:07:10+00:00"
},
{
"name": "doctrine/annotations",
@@ -8493,16 +8493,16 @@
},
{
"name": "symfony/process",
"version": "v7.2.0",
"version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e"
"reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e",
"reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e",
"url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
"reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
"shasum": ""
},
"require": {
@@ -8534,7 +8534,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.2.0"
"source": "https://github.com/symfony/process/tree/v7.2.4"
},
"funding": [
{
@@ -8550,7 +8550,7 @@
"type": "tidelift"
}
],
"time": "2024-11-06T14:24:19+00:00"
"time": "2025-02-05T08:33:46+00:00"
},
{
"name": "symfony/string",
@@ -8873,18 +8873,11 @@
"version": "dev-feat-pseudocode-draft2",
"alias": "0.1.99",
"alias_normalized": "0.1.99.0"
},
{
"package": "utopia-php/framework",
"version": "dev-fix-prevent-duplicate-compression",
"alias": "0.33.99",
"alias_normalized": "0.33.99.0"
}
],
"minimum-stability": "stable",
"stability-flags": {
"utopia-php/detector": 20,
"utopia-php/framework": 20
"utopia-php/detector": 20
},
"prefer-stable": false,
"prefer-lowest": false,
+6 -1
View File
@@ -76,6 +76,7 @@ services:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- ./phpunit.xml:/usr/src/code/phpunit.xml
- ./tests:/usr/src/code/tests
@@ -205,7 +206,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:5.3.0-sites-rc.13
image: appwrite/console:5.3.0-sites-rc.15
restart: unless-stopped
networks:
- appwrite
@@ -348,6 +349,7 @@ services:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- appwrite-certificates:/storage/certificates:rw
- ./app:/usr/src/code/app
@@ -434,6 +436,7 @@ services:
- appwrite
volumes:
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
@@ -968,6 +971,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- appwrite-builds:/storage/builds:rw
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
# Host mount nessessary to share files between executor and runtimes.
# It's not possible to share mount file between 2 containers without host mount (copying is too slow)
- /tmp:/tmp:rw
@@ -1139,5 +1143,6 @@ volumes:
appwrite-uploads:
appwrite-certificates:
appwrite-functions:
appwrite-sites:
appwrite-builds:
appwrite-config:
@@ -181,11 +181,11 @@ class Base extends Action
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'resourceType' => 'deployment',
'resourceId' => $deploymentId,
'resourceInternalId' => $deployment->getInternalId(),
'type' => 'deployment',
'value' => $deployment->getId(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
@@ -23,7 +23,7 @@ class Get extends Action
public static function getName()
{
return 'getResources';
return 'getResource';
}
public function __construct()
@@ -0,0 +1,112 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Builds;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createDeploymentBuild';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build')
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
->desc('Rebuild deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
->label('audits.event', 'deployment.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'createBuild',
description: <<<EOT
Create a new build for an existing function deployment. This endpoint allows you to rebuild a deployment with the updated function configuration, including its entrypoint and build commands if they have been modified. The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('deviceForFunctions')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, string $buildId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$path = $deployment->getAttribute('path');
if (empty($path) || !$deviceForFunctions->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$deploymentId = ID::unique();
$destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$deviceForFunctions->transfer($path, $destination, $deviceForFunctions);
$deployment->removeAttribute('$internalId');
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
'$internalId' => '',
'$id' => $deploymentId,
'buildId' => '',
'buildInternalId' => '',
'path' => $destination,
'entrypoint' => $function->getAttribute('entrypoint'),
'commands' => $function->getAttribute('commands', ''),
'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]),
]));
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setDeployment($deployment);
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->noContent();
}
}
@@ -0,0 +1,136 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Builds;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateDeploymentBuild';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build')
->desc('Cancel deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'deployment.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'updateDeploymentBuild',
description: <<<EOT
Cancel an ongoing function deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_BUILD,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
$buildId = ID::unique();
$build = $dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$permissions' => [],
'startTime' => DateTime::now(),
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'status' => 'canceled',
'path' => '',
'runtime' => $function->getAttribute('runtime'),
'source' => $deployment->getAttribute('path', ''),
'sourceType' => '',
'logs' => '',
'duration' => 0,
'size' => 0
]));
$deployment->setAttribute('buildId', $build->getId());
$deployment->setAttribute('buildInternalId', $build->getInternalId());
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
} else {
if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) {
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
}
$startTime = new \DateTime($build->getAttribute('startTime'));
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([
'endTime' => DateTime::now(),
'duration' => $duration,
'status' => 'canceled'
]));
}
$dbForProject->purgeCachedDocument('deployments', $deployment->getId());
try {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
$executor->deleteRuntime($project->getId(), $deploymentId . "-build");
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($build, Response::MODEL_BUILD);
}
}
@@ -0,0 +1,109 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
class Delete extends Action
{
use HTTP;
public static function getName()
{
return 'deleteDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId')
->desc('Delete deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].deployments.[deploymentId].delete')
->label('audits.event', 'deployment.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'deleteDeployment',
description: <<<EOT
Delete a code deployment by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('deviceForFunctions')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Device $deviceForFunctions)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('deployments', $deployment->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from DB');
}
if (!empty($deployment->getAttribute('path', ''))) {
if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage');
}
}
if ($function->getAttribute('deployment') === $deployment->getId()) { // Reset function deployment
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'deployment' => '',
'deploymentInternalId' => '',
])));
}
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($deployment);
$response->noContent();
}
}
@@ -0,0 +1,129 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Download;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getDeploymentDownload';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/download')
->groups(['api', 'functions'])
->desc('Download deployment')
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'getDeploymentDownload',
description: <<<EOT
Get a function deployment content by its unique ID. The endpoint response return with a 'Content-Disposition: attachment' header that tells the browser to start downloading the file to user downloads directory.
EOT,
auth: [AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::ANY,
type: MethodType::LOCATION
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('deviceForFunctions')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$path = $deployment->getAttribute('path', '');
if (!$deviceForFunctions->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$response
->setContentType('application/gzip')
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->addHeader('X-Peak', \memory_get_peak_usage())
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"');
$size = $deviceForFunctions->getFileSize($path);
$rangeHeader = $request->getHeader('range');
if (!empty($rangeHeader)) {
$start = $request->getRangeStart();
$end = $request->getRangeEnd();
$unit = $request->getRangeUnit();
if ($end === null) {
$end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1));
}
if ($unit !== 'bytes' || $start >= $end || $end >= $size) {
throw new Exception(Exception::STORAGE_INVALID_RANGE);
}
$response
->addHeader('Accept-Ranges', 'bytes')
->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size)
->addHeader('Content-Length', $end - $start + 1)
->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT);
$response->send($deviceForFunctions->read($path, $start, ($end - $start + 1)));
}
if ($size > APP_STORAGE_READ_BUFFER) {
for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) {
$response->chunk(
$deviceForFunctions->read(
$path,
($i * MAX_OUTPUT_CHUNK_SIZE),
min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE))
),
(($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size
);
}
} else {
$response->send($deviceForFunctions->read($path));
}
}
}
@@ -0,0 +1,81 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId')
->desc('Get deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'getDeployment',
description: <<<EOT
Get a function deployment by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEPLOYMENT,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
$deployment->setAttribute('status', $build->getAttribute('status', 'waiting'));
$deployment->setAttribute('buildLogs', $build->getAttribute('logs', ''));
$deployment->setAttribute('buildTime', $build->getAttribute('duration', 0));
$deployment->setAttribute('buildSize', $build->getAttribute('size', 0));
$deployment->setAttribute('size', $deployment->getAttribute('size', 0));
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -0,0 +1,103 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateDeployment';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId')
->desc('Update deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
->label('audits.event', 'deployment.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'updateDeployment',
description: <<<EOT
Update the function active deployment. Use this endpoint to switch the code deployment that should be used when visitor opens your function.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_FUNCTION,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Database $dbForPlatform)
{
$function = $dbForProject->getDocument('functions', $functionId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
if ($build->getAttribute('status') !== 'ready') {
throw new Exception(Exception::BUILD_NOT_READY);
}
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [
'deploymentInternalId' => $deployment->getInternalId(),
'deployment' => $deployment->getId(),
])));
// Inform scheduler if function is still active
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($function, Response::MODEL_FUNCTION);
}
}
@@ -0,0 +1,127 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Deployments;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listDeployments';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/deployments')
->desc('List deployments')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'listDeployments',
description: <<<EOT
Get a list of all the function's code deployments. You can use the query params to filter your results.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DEPLOYMENT_LIST,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Deployments(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, array $queries, string $search, Response $response, Database $dbForProject)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
// Set resource queries
$queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]);
$queries[] = Query::equal('resourceType', ['functions']);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$deploymentId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('deployments', $deploymentId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Deployment '{$deploymentId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForProject->find('deployments', $queries);
$total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT);
foreach ($results as $result) {
$build = $dbForProject->getDocument('builds', $result->getAttribute('buildId', ''));
$result->setAttribute('status', $build->getAttribute('status', 'processing'));
$result->setAttribute('buildLogs', $build->getAttribute('logs', ''));
$result->setAttribute('buildTime', $build->getAttribute('duration', 0));
$result->setAttribute('buildSize', $build->getAttribute('size', 0));
$result->setAttribute('size', $result->getAttribute('size', 0));
}
$response->dynamic(new Document([
'deployments' => $results,
'total' => $total,
]), Response::MODEL_DEPLOYMENT_LIST);
}
}
@@ -0,0 +1,474 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Event\Build;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\Validator\Headers;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Tasks\ScheduleExecutions;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Executor\Executor;
use MaxMind\Db\Reader;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Swoole\Request;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createExecution';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/executions')
->desc('Create execution')
->groups(['api', 'functions'])
->label('scope', 'execution.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].executions.[executionId].create')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'createExecution',
description: <<<EOT
Trigger a function execution. The returned object will return you the current execution status. You can ping the `Get Execution` endpoint to get updates on the current execution status. Once this endpoint is called, your function execution process will start asynchronously.
EOT,
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_EXECUTION,
)
],
contentType: ContentType::MULTIPART,
requestType: 'application/json',
))
->param('functionId', '', new UID(), 'Function ID.')
->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true)
->param('async', false, new Boolean(true), 'Execute code in the background. Default value is false.', true)
->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true)
->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'HTTP method of execution. Default value is GET.', true)
->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true)
->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_MINUTES, offset: 60), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true)
->inject('response')
->inject('request')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('user')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('geodb')
->callback([$this, 'action']);
}
public function action(string $functionId, string $body, mixed $async, string $path, string $method, mixed $headers, ?string $scheduledAt, Response $response, Request $request, Document $project, Database $dbForProject, Database $dbForPlatform, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb)
{
$async = \strval($async) === 'true' || \strval($async) === '1';
if (!$async && !is_null($scheduledAt)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Scheduled executions must run asynchronously. Set scheduledAt to a future date, or set async to true.');
}
/**
* @var array<string, mixed> $headers
*/
$assocParams = ['headers'];
foreach ($assocParams as $assocParam) {
if (!empty('headers') && !is_array($$assocParam)) {
$$assocParam = \json_decode($$assocParam, true);
}
}
$booleanParams = ['async'];
foreach ($booleanParams as $booleamParam) {
if (!empty($$booleamParam) && !is_bool($$booleamParam)) {
$$booleamParam = $$booleamParam === "true" ? true : false;
}
}
// 'headers' validator
$validator = new Headers();
if (!$validator->isValid($headers)) {
throw new Exception($validator->getDescription(), 400);
}
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$version = $function->getAttribute('version', 'v2');
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('runtime-specifications')[$function->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null;
if (\is_null($runtime)) {
throw new Exception(Exception::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported');
}
$deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', '')));
if ($deployment->getAttribute('resourceId') !== $function->getId()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
}
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function');
}
/** Check if build has completed */
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
if ($build->getAttribute('status') !== 'ready') {
throw new Exception(Exception::BUILD_NOT_READY);
}
$validator = new Authorization('execute');
if (!$validator->isValid($function->getAttribute('execute'))) { // Check if user has write access to execute function
throw new Exception(Exception::USER_UNAUTHORIZED, $validator->getDescription());
}
$jwt = ''; // initialize
if (!$user->isEmpty()) { // If userId exists, generate a JWT for function
$sessions = $user->getAttribute('sessions', []);
$current = new Document();
foreach ($sessions as $session) {
/** @var Utopia\Database\Document $session */
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$current = $session;
}
}
if (!$current->isEmpty()) {
$jwtExpiry = $function->getAttribute('timeout', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$jwt = $jwtObj->encode([
'userId' => $user->getId(),
'sessionId' => $current->getId(),
]);
}
}
$jwtExpiry = $function->getAttribute('timeout', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$apiKey = $jwtObj->encode([
'projectId' => $project->getId(),
'scopes' => $function->getAttribute('scopes', [])
]);
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
$ip = $headers['x-real-ip'] ?? '';
if (!empty($ip)) {
$record = $geodb->get($ip);
if ($record) {
$eu = Config::getParam('locale-eu');
$headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? '';
$headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? '';
$headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false';
}
}
$headersFiltered = [];
foreach ($headers as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
$headersFiltered[] = ['name' => $key, 'value' => $value];
}
}
$executionId = ID::unique();
$status = $async ? 'waiting' : 'processing';
if (!is_null($scheduledAt)) {
$status = 'scheduled';
}
$execution = new Document([
'$id' => $executionId,
'$permissions' => !$user->isEmpty() ? [Permission::read(Role::user($user->getId()))] : [],
'resourceInternalId' => $function->getInternalId(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'trigger' => (!is_null($scheduledAt)) ? 'schedule' : 'http',
'status' => $status, // waiting / processing / completed / failed / scheduled
'responseStatusCode' => 0,
'responseHeaders' => [],
'requestPath' => $path,
'requestMethod' => $method,
'requestHeaders' => $headersFiltered,
'errors' => '',
'logs' => '',
'duration' => 0.0,
'search' => implode(' ', [$functionId, $executionId]),
]);
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->setContext('function', $function);
if ($async) {
if (is_null($scheduledAt)) {
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
$queueForFunctions
->setType('http')
->setExecution($execution)
->setFunction($function)
->setBody($body)
->setHeaders($headers)
->setPath($path)
->setMethod($method)
->setJWT($jwt)
->setProject($project)
->setUser($user)
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->trigger();
} else {
$data = [
'headers' => $headers,
'path' => $path,
'method' => $method,
'body' => $body,
'userId' => $user->getId()
];
$schedule = $dbForPlatform->createDocument('schedules', new Document([
'region' => System::getEnv('_APP_REGION', 'default'),
'resourceType' => ScheduleExecutions::getSupportedResource(),
'resourceId' => $execution->getId(),
'resourceInternalId' => $execution->getInternalId(),
'resourceUpdatedAt' => DateTime::now(),
'projectId' => $project->getId(),
'schedule' => $scheduledAt,
'data' => $data,
'active' => true,
]));
$execution = $execution
->setAttribute('scheduleId', $schedule->getId())
->setAttribute('scheduleInternalId', $schedule->getInternalId())
->setAttribute('scheduledAt', $scheduledAt);
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
}
return $response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($execution, Response::MODEL_EXECUTION);
}
$durationStart = \microtime(true);
$vars = [];
// V2 vars
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
]);
}
// Shared vars
foreach ($function->getAttribute('varsProject', []) as $var) {
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
// Function vars
foreach ($function->getAttribute('vars', []) as $var) {
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
// Appwrite vars
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_ID' => $functionId,
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(),
'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '',
'APPWRITE_FUNCTION_CPUS' => $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
'APPWRITE_FUNCTION_MEMORY' => $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
'APPWRITE_VERSION' => APP_VERSION_STABLE,
'APPWRITE_REGION' => $project->getAttribute('region'),
'APPWRITE_DEPLOYMENT_TYPE' => $deployment->getAttribute('type', ''),
'APPWRITE_VCS_REPOSITORY_ID' => $deployment->getAttribute('providerRepositoryId', ''),
'APPWRITE_VCS_REPOSITORY_NAME' => $deployment->getAttribute('providerRepositoryName', ''),
'APPWRITE_VCS_REPOSITORY_OWNER' => $deployment->getAttribute('providerRepositoryOwner', ''),
'APPWRITE_VCS_REPOSITORY_URL' => $deployment->getAttribute('providerRepositoryUrl', ''),
'APPWRITE_VCS_REPOSITORY_BRANCH' => $deployment->getAttribute('providerBranch', ''),
'APPWRITE_VCS_REPOSITORY_BRANCH_URL' => $deployment->getAttribute('providerBranchUrl', ''),
'APPWRITE_VCS_COMMIT_HASH' => $deployment->getAttribute('providerCommitHash', ''),
'APPWRITE_VCS_COMMIT_MESSAGE' => $deployment->getAttribute('providerCommitMessage', ''),
'APPWRITE_VCS_COMMIT_URL' => $deployment->getAttribute('providerCommitUrl', ''),
'APPWRITE_VCS_COMMIT_AUTHOR_NAME' => $deployment->getAttribute('providerCommitAuthor', ''),
'APPWRITE_VCS_COMMIT_AUTHOR_URL' => $deployment->getAttribute('providerCommitAuthorUrl', ''),
'APPWRITE_VCS_ROOT_DIRECTORY' => $deployment->getAttribute('providerRootDirectory', ''),
]);
/** Execute function */
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
try {
$version = $function->getAttribute('version', 'v2');
$command = $runtime['startCommand'];
$command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"';
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deployment->getId(),
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $function->getAttribute('timeout', 0),
image: $runtime['image'],
source: $build->getAttribute('path', ''),
entrypoint: $deployment->getAttribute('entrypoint', ''),
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
requestTimeout: 30
);
$headersFiltered = [];
foreach ($executionResponse['headers'] as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) {
$headersFiltered[] = ['name' => $key, 'value' => $value];
}
}
/** Update execution status */
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
$execution->setAttribute('status', $status);
$execution->setAttribute('responseStatusCode', $executionResponse['statusCode']);
$execution->setAttribute('responseHeaders', $headersFiltered);
$execution->setAttribute('logs', $executionResponse['logs']);
$execution->setAttribute('errors', $executionResponse['errors']);
$execution->setAttribute('duration', $executionResponse['duration']);
} catch (\Throwable $th) {
$durationEnd = \microtime(true);
$execution
->setAttribute('duration', $durationEnd - $durationStart)
->setAttribute('status', 'failed')
->setAttribute('responseStatusCode', 500)
->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode());
Console::error($th->getMessage());
if ($th instanceof AppwriteException) {
throw $th;
}
} finally {
$queueForStatsUsage
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
;
$execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if (!$isPrivilegedUser && !$isAppUser) {
$execution->setAttribute('logs', '');
$execution->setAttribute('errors', '');
}
$headers = [];
foreach (($executionResponse['headers'] ?? []) as $key => $value) {
$headers[] = ['name' => $key, 'value' => $value];
}
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
$execution->setAttribute('responseHeaders', $headers);
$acceptTypes = \explode(', ', $request->getHeader('accept'));
foreach ($acceptTypes as $acceptType) {
if (\str_starts_with($acceptType, 'application/json') || \str_starts_with($acceptType, 'application/*')) {
$response->setContentType(Response::CONTENT_TYPE_JSON);
break;
} elseif (\str_starts_with($acceptType, 'multipart/form-data') || \str_starts_with($acceptType, 'multipart/*')) {
$response->setContentType(Response::CONTENT_TYPE_MULTIPART);
break;
}
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($execution, Response::MODEL_EXECUTION);
}
}
@@ -0,0 +1,116 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Tasks\ScheduleExecutions;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteExecution';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/functions/:functionId/executions/:executionId')
->desc('Delete execution')
->groups(['api', 'functions'])
->label('scope', 'execution.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].executions.[executionId].delete')
->label('audits.event', 'executions.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'deleteExecution',
description: <<<EOT
Delete a function execution by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('functionId', '', new UID(), 'Function ID.')
->param('executionId', '', new UID(), 'Execution ID.')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('queueForEvents')
->callback([$this, 'action']);
}
public function action(string $functionId, string $executionId, Response $response, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$execution = $dbForProject->getDocument('executions', $executionId);
if ($execution->isEmpty()) {
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) {
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
$status = $execution->getAttribute('status');
if (!in_array($status, ['completed', 'failed', 'scheduled'])) {
throw new Exception(Exception::EXECUTION_IN_PROGRESS);
}
if (!$dbForProject->deleteDocument('executions', $execution->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove execution from DB');
}
if ($status === 'scheduled') {
$schedule = $dbForPlatform->findOne('schedules', [
Query::equal('resourceId', [$execution->getId()]),
Query::equal('resourceType', [ScheduleExecutions::getSupportedResource()]),
Query::equal('active', [true]),
]);
if (!$schedule->isEmpty()) {
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
}
}
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('executionId', $execution->getId())
->setPayload($response->output($execution, Response::MODEL_EXECUTION));
$response->noContent();
}
}
@@ -0,0 +1,88 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getExecution';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/executions/:executionId')
->desc('Get execution')
->groups(['api', 'functions'])
->label('scope', 'execution.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'getExecution',
description: <<<EOT
Get a function execution log by its unique ID.
EOT,
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EXECUTION,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('executionId', '', new UID(), 'Execution ID.')
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, string $executionId, Response $response, Database $dbForProject)
{
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$execution = $dbForProject->getDocument('executions', $executionId);
if ($execution->getAttribute('resourceType') !== 'functions' && $execution->getAttribute('resourceInternalId') !== $function->getInternalId()) {
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
if ($execution->isEmpty()) {
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if (!$isPrivilegedUser && !$isAppUser) {
$execution->setAttribute('logs', '');
$execution->setAttribute('errors', '');
}
$response->dynamic($execution, Response::MODEL_EXECUTION);
}
}
@@ -0,0 +1,135 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Appwrite\Auth\Auth;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Executions;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listExecutions';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/executions')
->desc('List executions')
->groups(['api', 'functions'])
->label('scope', 'execution.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'listExecutions',
description: <<<EOT
Get a list of all the current user function execution logs. You can use the query params to filter your results.
EOT,
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EXECUTION_LIST,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Executions(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Executions::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, array $queries, string $search, Response $response, Database $dbForProject)
{
$function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($function->isEmpty() || (!$function->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
// Set internal queries
$queries[] = Query::equal('resourceInternalId', [$function->getInternalId()]);
$queries[] = Query::equal('resourceType', ['functions']);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$executionId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('executions', $executionId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$results = $dbForProject->find('executions', $queries);
$total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT);
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if (!$isPrivilegedUser && !$isAppUser) {
$results = array_map(function ($execution) {
$execution->setAttribute('logs', '');
$execution->setAttribute('errors', '');
return $execution;
}, $results);
}
$response->dynamic(new Document([
'executions' => $results,
'total' => $total,
]), Response::MODEL_EXECUTION_LIST);
}
}
@@ -0,0 +1,93 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteFunction';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/functions/:functionId')
->desc('Delete function')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].delete')
->label('audits.event', 'function.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'delete',
description: <<<EOT
Delete a function by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('functionId', '', new UID(), 'Function ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $functionId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Database $dbForPlatform)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('functions', $function->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove function from DB');
}
// Inform scheduler to no longer run function
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('active', false);
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($function);
$queueForEvents->setParam('functionId', $function->getId());
$response->noContent();
}
}
@@ -0,0 +1,64 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getFunction';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId')
->desc('Get function')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'get',
description: <<<EOT
Get a function by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_FUNCTION,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, Response $response, Database $dbForProject)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$response->dynamic($function, Response::MODEL_FUNCTION);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Specifications;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listFunctionsSpecifications';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/specifications')
->groups(['api', 'functions'])
->desc('List available function runtime specifications')
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'listSpecifications',
description: <<<EOT
List allowed function specifications for this instance.
EOT,
auth: [AuthType::KEY, AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SPECIFICATION_LIST,
)
]
))
->inject('response')
->inject('plan')
->callback([$this, 'action']);
}
public function action(Response $response, array $plan)
{
$allRuntimeSpecs = Config::getParam('runtime-specifications', []);
$runtimeSpecs = [];
foreach ($allRuntimeSpecs as $spec) {
$spec['enabled'] = true;
if (array_key_exists('runtimeSpecifications', $plan)) {
$spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']);
}
// Only add specs that are within the limits set by environment variables
if ($spec['cpus'] <= System::getEnv('_APP_COMPUTE_CPUS', 1) && $spec['memory'] <= System::getEnv('_APP_COMPUTE_MEMORY', 512)) {
$runtimeSpecs[] = $spec;
}
}
$response->dynamic(new Document([
'specifications' => $runtimeSpecs,
'total' => count($runtimeSpecs)
]), Response::MODEL_SPECIFICATION_LIST);
}
}
@@ -0,0 +1,69 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Templates;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getTemplate';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/templates/:templateId')
->desc('Get function template')
->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'getTemplate',
description: <<<EOT
Get a function template using ID. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_TEMPLATE_FUNCTION,
)
]
))
->param('templateId', '', new Text(128), 'Template ID.')
->inject('response')
->callback([$this, 'action']);
}
public function action(string $templateId, Response $response)
{
$templates = Config::getParam('function-templates', []);
$filtered = \array_filter($templates, function ($template) use ($templateId) {
return $template['id'] === $templateId;
});
$template = array_shift($filtered);
if (empty($template)) {
throw new Exception(Exception::FUNCTION_TEMPLATE_NOT_FOUND);
}
$response->dynamic(new Document($template), Response::MODEL_TEMPLATE_FUNCTION);
}
}
@@ -0,0 +1,79 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Templates;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listTemplates';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/templates')
->desc('List function templates')
->label('scope', 'public')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'listTemplates',
description: <<<EOT
List available function templates. You can use template details in [createFunction](/docs/references/cloud/server-nodejs/functions#create) method.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_TEMPLATE_FUNCTION_LIST,
)
]
))
->param('runtimes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('runtimes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of runtimes allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' runtimes are allowed.', true)
->param('useCases', [], new ArrayList(new WhiteList(['dev-tools','starter','databases','ai','messaging','utilities']), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true)
->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true)
->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', true)
->inject('response')
->callback([$this, 'action']);
}
public function action(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);
}
}
@@ -0,0 +1,149 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getFunctionUsage';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/usage')
->desc('Get function usage')
->groups(['api', 'functions', 'usage'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'getUsage',
description: <<<EOT
Get usage metrics and statistics for a for a specific function. View statistics including total deployments, builds, executions, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_USAGE_FUNCTION,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, string $range, Response $response, Database $dbForProject)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS),
str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $function->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE),
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS),
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE),
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE),
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS),
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE),
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_BUILDS_MB_SECONDS),
str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS)
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$response->dynamic(new Document([
'range' => $range,
'deploymentsTotal' => $usage[$metrics[0]]['total'],
'deploymentsStorageTotal' => $usage[$metrics[1]]['total'],
'buildsTotal' => $usage[$metrics[2]]['total'],
'buildsStorageTotal' => $usage[$metrics[3]]['total'],
'buildsTimeTotal' => $usage[$metrics[4]]['total'],
'executionsTotal' => $usage[$metrics[5]]['total'],
'executionsTimeTotal' => $usage[$metrics[6]]['total'],
'deployments' => $usage[$metrics[0]]['data'],
'deploymentsStorage' => $usage[$metrics[1]]['data'],
'builds' => $usage[$metrics[2]]['data'],
'buildsStorage' => $usage[$metrics[3]]['data'],
'buildsTime' => $usage[$metrics[4]]['data'],
'executions' => $usage[$metrics[5]]['data'],
'executionsTime' => $usage[$metrics[6]]['data'],
'buildsMbSecondsTotal' => $usage[$metrics[7]]['total'],
'buildsMbSeconds' => $usage[$metrics[7]]['data'],
'executionsMbSeconds' => $usage[$metrics[8]]['data'],
'executionsMbSecondsTotal' => $usage[$metrics[8]]['total']
]), Response::MODEL_USAGE_FUNCTION);
}
}
@@ -0,0 +1,142 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Usage;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'getFunctionsUsage';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/usage')
->desc('Get functions usage')
->groups(['api', 'functions', 'usage'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('sdk', new Method(
namespace: 'functions',
name: 'listUsage',
description: <<<EOT
Get usage metrics and statistics for all functions in the project. View statistics including total deployments, builds, logs, storage usage, and compute time. The response includes both current totals and historical data for each metric. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, defaults to 30 days.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_USAGE_FUNCTIONS,
)
]
))
->param('range', '30d', new WhiteList(['24h', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $range, Response $response, Database $dbForProject)
{
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
METRIC_FUNCTIONS,
METRIC_DEPLOYMENTS,
METRIC_DEPLOYMENTS_STORAGE,
METRIC_BUILDS,
METRIC_BUILDS_STORAGE,
METRIC_BUILDS_COMPUTE,
METRIC_EXECUTIONS,
METRIC_EXECUTIONS_COMPUTE,
METRIC_BUILDS_MB_SECONDS,
METRIC_EXECUTIONS_MB_SECONDS,
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$response->dynamic(new Document([
'range' => $range,
'functionsTotal' => $usage[$metrics[0]]['total'],
'deploymentsTotal' => $usage[$metrics[1]]['total'],
'deploymentsStorageTotal' => $usage[$metrics[2]]['total'],
'buildsTotal' => $usage[$metrics[3]]['total'],
'buildsStorageTotal' => $usage[$metrics[4]]['total'],
'buildsTimeTotal' => $usage[$metrics[5]]['total'],
'executionsTotal' => $usage[$metrics[6]]['total'],
'executionsTimeTotal' => $usage[$metrics[7]]['total'],
'functions' => $usage[$metrics[0]]['data'],
'deployments' => $usage[$metrics[1]]['data'],
'deploymentsStorage' => $usage[$metrics[2]]['data'],
'builds' => $usage[$metrics[3]]['data'],
'buildsStorage' => $usage[$metrics[4]]['data'],
'buildsTime' => $usage[$metrics[5]]['data'],
'executions' => $usage[$metrics[6]]['data'],
'executionsTime' => $usage[$metrics[7]]['data'],
'buildsMbSecondsTotal' => $usage[$metrics[8]]['total'],
'buildsMbSeconds' => $usage[$metrics[8]]['data'],
'executionsMbSeconds' => $usage[$metrics[9]]['data'],
'executionsMbSecondsTotal' => $usage[$metrics[9]]['total'],
]), Response::MODEL_USAGE_FUNCTIONS);
}
}
@@ -0,0 +1,115 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Base
{
use HTTP;
public static function getName()
{
return 'createVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/variables')
->desc('Create variable')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'variable.create')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'createVariable',
description: <<<EOT
Create a new function environment variable. These variables can be accessed in the function at runtime as environment variables.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_VARIABLE,
)
]
))
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $functionId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$variableId = ID::unique();
$variable = new Document([
'$id' => $variableId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $function->getInternalId(),
'resourceId' => $function->getId(),
'resourceType' => 'function',
'key' => $key,
'value' => $value,
'secret' => $secret,
'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']),
]);
try {
$variable = $dbForProject->createDocument('variables', $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,93 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
{
use HTTP;
public static function getName()
{
return 'deleteVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/functions/:functionId/variables/:variableId')
->desc('Delete variable')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'variable.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'deleteVariable',
description: <<<EOT
Delete a variable by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $functionId, string $variableId, Response $response, Database $dbForProject, Database $dbForPlatform)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$dbForProject->deleteDocument('variables', $variable->getId());
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->noContent();
}
}
@@ -0,0 +1,82 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
{
use HTTP;
public static function getName()
{
return 'getVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/variables/:variableId')
->desc('Get variable')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label(
'sdk',
new Method(
namespace: 'functions',
name: 'getVariable',
description: <<<EOT
Get a variable by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
],
)
)
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, string $variableId, Response $response, Database $dbForProject)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if (
$variable === false ||
$variable->isEmpty() ||
$variable->getAttribute('resourceInternalId') !== $function->getInternalId() ||
$variable->getAttribute('resourceType') !== 'function'
) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,111 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateVariable';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/functions/:functionId/variables/:variableId')
->desc('Update variable')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'variable.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'updateVariable',
description: <<<EOT
Update variable by its unique ID.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
]
))
->param('functionId', '', new UID(), 'Function unique ID.', false)
->param('variableId', '', new UID(), 'Variable unique ID.', false)
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $functionId, string $variableId, string $key, ?string $value, ?bool $secret, Response $response, Database $dbForProject, Database $dbForPlatform)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceInternalId') !== $function->getInternalId() || $variable->getAttribute('resourceType') !== 'function') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable === false || $variable->isEmpty()) {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable->getAttribute('secret') === true && $secret === false) {
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
}
$variable
->setAttribute('key', $key)
->setAttribute('value', $value ?? $variable->getAttribute('value'))
->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function']));
try {
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
// Inform scheduler to pull the latest changes
$schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deployment')));
Authorization::skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule));
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Base
{
use HTTP;
public static function getName()
{
return 'listVariables';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/variables')
->desc('List variables')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label(
'sdk',
new Method(
namespace: 'functions',
name: 'listVariables',
description: <<<EOT
Get a list of all variables of a specific function.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE_LIST,
)
],
)
)
->param('functionId', '', new UID(), 'Function unique ID.', false)
->inject('response')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $functionId, Response $response, Database $dbForProject)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$response->dynamic(new Document([
'variables' => $function->getAttribute('vars', []),
'total' => \count($function->getAttribute('vars', [])),
]), Response::MODEL_VARIABLE_LIST);
}
}
@@ -2,13 +2,36 @@
namespace Appwrite\Platform\Modules\Functions\Services;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Create as CreateBuild;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Update as UpdateBuild;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Create as CreateDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Delete as DeleteDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Download\Get as DownloadDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Get as GetDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Template\Create as CreateTemplateDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Update as UpdateDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Vcs\Create as CreateVcsDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\XList as ListDeployments;
use Appwrite\Platform\Modules\Functions\Http\Executions\Create as CreateExecution;
use Appwrite\Platform\Modules\Functions\Http\Executions\Delete as DeleteExecution;
use Appwrite\Platform\Modules\Functions\Http\Executions\Get as GetExecution;
use Appwrite\Platform\Modules\Functions\Http\Executions\XList as ListExecutions;
use Appwrite\Platform\Modules\Functions\Http\Functions\Create as CreateFunction;
use Appwrite\Platform\Modules\Functions\Http\Functions\Delete as DeleteFunction;
use Appwrite\Platform\Modules\Functions\Http\Functions\Get as GetFunction;
use Appwrite\Platform\Modules\Functions\Http\Functions\Update as UpdateFunction;
use Appwrite\Platform\Modules\Functions\Http\Functions\XList as ListFunctions;
use Appwrite\Platform\Modules\Functions\Http\Runtimes\XList as ListRuntimes;
use Appwrite\Platform\Modules\Functions\Http\Specifications\XList as ListSpecifications;
use Appwrite\Platform\Modules\Functions\Http\Templates\Get as GetTemplate;
use Appwrite\Platform\Modules\Functions\Http\Templates\XList as ListTemplates;
use Appwrite\Platform\Modules\Functions\Http\Usage\Get as GetUsage;
use Appwrite\Platform\Modules\Functions\Http\Usage\XList as ListUsage;
use Appwrite\Platform\Modules\Functions\Http\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Functions\Http\Variables\Delete as DeleteVariable;
use Appwrite\Platform\Modules\Functions\Http\Variables\Get as GetVariable;
use Appwrite\Platform\Modules\Functions\Http\Variables\Update as UpdateVariable;
use Appwrite\Platform\Modules\Functions\Http\Variables\XList as ListVariables;
use Utopia\Platform\Service;
class Http extends Service
@@ -16,12 +39,51 @@ class Http extends Service
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Functions
$this->addAction(CreateFunction::getName(), new CreateFunction());
$this->addAction(GetFunction::getName(), new GetFunction());
$this->addAction(UpdateFunction::getName(), new UpdateFunction());
$this->addAction(ListFunctions::getName(), new ListFunctions());
$this->addAction(DeleteFunction::getName(), new DeleteFunction());
// Runtimes
$this->addAction(ListRuntimes::getName(), new ListRuntimes());
// Specifications
$this->addAction(ListSpecifications::getName(), new ListSpecifications());
// Deployments
$this->addAction(CreateDeployment::getName(), new CreateDeployment());
$this->addAction(GetDeployment::getName(), new GetDeployment());
$this->addAction(UpdateDeployment::getName(), new UpdateDeployment());
$this->addAction(ListDeployments::getName(), new ListDeployments());
$this->addAction(DeleteDeployment::getName(), new DeleteDeployment());
$this->addAction(CreateTemplateDeployment::getName(), new CreateTemplateDeployment());
$this->addAction(CreateVcsDeployment::getName(), new CreateVcsDeployment());
$this->addAction(DownloadDeployment::getName(), new DownloadDeployment());
$this->addAction(CreateBuild::getName(), new CreateBuild());
$this->addAction(UpdateBuild::getName(), new UpdateBuild());
// Executions
$this->addAction(CreateExecution::getName(), new CreateExecution());
$this->addAction(GetExecution::getName(), new GetExecution());
$this->addAction(ListExecutions::getName(), new ListExecutions());
$this->addAction(DeleteExecution::getName(), new DeleteExecution());
// Usage
$this->addAction(GetUsage::getName(), new GetUsage());
$this->addAction(ListUsage::getName(), new ListUsage());
// Variables
$this->addAction(CreateVariable::getName(), new CreateVariable());
$this->addAction(GetVariable::getName(), new GetVariable());
$this->addAction(ListVariables::getName(), new ListVariables());
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
// Templates
$this->addAction(GetTemplate::getName(), new GetTemplate());
$this->addAction(ListTemplates::getName(), new ListTemplates());
}
}
@@ -62,11 +62,12 @@ class Builds extends Action
->inject('cache')
->inject('dbForProject')
->inject('deviceForFunctions')
->inject('deviceForSites')
->inject('isResourceBlocked')
->inject('deviceForFiles')
->inject('log')
->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Device $deviceForFiles, Log $log) =>
$this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $isResourceBlocked, $deviceForFiles, $log));
->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Device $deviceForSites, callable $isResourceBlocked, Device $deviceForFiles, Log $log) =>
$this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $deviceForSites, $isResourceBlocked, $deviceForFiles, $log));
}
/**
@@ -79,12 +80,13 @@ class Builds extends Action
* @param Cache $cache
* @param Database $dbForProject
* @param Device $deviceForFunctions
* @param Device $deviceForSites
* @param Device $deviceForFiles
* @param Log $log
* @return void
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Device $deviceForFiles, Log $log): void
public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Device $deviceForSites, callable $isResourceBlocked, Device $deviceForFiles, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -105,7 +107,7 @@ class Builds extends Action
case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId());
$github = new GitHub($cache);
$this->buildDeployment($deviceForFunctions, $deviceForFiles, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log);
$this->buildDeployment($deviceForFunctions, $deviceForSites, $deviceForFiles, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log);
break;
default:
@@ -115,6 +117,7 @@ class Builds extends Action
/**
* @param Device $deviceForFunctions
* @param Device $deviceForSites
* @param Device $deviceForFiles
* @param Func $queueForFunctions
* @param Event $queueForEvents
@@ -132,7 +135,7 @@ class Builds extends Action
*
* @throws Exception
*/
protected function buildDeployment(Device $deviceForFunctions, Device $deviceForFiles, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void
protected function buildDeployment(Device $deviceForFunctions, Device $deviceForSites, Device $deviceForFiles, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void
{
$resourceKey = match($resource->getCollection()) {
'functions' => 'functionId',
@@ -140,6 +143,11 @@ class Builds extends Action
default => throw new \Exception('Invalid resource type')
};
$device = match ($resource->getCollection()) {
'sites' => $deviceForSites,
'functions' => $deviceForFunctions,
};
$executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST'));
$log->addTag($resourceKey, $resource->getId());
@@ -149,8 +157,7 @@ class Builds extends Action
throw new \Exception('Resource not found');
}
// TODO: Sites support
if ($isResourceBlocked($project, RESOURCE_TYPE_FUNCTIONS, $resource->getId())) {
if ($isResourceBlocked($project, $resourceKey === 'functions' ? RESOURCE_TYPE_FUNCTIONS : RESOURCE_TYPE_SITES, $resource->getId())) {
throw new \Exception('Resource is blocked');
}
@@ -272,8 +279,8 @@ class Builds extends Action
$tarParamDirectory = \escapeshellarg($tmpTemplateDirectory . (empty($templateRootDirectory) ? '' : '/' . $templateRootDirectory));
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
$source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions);
$source = $device->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$result = $localDevice->transfer($tmpPathFile, $source, $device);
if (!$result) {
throw new \Exception("Unable to move file");
@@ -281,7 +288,7 @@ class Builds extends Action
Console::execute('rm -rf ' . \escapeshellarg($tmpTemplateDirectory), '', $stdout, $stderr);
$directorySize = $deviceForFunctions->getFileSize($source);
$directorySize = $device->getFileSize($source);
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize));
}
@@ -429,8 +436,8 @@ class Builds extends Action
$tarParamDirectory = '/tmp/builds/' . $buildId . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory);
Console::execute('tar --exclude code.tar.gz -czf ' . \escapeshellarg($tmpPathFile) . ' -C ' . \escapeshellcmd($tarParamDirectory) . ' .', '', $stdout, $stderr); // TODO: Replace escapeshellcmd with escapeshellarg if we find a way that doesnt break syntax
$source = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$result = $localDevice->transfer($tmpPathFile, $source, $deviceForFunctions);
$source = $device->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$result = $localDevice->transfer($tmpPathFile, $source, $device);
if (!$result) {
throw new \Exception("Unable to move file");
@@ -440,7 +447,7 @@ class Builds extends Action
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttribute('source', $source));
$directorySize = $deviceForFunctions->getFileSize($source);
$directorySize = $device->getFileSize($source);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttribute('path', $source)->setAttribute('size', $directorySize));
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform);
@@ -749,8 +756,8 @@ class Builds extends Action
try {
$rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
Query::equal("projectInternalId", [$project->getInternalId()]),
Query::equal("resourceType", ["deployment"]),
Query::equal("resourceInternalId", [$deployment->getInternalId()])
Query::equal("type", ["deployment"]),
Query::equal("value", [$deployment->getId()])
]));
if ($rule->isEmpty()) {
@@ -760,7 +767,7 @@ class Builds extends Action
$client = new FetchClient();
$client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON);
$bucket = $dbForPlatform->getDocument('buckets', 'screenshots');
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
$configs = [
'screenshotLight' => [
@@ -856,14 +863,51 @@ class Builds extends Action
case 'functions':
$resource->setAttribute('deployment', $deployment->getId());
$resource = $dbForProject->updateDocument('functions', $resource->getId(), $resource);
$this->listRules($project, [
Query::equal("automation", ["function=" . $resource->getId()]),
], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) {
$rule = $rule->setAttribute('value', $deployment->getId());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
});
break;
case 'sites':
$resource->setAttribute('deploymentId', $deployment->getId());
$resource = $dbForProject->updateDocument('sites', $resource->getId(), $resource);
$this->listRules($project, [
Query::equal("automation", ["site=" . $resource->getId()]),
], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) {
$rule = $rule->setAttribute('value', $deployment->getId());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
});
// VCS branch
$branchName = $deployment->getAttribute('providerBranch');
if (!empty($branchName)) {
$this->listRules($project, [
Query::equal("automation", ["branch=" . $branchName]),
], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) {
$rule = $rule->setAttribute('value', $deployment->getId());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
});
}
// VCS commit
$commitHash = $deployment->getAttribute('providerCommitHash', '');
if (!empty($commitHash)) {
$this->listRules($project, [
Query::equal("automation", ["commit=" . $commitHash]),
], $dbForPlatform, function (Document $rule) use ($dbForPlatform, $deployment) {
$rule = $rule->setAttribute('value', $deployment->getId());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
});
}
break;
}
}
if ($dbForProject->getDocument('builds', $buildId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
return;
@@ -1130,8 +1174,8 @@ class Builds extends Action
$rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
Query::equal("projectInternalId", [$project->getInternalId()]),
Query::equal("resourceType", ["deployment"]),
Query::equal("resourceInternalId", [$deployment->getInternalId()])
Query::equal("type", ["deployment"]),
Query::equal("value", [$deployment->getId()])
]));
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
@@ -1156,4 +1200,38 @@ class Builds extends Action
}
}
}
protected function listRules(Document $project, array $queries, Database $database, callable $callback = null): void
{
$limit = 100;
$cursor = null;
do {
$queries = \array_merge([
Query::limit($limit),
Query::equal("projectInternalId", [$project->getInternalId()])
], $queries);
if ($cursor !== null) {
$queries[] = Query::cursorAfter($cursor);
}
$results = $database->find('rules', $queries);
$total = \count($results);
if ($total > 0) {
$cursor = $results[$total - 1];
}
if ($total < $limit) {
$cursor = null;
}
foreach ($results as $document) {
if (is_callable($callback)) {
$callback($document);
}
}
} while (!\is_null($cursor));
}
}
@@ -0,0 +1,152 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createAPIRule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/proxy/rules/api')
->groups(['api', 'proxy'])
->desc('Create API rule')
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].create')
->label('audits.event', 'rule.create')
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
name: 'createAPIRule',
description: <<<EOT
Create a new proxy rule for serving Appwrite's API on custom domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform)
{
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$deniedDomains = [
$mainDomain,
$sitesDomain,
$functionsDomain,
'localhost',
APP_HOSTNAME_INTERNAL,
];
if (\in_array($domain, $deniedDomains)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
if (\str_starts_with($domain, 'commit-') || \str_starts_with($domain, 'branch-')) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// Apex domain prevention due to CNAME limitations
if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) {
if ($domain->get() === $domain->getRegisterable()) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.');
}
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {
$status = 'verified';
}
if ($status === 'created') {
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validator = new CNAME($target->get());
if ($validator->isValid($domain->get())) {
$status = 'verifying';
}
}
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'status' => $status,
'type' => 'api',
'value' => '',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain->get()]),
]);
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
if ($rule->getAttribute('status', '') === 'verifying') {
$queueForCertificates
->setDomain(new Document([
'domain' => $rule->getAttribute('domain')
]))
->trigger();
}
$queueForEvents->setParam('ruleId', $rule->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules;
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
@@ -10,8 +10,10 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
@@ -19,7 +21,7 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
class Create extends Action
{
@@ -27,25 +29,25 @@ class Create extends Action
public static function getName()
{
return 'createRule';
return 'createFunctionRule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/proxy/rules')
->setHttpPath('/v1/proxy/rules/function')
->groups(['api', 'proxy'])
->desc('Create rule')
->desc('Create function rule')
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].create')
->label('audits.event', 'rule.create')
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
name: 'createRule',
name: 'createFunctionRule',
description: <<<EOT
Create a new proxy rule.
Create a new proxy rule for executing Appwrite Function on custom domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
@@ -59,8 +61,8 @@ class Create extends Action
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('resourceType', null, new WhiteList(['api', 'function', 'site']), 'Action definition for the rule. Possible values are "api", "function" and "site"')
->param('resourceId', '', new UID(), 'ID of resource for the action type. If resourceType is "api", leave empty. If resourceType is "function", provide ID of the function.', true)
->param('functionId', '', new UID(), 'ID of function to be executed.')
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
->inject('project')
->inject('queueForCertificates')
@@ -70,7 +72,7 @@ class Create extends Action
->callback([$this, 'action']);
}
public function action(string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
{
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
@@ -84,91 +86,70 @@ class Create extends Action
APP_HOSTNAME_INTERNAL,
];
if (in_array($domain, $deniedDomains, true)) {
if (\in_array($domain, $deniedDomains)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
$resourceInternalId = '';
switch ($resourceType) {
case 'function':
case 'site':
if (empty($resourceId)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'resourceId cannot be empty for resourceType "' . $resourceType . '".');
}
$expectedDomain = ($resourceType === 'function') ? $functionsDomain : $sitesDomain;
if (!\str_ends_with($domain, $expectedDomain)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain must end with ' . $expectedDomain . ' for resourceType "' . $resourceType . '".');
}
$collection = ($resourceType === 'function') ? 'functions' : 'sites';
$document = $dbForProject->getDocument($collection, $resourceId);
if ($document->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
$resourceInternalId = $document->getInternalId();
break;
case 'api':
if (\str_ends_with($domain, $functionsDomain) || \str_ends_with($domain, $sitesDomain)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain must not end with ' . $functionsDomain . ' or ' . $sitesDomain . ' for resourceType "api".');
}
break;
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// Apex domain prevention due to CNAME limitations
if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) {
if ($domain->get() === $domain->getRegisterable()) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.');
}
}
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
try {
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'resourceType' => $resourceType,
'resourceId' => $resourceId,
'resourceInternalId' => $resourceInternalId,
'certificateId' => '',
]);
} catch (\Throwable $e) {
if ($e->getCode() === Exception::DOCUMENT_ALREADY_EXISTS) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'An unexpected error occurred: ' . $e->getMessage());
}
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {
$status = 'verified';
}
if ($status === 'created') {
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validator = new CNAME($target->get()); // Verify Domain with DNS records
$validator = new CNAME($target->get());
if ($validator->isValid($domain->get())) {
$status = 'verifying';
$queueForCertificates
->setDomain(new Document([
'domain' => $rule->getAttribute('domain')
]))
->trigger();
}
}
$rule->setAttribute('status', $status);
$rule = $dbForPlatform->createDocument('rules', $rule);
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'status' => $status,
'type' => 'deployment',
'value' => $function->getAttribute('deployment', ''),
'certificateId' => '',
'automation' => 'function=' . $function->getId(),
'automation' => !empty($branch) ? ('branch=' . $branch) : ('function=' . $function->getId()),
'search' => implode(' ', [$ruleId, $domain->get(), $branch]),
]);
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
if ($rule->getAttribute('status', '') === 'verifying') {
$queueForCertificates
->setDomain(new Document([
'domain' => $rule->getAttribute('domain')
]))
->trigger();
}
$queueForEvents->setParam('ruleId', $rule->getId());
@@ -0,0 +1,155 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createRedirectRule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/proxy/rules/redirect')
->groups(['api', 'proxy'])
->desc('Create Redirect rule')
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].create')
->label('audits.event', 'rule.create')
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
name: 'createRedirectRule',
description: <<<EOT
Create a new proxy rule for to redirect from custom domain to another domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('target', null, new ValidatorDomain(), 'Target domain (hostname) of redirection')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $domain, string $target, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform)
{
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$deniedDomains = [
$mainDomain,
$sitesDomain,
$functionsDomain,
'localhost',
APP_HOSTNAME_INTERNAL,
];
if (\in_array($domain, $deniedDomains)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
try {
$target = new Domain($target);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Target may not start with http:// or https://.');
}
// Apex domain prevention due to CNAME limitations
if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) {
if ($domain->get() === $domain->getRegisterable()) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.');
}
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {
$status = 'verified';
}
if ($status === 'created') {
$dnsTarget = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validator = new CNAME($dnsTarget->get());
if ($validator->isValid($domain->get())) {
$status = 'verifying';
}
}
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'status' => $status,
'type' => 'redirect',
'value' => $target->get(),
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain->get()]),
]);
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
if ($rule->getAttribute('status', '') === 'verifying') {
$queueForCertificates
->setDomain(new Document([
'domain' => $rule->getAttribute('domain')
]))
->trigger();
}
$queueForEvents->setParam('ruleId', $rule->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
@@ -0,0 +1,159 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createSiteRule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/proxy/rules/site')
->groups(['api', 'proxy'])
->desc('Create site rule')
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].create')
->label('audits.event', 'rule.create')
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
name: 'createSiteRule',
description: <<<EOT
Create a new proxy rule for serving Appwrite Site on custom domain.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PROXY_RULE,
)
]
))
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('siteId', '', new UID(), 'ID of site to be executed.')
->param('branch', '', new Text(255, 0), 'Name of VCS branch to deploy changes automatically', true)
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
{
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$deniedDomains = [
$mainDomain,
$sitesDomain,
$functionsDomain,
'localhost',
APP_HOSTNAME_INTERNAL,
];
if (\in_array($domain, $deniedDomains)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.');
}
try {
$domain = new Domain($domain);
} catch (\Throwable) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// Apex domain prevention due to CNAME limitations
if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) {
if ($domain->get() === $domain->getRegisterable()) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.');
}
}
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {
$status = 'verified';
}
if ($status === 'created') {
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validator = new CNAME($target->get());
if ($validator->isValid($domain->get())) {
$status = 'verifying';
}
}
$rule = new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain->get(),
'status' => $status,
'type' => 'deployment',
'value' => $site->getAttribute('deploymentId', ''),
'certificateId' => '',
'automation' => !empty($branch) ? ('branch=' . $branch) : ('site=' . $site->getId()),
'search' => implode(' ', [$ruleId, $domain->get(), $branch]),
]);
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
if ($rule->getAttribute('status', '') === 'verifying') {
$queueForCertificates
->setDomain(new Document([
'domain' => $rule->getAttribute('domain')
]))
->trigger();
}
$queueForEvents->setParam('ruleId', $rule->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
@@ -2,7 +2,10 @@
namespace Appwrite\Platform\Modules\Proxy\Services;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Create as CreateRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\API\Create as CreateAPIRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Function\Create as CreateFunctionRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect\Create as CreateRedirectRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Site\Create as CreateSiteRule;
use Utopia\Platform\Service;
class Http extends Service
@@ -10,7 +13,11 @@ class Http extends Service
public function __construct()
{
$this->type = Service::TYPE_HTTP;
// Rules
$this->addAction(CreateRule::getName(), new CreateRule());
$this->addAction(CreateAPIRule::getName(), new CreateAPIRule());
$this->addAction(CreateSiteRule::getName(), new CreateSiteRule());
$this->addAction(CreateFunctionRule::getName(), new CreateFunctionRule());
$this->addAction(CreateRedirectRule::getName(), new CreateRedirectRule());
}
}
@@ -62,11 +62,10 @@ class Create extends Action
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('deviceForSites')
->inject('deviceForFunctions') //TODO: remove it later
->callback([$this, 'action']);
}
public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions)
public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites)
{
$site = $dbForProject->getDocument('sites', $siteId);
@@ -80,14 +79,14 @@ class Create extends Action
}
$path = $deployment->getAttribute('path');
if (empty($path) || !$deviceForFunctions->exists($path)) {
if (empty($path) || !$deviceForSites->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$deploymentId = ID::unique();
$destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$deviceForFunctions->transfer($path, $destination, $deviceForFunctions);
$destination = $deviceForSites->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$deviceForSites->transfer($path, $destination, $deviceForSites);
$deployment->removeAttribute('$internalId');
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
@@ -100,6 +99,8 @@ class Create extends Action
'installCommand' => $site->getAttribute('installCommand', ''),
'outputDirectory' => $site->getAttribute('outputDirectory', ''),
'search' => implode(' ', [$deploymentId]),
'screenshotLight' => '',
'screenshotDark' => ''
]));
// Preview deployments for sites
@@ -112,11 +113,11 @@ class Create extends Action
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'resourceType' => 'deployment',
'resourceId' => $deploymentId,
'resourceInternalId' => $deployment->getInternalId(),
'type' => 'deployment',
'value' => $deployment->getId(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
@@ -80,13 +80,12 @@ class Create extends Action
->inject('project')
->inject('queueForEvents')
->inject('deviceForSites')
->inject('deviceForFunctions') // TODO: Remove this later once volume is added to executor
->inject('deviceForLocal')
->inject('queueForBuilds')
->callback([$this, 'action']);
}
public function action(string $siteId, ?string $installCommand, ?string $buildCommand, ?string $outputDirectory, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project, Event $queueForEvents, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds)
public function action(string $siteId, ?string $installCommand, ?string $buildCommand, ?string $outputDirectory, mixed $code, mixed $activate, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Document $project, Event $queueForEvents, Device $deviceForSites, Device $deviceForLocal, Build $queueForBuilds)
{
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
@@ -168,7 +167,7 @@ class Create extends Action
// Save to storage
$fileSize ??= $deviceForLocal->getFileSize($fileTmpName);
$path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$path = $deviceForSites->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
@@ -180,7 +179,7 @@ class Create extends Action
}
}
$chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
$chunksUploaded = $deviceForSites->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
if (empty($chunksUploaded)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file');
@@ -203,7 +202,7 @@ class Create extends Action
}
}
$fileSize = $deviceForFunctions->getFileSize($path);
$fileSize = $deviceForSites->getFileSize($path);
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
@@ -237,11 +236,11 @@ class Create extends Action
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'resourceType' => 'deployment',
'resourceId' => $deploymentId,
'resourceInternalId' => $deployment->getInternalId(),
'type' => 'deployment',
'value' => $deployment->getId(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
} else {
@@ -288,11 +287,11 @@ class Create extends Action
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'resourceType' => 'deployment',
'resourceId' => $deploymentId,
'resourceInternalId' => $deployment->getInternalId(),
'type' => 'deployment',
'value' => $deployment->getId(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
} else {
@@ -59,11 +59,10 @@ class Delete extends Action
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('deviceForSites')
->inject('deviceForFunctions') //TODO: remove it later
->callback([$this, 'action']);
}
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Device $deviceForSites, Device $deviceForFunctions)
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Device $deviceForSites)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
@@ -84,14 +83,14 @@ class Delete extends Action
}
if (!empty($deployment->getAttribute('path', ''))) {
if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) {
if (!($deviceForSites->delete($deployment->getAttribute('path', '')))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage');
}
}
if ($site->getAttribute('deployment') === $deployment->getId()) { // Reset site deployment
$site = $dbForProject->updateDocument('sites', $site->getId(), new Document(array_merge($site->getArrayCopy(), [
'deployment' => '',
'deploymentId' => '',
'deploymentInternalId' => '',
])));
}
@@ -55,11 +55,10 @@ class Get extends Action
->inject('request')
->inject('dbForProject')
->inject('deviceForSites')
->inject('deviceForFunctions') //TODO: Remove this later
->callback([$this, 'action']);
}
public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForSites, Device $deviceForFunctions)
public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForSites)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
@@ -76,7 +75,7 @@ class Get extends Action
}
$path = $deployment->getAttribute('path', '');
if (!$deviceForFunctions->exists($path)) {
if (!$deviceForSites->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
@@ -86,7 +85,7 @@ class Get extends Action
->addHeader('X-Peak', \memory_get_peak_usage())
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"');
$size = $deviceForFunctions->getFileSize($path);
$size = $deviceForSites->getFileSize($path);
$rangeHeader = $request->getHeader('range');
if (!empty($rangeHeader)) {
@@ -108,13 +107,13 @@ class Get extends Action
->addHeader('Content-Length', $end - $start + 1)
->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT);
$response->send($deviceForFunctions->read($path, $start, ($end - $start + 1)));
$response->send($deviceForSites->read($path, $start, ($end - $start + 1)));
}
if ($size > APP_STORAGE_READ_BUFFER) {
for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) {
$response->chunk(
$deviceForFunctions->read(
$deviceForSites->read(
$path,
($i * MAX_OUTPUT_CHUNK_SIZE),
min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE))
@@ -123,7 +122,7 @@ class Get extends Action
);
}
} else {
$response->send($deviceForFunctions->read($path));
$response->send($deviceForSites->read($path));
}
}
}
@@ -8,9 +8,6 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -49,13 +46,11 @@ class Get extends Action
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform)
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
@@ -80,16 +75,6 @@ class Get extends Action
$deployment->setAttribute('buildSize', $build->getAttribute('size', 0));
$deployment->setAttribute('size', $deployment->getAttribute('size', 0));
$rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
Query::equal("projectInternalId", [$project->getInternalId()]),
Query::equal("resourceType", ["deployment"]),
Query::equal("resourceInternalId", [$deployment->getInternalId()])
]));
if (!empty($rule)) {
$deployment->setAttribute('domain', $rule->getAttribute('domain', ''));
}
$response->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -148,11 +148,11 @@ class Create extends Base
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'resourceType' => 'deployment',
'resourceId' => $deploymentId,
'resourceInternalId' => $deployment->getInternalId(),
'type' => 'deployment',
'value' => $deployment->getId(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
@@ -44,7 +44,7 @@ class Update extends Action
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_FUNCTION,
model: Response::MODEL_SITE,
)
]
))
@@ -12,7 +12,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
@@ -54,13 +53,11 @@ class XList extends Action
->param('queries', [], new Deployments(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->callback([$this, 'action']);
}
public function action(string $siteId, array $queries, string $search, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform)
public function action(string $siteId, array $queries, string $search, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
@@ -119,16 +116,6 @@ class XList extends Action
$result->setAttribute('buildTime', $build->getAttribute('duration', 0));
$result->setAttribute('buildSize', $build->getAttribute('size', 0));
$result->setAttribute('size', $result->getAttribute('size', 0));
$rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
Query::equal("projectInternalId", [$project->getInternalId()]),
Query::equal("resourceType", ["deployment"]),
Query::equal("resourceInternalId", [$result->getInternalId()])
]));
if (!empty($rule)) {
$result->setAttribute('domain', $rule->getAttribute('domain', ''));
}
}
$response->dynamic(new Document([
@@ -69,7 +69,7 @@ class Create extends Base
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.')
->param('adapter', '', new Text(8192, 0), 'Framework adapter. Allows: static, ssr', true)
->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true)
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true)
@@ -73,7 +73,7 @@ class Update extends Base
->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true)
->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true)
->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.', true)
->param('adapter', '', new Text(8192, 0), 'Framework adapter. Usuallly allows: static, ssr', true)
->param('adapter', '', new WhiteList(['static', 'ssr']), 'Framework adapter defining rendering strategy. Allowed values are: static, ssr', true)
->param('fallbackFile', '', new Text(255, 0), 'Fallback file for single page application sites.', true)
->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true)
->param('providerRepositoryId', '', new Text(128, 0), 'Repository ID of the repo linked to the site.', true)
@@ -29,7 +29,6 @@ class Get extends Base
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/templates/:templateId')
->desc('Get site template')
->groups(['api'])
->label('scope', 'public')
->label('sdk', new Method(
namespace: 'sites',
@@ -30,7 +30,6 @@ class XList extends Base
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/templates')
->desc('List templates')
->groups(['api'])
->label('scope', 'public')
->label('sdk', new Method(
namespace: 'sites',
@@ -31,6 +31,7 @@ class Delete extends Base
->desc('Delete variable')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', 'sites')
->label('audits.event', 'variable.delete')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
@@ -30,6 +30,7 @@ class Get extends Base
->desc('Get variable')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
->label('resourceType', RESOURCE_TYPE_SITES)
->label(
'sdk',
new Method(
@@ -35,6 +35,7 @@ class Update extends Base
->label('scope', 'sites.write')
->label('audits.event', 'variable.update')
->label('audits.resource', 'site/{request.siteId}')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('sdk', new Method(
namespace: 'sites',
name: 'updateVariable',
+39 -20
View File
@@ -93,13 +93,13 @@ class Deletes extends Action
$this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document);
break;
case DELETE_TYPE_SITES:
$this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project);
$this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project);
break;
case DELETE_TYPE_FUNCTIONS:
$this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project);
break;
case DELETE_TYPE_DEPLOYMENTS:
$this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project);
$this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForSites, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project);
break;
case DELETE_TYPE_USERS:
$this->deleteUser($getProjectDB, $document, $project);
@@ -743,14 +743,13 @@ class Deletes extends Action
/**
* @param callable $getProjectDB
* @param Device $deviceForSites
* @param Device $deviceForFunctions
* @param Device $deviceForBuilds
* @param Document $document function document
* @param Document $project
* @return void
* @throws Exception
*/
private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void
private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void
{
$dbForProject = $getProjectDB($project);
$siteId = $document->getId();
@@ -761,8 +760,8 @@ class Deletes extends Action
*/
Console::info("Deleting rules for site " . $siteId);
$this->deleteByGroup('rules', [
Query::equal('resourceType', ['site']),
Query::equal('resourceInternalId', [$siteInternalId]),
Query::equal('type', ['deployment']),
Query::equal('automation', ['site=' . $siteId]),
Query::equal('projectInternalId', [$project->getInternalId()])
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
@@ -782,23 +781,25 @@ class Deletes extends Action
*/
Console::info("Deleting deployments for site " . $siteId);
$deploymentInternalIds = [];
$deploymentIds = [];
$this->deleteByGroup('deployments', [
Query::equal('resourceInternalId', [$siteInternalId])
], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
$deploymentInternalIds[] = $document->getInternalId();
$this->deleteDeploymentFiles($deviceForFunctions, $document);
$deploymentIds[] = $document->getId();
$this->deleteDeploymentFiles($deviceForSites, $document);
$this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document);
$this->deleteDeploymentRules($dbForPlatform, $document, $project, $certificates);
});
/**
* Delete rules for all deployments of the site
*/
//TODO: If functions also have previews in the future, change the logic here to use unique identifier for sites and functions
foreach ($deploymentInternalIds as $deploymentInternalId) {
Console::info("Deleting rules for site " . $siteId . "'s deployment " . $deploymentInternalId);
foreach ($deploymentIds as $deploymentId) {
Console::info("Deleting rules for site " . $siteId . "'s deployment " . $deploymentId);
$this->deleteByGroup('rules', [
Query::equal('resourceType', ['deployment']),
Query::equal('resourceInternalId', [$deploymentInternalId]),
Query::equal('type', ['deployment']),
Query::equal('value', [$deploymentId]),
Query::equal('projectInternalId', [$project->getInternalId()])
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
@@ -856,8 +857,8 @@ class Deletes extends Action
*/
Console::info("Deleting rules for function " . $functionId);
$this->deleteByGroup('rules', [
Query::equal('resourceType', ['function']),
Query::equal('resourceInternalId', [$functionInternalId]),
Query::equal('type', ['deployment']),
Query::equal('automation', ['function=' . $functionId]),
Query::equal('projectInternalId', [$project->getInternalId()])
], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
@@ -880,9 +881,10 @@ class Deletes extends Action
$deploymentInternalIds = [];
$this->deleteByGroup('deployments', [
Query::equal('resourceInternalId', [$functionInternalId])
], $dbForProject, function (Document $document) use ($deviceForFunctions, &$deploymentInternalIds) {
], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, &$deploymentInternalIds) {
$deploymentInternalIds[] = $document->getInternalId();
$this->deleteDeploymentFiles($deviceForFunctions, $document);
$this->deleteDeploymentRules($dbForPlatform, $document, $project, $certificates);
});
/**
@@ -930,6 +932,18 @@ class Deletes extends Action
$this->deleteRuntimes($getProjectDB, $document, $project);
}
private function deleteDeploymentRules(Database $dbForPlatform, Document $deployment, Document $project, CertificatesAdapter $certificates): void
{
Console::info("Deleting rules for site " . $deployment->getId());
$this->deleteByGroup('rules', [
Query::equal('type', ['deployment']),
Query::equal('value', [$deployment->getId()]),
Query::equal('projectInternalId', [$project->getInternalId()])
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
});
}
private function deleteDeploymentScreenshots(Device $deviceForFiles, Database $dbForPlatform, Document $deployment): void
{
$screenshotIds = [];
@@ -1042,13 +1056,14 @@ class Deletes extends Action
/**
* @param callable $getProjectDB
* @param Device $deviceForFunctions
* @param Device $deviceForSites
* @param Device $deviceForBuilds
* @param Document $document
* @param Document $project
* @return void
* @throws Exception
*/
private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void
private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForSites, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void
{
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
@@ -1058,7 +1073,11 @@ class Deletes extends Action
/**
* Delete deployment files
*/
$this->deleteDeploymentFiles($deviceForFunctions, $document); //TODO: For sites, this should be deviceForSites
match ($document->getAttribute('resourceType')) {
'functions' => $this->deleteDeploymentFiles($deviceForFunctions, $document),
'sites' => $this->deleteDeploymentFiles($deviceForSites, $document),
default => throw new Exception('Invalid resource type')
};
/**
* Delete deployment screenshots
@@ -1081,8 +1100,8 @@ class Deletes extends Action
*/
Console::info("Deleting rules for deployment " . $deploymentId);
$this->deleteByGroup('rules', [
Query::equal('resourceType', ['deployment']),
Query::equal('resourceInternalId', [$deploymentInternalId]),
Query::equal('type', ['deployment']),
Query::equal('value', [$deploymentId]),
Query::equal('projectInternalId', [$project->getInternalId()])
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
$this->deleteRule($dbForPlatform, $document, $certificates);
+12
View File
@@ -113,6 +113,18 @@ abstract class Format
protected function getEnumName(string $service, string $method, string $param): ?string
{
switch ($service) {
case 'console':
switch ($method) {
case 'getResource':
switch ($param) {
case 'type':
return 'ConsoleResourceType';
case 'value':
return 'ConsoleResourceValue';
}
break;
}
break;
case 'account':
switch ($method) {
case 'createOAuth2Session':
@@ -6,8 +6,9 @@ class Rules extends Base
{
public const ALLOWED_ATTRIBUTES = [
'domain',
'resourceType',
'resourceId',
'type',
'value',
'automation',
'url'
];
@@ -66,6 +66,15 @@ class ConsoleVariables extends Model
'default' => '',
'example' => 'enabled',
]
)
->addRule(
'_APP_DOMAINS_NAMESERVERS',
[
'type' => self::TYPE_STRING,
'description' => 'Comma-separated list of nameservers.',
'default' => '',
'example' => 'ns1.example.com,ns2.example.com',
]
);
}
@@ -106,12 +106,6 @@ class Deployment extends Model
'default' => 0,
'example' => 128,
])
->addRule('domain', [
'type' => self::TYPE_STRING,
'description' => 'Preview domain.',
'default' => '',
'example' => 'deploy1-project1.appwrite.site',
])
->addRule('providerRepositoryName', [
'type' => self::TYPE_STRING,
'description' => 'The name of the vcs provider repository',
+13 -6
View File
@@ -34,17 +34,24 @@ class Rule extends Model
'default' => '',
'example' => 'appwrite.company.com',
])
->addRule('resourceType', [
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Action definition for the rule. Possible values are "api", "function", or "redirect"',
'description' => 'Action definition for the rule. Possible values are "api", "deployment", or "redirect"',
'default' => '',
'example' => 'function',
'example' => 'deployment',
])
->addRule('resourceId', [
->addRule('value', [
'type' => self::TYPE_STRING,
'description' => 'ID of resource for the action type. If resourceType is "api" or "url", it is empty. If resourceType is "function", it is ID of the function.',
'description' => 'Detail specification for the type. If type is "api", this is empty. If type is "redirect", this is URL. If type is "deployment", this is deployment ID.',
'default' => '',
'example' => 'myAwesomeFunction',
'example' => '67a9cf1a00150ee93abd',
])
->addRule('automation', [
'type' => self::TYPE_STRING,
'description' => 'Action that results in a rule update. If VCS branch, value can be of syntax "branch=[name]"',
'array' => false,
'default' => '',
'example' => 'branch=dev',
])
->addRule('status', [
'type' => self::TYPE_STRING,
+2 -3
View File
@@ -1101,15 +1101,14 @@ class UsageTest extends Scope
$rule = $this->client->call(
Client::METHOD_POST,
'/proxy/rules',
'/proxy/rules/function',
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()),
[
'domain' => 'test-' . ID::unique() . System::getEnv('_APP_DOMAIN_FUNCTIONS'),
'resourceType' => 'function',
'resourceId' => $functionId,
'functionId' => $functionId,
],
);
@@ -24,7 +24,7 @@ class ConsoleConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(9, $response['body']);
$this->assertCount(10, $response['body']);
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET']);
$this->assertIsInt($response['body']['_APP_STORAGE_LIMIT']);
$this->assertIsInt($response['body']['_APP_COMPUTE_SIZE_LIMIT']);
@@ -34,5 +34,7 @@ class ConsoleConsoleClientTest extends Scope
$this->assertIsBool($response['body']['_APP_ASSISTANT_ENABLED']);
$this->assertIsString($response['body']['_APP_DOMAIN_SITES']);
$this->assertIsString($response['body']['_APP_OPTIONS_FORCE_HTTPS']);
$this->assertIsString($response['body']['_APP_DOMAINS_NAMESERVERS']);
// When adding new keys, dont forget to update count a few lines above
}
}
@@ -274,13 +274,12 @@ trait FunctionsBase
protected function setupFunctionDomain(string $functionId, string $subdomain = ''): string
{
$subdomain = $subdomain ? $subdomain : ID::unique();
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', array_merge([
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_FUNCTIONS', ''),
'resourceType' => 'function',
'resourceId' => $functionId,
'functionId' => $functionId,
]);
$this->assertEquals(201, $rule['headers']['status-code']);
@@ -299,8 +298,8 @@ trait FunctionsBase
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$functionId])->toString(),
Query::equal('resourceType', ['function'])->toString(),
Query::equal('automation', ['function=' . $functionId])->toString(),
Query::equal('type', ['deployment'])->toString(),
],
]);
@@ -130,7 +130,7 @@ class FunctionsServerTest extends Scope
$deployment = $deployment['body']['data']['functionsGetDeployment'];
$this->assertEquals('ready', $deployment['status']);
});
}, 30000);
return $deployment;
}
@@ -24,14 +24,13 @@ class ProjectsCustomServerTest extends Scope
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]);
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'api',
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [
'domain' => 'api.appwrite.test',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [
'resourceType' => 'api',
'domain' => 'abc.test.io',
]);
@@ -39,8 +38,7 @@ class ProjectsCustomServerTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
// duplicate rule
$response2 = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'api',
$response2 = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [
'domain' => 'abc.test.io',
]);
@@ -52,8 +50,7 @@ class ProjectsCustomServerTest extends Scope
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'api',
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [
'domain' => $functionsDomain,
]);
@@ -62,24 +59,21 @@ class ProjectsCustomServerTest extends Scope
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'api',
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [
'domain' => $sitesDomain,
]);
$this->assertEquals(400, $response['headers']['status-code']);
// prevent functions domain
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'function',
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', $headers, [
'domain' => $functionsDomain,
]);
$this->assertEquals(400, $response['headers']['status-code']);
// prevent sites domain
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'site',
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', $headers, [
'domain' => $sitesDomain,
]);
@@ -98,8 +92,7 @@ class ProjectsCustomServerTest extends Scope
];
foreach ($deniedDomains as $deniedDomain) {
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [
'resourceType' => 'api',
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [
'domain' => $deniedDomain,
]);
+296
View File
@@ -0,0 +1,296 @@
<?php
namespace Tests\E2E\Services\Proxy;
use Appwrite\ID;
use Appwrite\Tests\Async;
use CURLFile;
use Tests\E2E\Client;
use Utopia\CLI\Console;
trait ProxyBase
{
use Async;
protected function listRules(array $params = []): mixed
{
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
return $rule;
}
protected function createAPIRule(string $domain): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $domain,
]);
return $rule;
}
protected function updateRuleVerification(string $ruleId): mixed
{
$rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/verification', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
return $rule;
}
protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $domain,
'siteId' => $siteId,
'branch' => $branch,
]);
return $rule;
}
protected function getRule(string $ruleId): mixed
{
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
return $rule;
}
protected function createRedirectRule(string $domain, string $target): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $domain,
'target' => $target,
]);
return $rule;
}
protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $domain,
'functionId' => $functionId,
'branch' => $branch,
]);
return $rule;
}
protected function deleteRule(string $ruleId): mixed
{
$rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
return $rule;
}
protected function setupAPIRule(string $domain): string
{
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
return $rule['body']['$id'];
}
protected function setupRedirectRule(string $domain, string $target): string
{
$rule = $this->createRedirectRule($domain, $target);
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
return $rule['body']['$id'];
}
protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string
{
$rule = $this->createFunctionRule($domain, $functionId, $branch);
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
return $rule['body']['$id'];
}
protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string
{
$rule = $this->createSiteRule($domain, $siteId, $branch);
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
return $rule['body']['$id'];
}
protected function cleanupRule(string $ruleId): void
{
$rule = $this->deleteRule($ruleId);
$this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule));
}
protected function cleanupSite(string $siteId): void
{
$site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site));
}
protected function cleanupFunction(string $functionId): void
{
$function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function));
}
protected function setupSite(): mixed
{
// Site
$site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'siteId' => ID::unique(),
'name' => 'Proxy site',
'framework' => 'other',
'adapter' => 'static',
'buildRuntime' => 'static-1',
'outputDirectory' => './',
'buildCommand' => '',
'installCommand' => '',
'fallbackFile' => '',
]);
$this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
$siteId = $site['body']['$id'];
// Deployment
$deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'code' => $this->packageSite('static'),
'activate' => 'true'
]);
$this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEventually(function () use ($siteId, $deploymentId) {
$site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
}, 100000, 500);
return ['siteId' => $siteId, 'deploymentId' => $deploymentId];
}
protected function setupFunction(): mixed
{
// Function
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'name' => 'Proxy Function',
'entrypoint' => 'index.js',
'commands' => '',
'execute' => ['any']
]);
$this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
$functionId = $function['body']['$id'];
// Deployment
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'code' => $this->packageFunction('node'),
'activate' => 'true'
]);
$this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $deploymentId) {
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals($deploymentId, $function['body']['deployment'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
}, 100000, 500);
return ['functionId' => $functionId, 'deploymentId' => $deploymentId];
}
private function packageSite(string $site): CURLFile
{
$stdout = '';
$stderr = '';
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
}
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
}
private function packageFunction(string $function): CURLFile
{
$stdout = '';
$stderr = '';
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
}
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
}
}
@@ -0,0 +1,457 @@
<?php
namespace Tests\E2E\Services\Proxy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\App;
use Utopia\Database\Query;
class ProxyCustomServerTest extends Scope
{
use ProxyBase;
use ProjectCustom;
use SideServer;
public function testCreateRule(): void
{
$domain = \uniqid() . '-api.myapp.com';
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals($domain, $rule['body']['domain']);
$this->assertArrayHasKey('$id', $rule['body']);
$this->assertArrayHasKey('type', $rule['body']);
$this->assertArrayHasKey('value', $rule['body']);
$this->assertArrayHasKey('automation', $rule['body']);
$this->assertArrayHasKey('status', $rule['body']);
$this->assertArrayHasKey('logs', $rule['body']);
$this->assertArrayHasKey('renewAt', $rule['body']);
$ruleId = $rule['body']['$id'];
$rule = $this->createAPIRule($domain);
$this->assertEquals(409, $rule['headers']['status-code']);
$rule = $this->deleteRule($ruleId);
$this->assertEquals(204, $rule['headers']['status-code']);
}
public function testCreateRuleSetup(): void
{
$ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com');
$this->cleanupRule($ruleId);
}
public function testCreateRuleApex(): void
{
$rule = $this->createAPIRule('myapp.com');
$this->assertEquals(400, $rule['headers']['status-code']);
}
public function testCreateRuleVcs(): void
{
$domain = \uniqid() . '-vcs.myapp.com';
$rule = $this->createAPIRule('commit-' . $domain);
$this->assertEquals(400, $rule['headers']['status-code']);
$rule = $this->createAPIRule('branch-' . $domain);
$this->assertEquals(400, $rule['headers']['status-code']);
$rule = $this->createAPIRule('anything-' . $domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->cleanupRule($rule['body']['$id']);
}
public function testCreateAPIRule(): void
{
$domain = \uniqid() . '-api.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
// We should ideally assert 400, but server allows unknown domains, and serves API by default
$response = $proxyClient->call(Client::METHOD_GET, '/versions');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(APP_VERSION_STABLE, $response['body']['server']);
$ruleId = $this->setupAPIRule($domain);
$this->assertNotEmpty($ruleId);
$response = $proxyClient->call(Client::METHOD_GET, '/versions');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(APP_VERSION_STABLE, $response['body']['server']);
$this->cleanupRule($ruleId);
$rule = $this->createAPIRule('http://' . $domain);
$this->assertEquals(400, $rule['headers']['status-code']);
$rule = $this->createAPIRule('https://' . $domain);
$this->assertEquals(400, $rule['headers']['status-code']);
// Unexpected I would say, but it is the current behaviour
$rule = $this->createAPIRule('wss://' . $domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->cleanupRule($rule['body']['$id']);
// Unexpected I would say, but it is the current behaviour
$rule = $this->createAPIRule($domain . '/some-path');
$this->assertEquals(201, $rule['headers']['status-code']);
$this->cleanupRule($rule['body']['$id']);
}
public function testCreateRedirectRule(): void
{
$domain = \uniqid() . '-redirect.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
$this->assertEquals(404, $response['headers']['status-code']);
$ruleId = $this->setupRedirectRule($domain, 'jsonplaceholder.typicode.com');
$this->assertNotEmpty($ruleId);
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['id']);
$this->cleanupRule($ruleId);
}
public function testCreateFunctionRule(): void
{
$domain = \uniqid() . '-function.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/ping');
$this->assertEquals(404, $response['headers']['status-code']);
$setup = $this->setupFunction();
$functionId = $setup['functionId'];
$deploymentId = $setup['deploymentId'];
$this->assertNotEmpty($functionId);
$this->assertNotEmpty($deploymentId);
$ruleId = $this->setupFunctionRule($domain, $functionId);
$this->assertNotEmpty($ruleId);
$response = $proxyClient->call(Client::METHOD_GET, '/ping');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']);
$this->cleanupRule($ruleId);
$this->cleanupFunction($functionId);
$this->assertEventually(function () use ($functionId, $deploymentId) {
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('automation', ['function=' . $functionId])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('value', [$deploymentId])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
});
}
public function testCreateSiteRule(): void
{
$domain = \uniqid() . '-site.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/contact');
$this->assertEquals(404, $response['headers']['status-code']);
$setup = $this->setupSite();
$siteId = $setup['siteId'];
$deploymentId = $setup['deploymentId'];
$this->assertNotEmpty($siteId);
$this->assertNotEmpty($deploymentId);
$ruleId = $this->setupSiteRule($domain, $siteId);
$this->assertNotEmpty($ruleId);
$response = $proxyClient->call(Client::METHOD_GET, '/contact');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString('Contact page', $response['body']);
$this->cleanupRule($ruleId);
$this->cleanupSite($siteId);
$this->assertEventually(function () use ($siteId, $deploymentId) {
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('automation', ['site=' . $siteId])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('value', [$deploymentId])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
});
}
public function testCreatSiteBranchRule(): void
{
$domain = \uniqid() . '-site-branch.custom.localhost';
$setup = $this->setupSite();
$siteId = $setup['siteId'];
$deploymentId = $setup['deploymentId'];
$this->assertNotEmpty($siteId);
$this->assertNotEmpty($deploymentId);
$ruleId = $this->setupSiteRule($domain, $siteId, 'dev');
$this->assertNotEmpty($ruleId);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals('branch=dev', $rule['body']['automation']);
$this->cleanupRule($ruleId);
}
public function testCreatFunctionBranchRule(): void
{
$domain = \uniqid() . '-function-branch.custom.localhost';
$setup = $this->setupFunction();
$functionId = $setup['functionId'];
$deploymentId = $setup['deploymentId'];
$this->assertNotEmpty($functionId);
$this->assertNotEmpty($deploymentId);
$ruleId = $this->setupFunctionRule($domain, $functionId, 'dev');
$this->assertNotEmpty($ruleId);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals('branch=dev', $rule['body']['automation']);
$this->cleanupRule($ruleId);
}
public function testUpdateRule(): void
{
// Create function appwrite-network domain
$domain = \uniqid() . '-cname-api.' . App::getEnv('_APP_DOMAIN_FUNCTIONS');
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('verified', $rule['body']['status']);
$this->cleanupRule($rule['body']['$id']);
// Create site appwrite-network domain
$domain = \uniqid() . '-cname-api.' . App::getEnv('_APP_DOMAIN_SITES');
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('verified', $rule['body']['status']);
$this->cleanupRule($rule['body']['$id']);
// Create + update
$domain = \uniqid() . '-cname-api.custom.localhost';
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('created', $rule['body']['status']);
$ruleId = $rule['body']['$id'];
$rule = $this->updateRuleVerification($ruleId);
$this->assertEquals(401, $rule['headers']['status-code']);
$this->cleanupRule($ruleId);
}
public function testGetRule()
{
$domain = \uniqid() . '-get.custom.localhost';
$ruleId = $this->setupAPIRule($domain);
$this->assertNotEmpty($ruleId);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals($domain, $rule['body']['domain']);
$this->assertArrayHasKey('$id', $rule['body']);
$this->assertArrayHasKey('type', $rule['body']);
$this->assertArrayHasKey('value', $rule['body']);
$this->assertArrayHasKey('automation', $rule['body']);
$this->assertArrayHasKey('status', $rule['body']);
$this->assertArrayHasKey('logs', $rule['body']);
$this->assertArrayHasKey('renewAt', $rule['body']);
$this->cleanupRule($ruleId);
}
public function testListRules()
{
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
foreach ($rules['body']['rules'] as $rule) {
$rule = $this->deleteRule($rule['$id']);
$this->assertEquals(204, $rule['headers']['status-code']);
}
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
$rule1Domain = \uniqid() . '-list1.custom.localhost';
$rule1Id = $this->setupAPIRule($rule1Domain);
$this->assertNotEmpty($rule1Id);
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(1, $rules['body']['total']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
$this->assertArrayHasKey('$id', $rules['body']['rules'][0]);
$this->assertArrayHasKey('type', $rules['body']['rules'][0]);
$this->assertArrayHasKey('value', $rules['body']['rules'][0]);
$this->assertArrayHasKey('automation', $rules['body']['rules'][0]);
$this->assertArrayHasKey('status', $rules['body']['rules'][0]);
$this->assertArrayHasKey('logs', $rules['body']['rules'][0]);
$this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]);
$rule2Domain = \uniqid() . '-list1.custom.localhost';
$rule2Id = $this->setupAPIRule($rule2Domain);
$this->assertNotEmpty($rule2Id);
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(2, $rules['body']['total']);
$this->assertCount(2, $rules['body']['rules']);
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(2, $rules['body']['total']);
$this->assertCount(1, $rules['body']['rules']);
$rules = $this->listRules([
'queries' => [
Query::equal('$id', [$rule1Id])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
$rules = $this->listRules([
'queries' => [
Query::orderDesc('$id')->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertCount(2, $rules['body']['rules']);
$this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
$rules = $this->listRules([
'queries' => [
Query::equal('domain', [$rule2Domain])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
$rules = $this->listRules([
'search' => $rule1Domain,
'queries' => [ Query::orderDesc('$createdAt') ]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$ruleIds = \array_column($rules['body']['rules'], '$id');
$this->assertContains($rule1Id, $ruleIds);
$rules = $this->listRules([
'search' => $rule2Domain,
'queries' => [ Query::orderDesc('$createdAt') ]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$ruleIds = \array_column($rules['body']['rules'], '$id');
$this->assertContains($rule2Id, $ruleIds);
$rules = $this->listRules([
'search' => $rule1Id,
'queries' => [ Query::orderDesc('$createdAt') ]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$ruleDomains = \array_column($rules['body']['rules'], 'domain');
$this->assertContains($rule1Domain, $ruleDomains);
$rules = $this->listRules([
'search' => $rule2Id,
'queries' => [ Query::orderDesc('$createdAt') ]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$ruleDomains = \array_column($rules['body']['rules'], 'domain');
$this->assertContains($rule2Domain, $ruleDomains);
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
foreach ($rules['body']['rules'] as $rule) {
$rule = $this->deleteRule($rule['$id']);
$this->assertEquals(204, $rule['headers']['status-code']);
}
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
}
}
+7 -8
View File
@@ -284,13 +284,12 @@ trait SitesBase
protected function setupSiteDomain(string $siteId, string $subdomain = ''): string
{
$subdomain = $subdomain ? $subdomain : ID::unique();
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', array_merge([
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''),
'resourceType' => 'site',
'resourceId' => $siteId,
'siteId' => $siteId,
]);
$this->assertEquals(201, $rule['headers']['status-code']);
@@ -309,8 +308,8 @@ trait SitesBase
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$siteId])->toString(),
Query::equal('resourceType', ['site'])->toString(),
Query::equal('automation', ['site=' . $siteId])->toString(),
Query::equal('type', ['deployment'])->toString(),
],
]);
@@ -324,7 +323,6 @@ trait SitesBase
return $domain;
}
protected function getDeploymentDomain(string $deploymentId): string
{
$rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
@@ -332,8 +330,9 @@ trait SitesBase
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$deploymentId])->toString(),
Query::equal('resourceType', ['deployment'])->toString(),
Query::equal('value', [$deploymentId])->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('automation', [''])->toString(),
],
]);
@@ -79,7 +79,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($siteId);
$rule = $this->setupSiteDomain($siteId);
$domain = $this->setupSiteDomain($siteId);
$response = $this->client->call(Client::METHOD_GET, '/console/resources', [
'origin' => 'http://localhost',
@@ -88,7 +88,7 @@ class SitesCustomServerTest extends Scope
'x-appwrite-project' => 'console',
], [
'type' => 'rules',
'value' => $rule,
'value' => $domain,
]);
$this->assertEquals(409, $response['headers']['status-code']); // domain unavailable
@@ -115,7 +115,7 @@ class SitesCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('resourceId', [$siteId])
Query::equal('automation', ['site=' . $siteId])
]
]);
@@ -130,7 +130,7 @@ class SitesCustomServerTest extends Scope
'x-appwrite-project' => 'console',
], [
'type' => 'rules',
'value' => $rule,
'value' => $domain,
]);
$this->assertEquals(204, $response['headers']['status-code']); // domain available as site is deleted
@@ -273,6 +273,8 @@ class SitesCustomServerTest extends Scope
$this->cleanupSite($siteId);
}
// This is first Sites test with Proxy
// If this fails, it may not be related to variables; but Router flow failing
public function testVariablesE2E(): void
{
$siteId = $this->setupSite([
@@ -1008,41 +1010,6 @@ class SitesCustomServerTest extends Scope
$this->cleanupSite($siteId);
}
// public function testLoadSite(): void
// {
// $site = $this->createSite([
// 'buildRuntime' => 'ssr-22',
// 'fallbackFile' => null,
// 'framework' => 'other',
// 'name' => 'Test Site',
// 'outputDirectory' => './',
// 'providerBranch' => 'main',
// 'providerRootDirectory' => './',
// 'siteId' => ID::unique()
// ]);
// $siteId = $site['body']['$id'] ?? '';
// $this->assertNotEmpty($siteId);
// $deployment = $this->createDeployment($siteId, [
// 'code' => $this->packageSite('static'),
// 'activate' => 'false'
// ]);
// $deploymentId = $deployment['body']['$id'] ?? '';
// $this->assertEventually(function () use ($siteId, $deploymentId) {
// $deployment = $this->getDeployment($siteId, $deploymentId);
// $this->assertEquals('ready', $deployment['body']['status']);
// }, 30000, 300);
// // get rule for this site from rules collection
// $response = $this->client->call(Client::METHOD_GET, $domain);
// var_dump($response);
// }
public function testUpdateSpecs(): void
{
$siteId = $this->setupSite([
@@ -1205,6 +1172,118 @@ class SitesCustomServerTest extends Scope
$this->assertArrayHasKey('adapters', $framework);
}
public function testSiteStatic(): void
{
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Non-SPA site',
'framework' => 'other',
'adapter' => 'static',
'buildRuntime' => 'static-1',
'outputDirectory' => './',
'buildCommand' => '',
'installCommand' => '',
'fallbackFile' => '',
]);
$this->assertNotEmpty($siteId);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static-spa'),
'activate' => 'true'
]);
$this->assertNotEmpty($deploymentId);
$domain = $this->setupSiteDomain($siteId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Index page", $response['body']);
$response = $proxyClient->call(Client::METHOD_GET, '/contact', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Contact page", $response['body']);
$response = $proxyClient->call(Client::METHOD_GET, '/non-existing', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertStringContainsString("Page not found", $response['body']); // Title
$this->assertStringContainsString("Go to homepage", $response['body']); // Button
$this->assertStringContainsString("Powered by", $response['body']); // Brand
$this->cleanupSite($siteId);
}
public function testSiteStaticSPA(): void
{
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'SPA site',
'framework' => 'other',
'adapter' => 'static',
'buildRuntime' => 'static-1',
'outputDirectory' => './',
'buildCommand' => '',
'installCommand' => '',
'fallbackFile' => '404.html',
]);
$this->assertNotEmpty($siteId);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static-spa'),
'activate' => 'true'
]);
$this->assertNotEmpty($deploymentId);
$domain = $this->setupSiteDomain($siteId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Index page", $response['body']);
$response = $proxyClient->call(Client::METHOD_GET, '/contact', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Contact page", $response['body']);
$response = $proxyClient->call(Client::METHOD_GET, '/non-existing', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Customized 404 page", $response['body']);
$this->assertStringNotContainsString("Powered by", $response['body']); // Brand
$this->cleanupSite($siteId);
}
public function testSiteTemplate(): void
{
$template = $this->getTemplate('astro-starter');
@@ -1310,17 +1389,15 @@ class SitesCustomServerTest extends Scope
$siteId2 = $site2['body']['$id'];
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', array_merge([
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''),
'resourceType' => 'site',
'resourceId' => $siteId2,
'siteId' => $siteId2,
]);
$this->assertEquals(409, $rule['headers']['status-code']);
$this->assertStringContainsString("Document with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.", $rule['body']['message']);
$this->cleanupSite($siteId);
@@ -1548,6 +1625,12 @@ class SitesCustomServerTest extends Scope
$this->assertNotEquals($screenshotDarkHash, $screenshotHash);
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin");
$this->assertEquals(404, $file['headers']['status-code']);
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin");
$this->assertEquals(404, $file['headers']['status-code']);
$this->cleanupSite($siteId);
}
+10
View File
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Customized 404 page</h1>
</body>
</html>
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Contact page</h1>
</body>
</html>
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Index page</h1>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact page</title>
</head>
<body>
<h1>Contact page</h1>
</body>
</html>