From 6e16340f16c7388e3e2fdfe1c73659052d392c11 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sat, 13 Sep 2025 17:56:19 +0530 Subject: [PATCH 01/35] feat: add branch deployments to appwrite --- .../specs/open-api3-latest-console.json | 151 ++++++++++-- app/config/specs/open-api3-latest-server.json | 126 ++++++++-- app/config/specs/swagger2-latest-console.json | 159 ++++++++++-- app/config/specs/swagger2-latest-server.json | 134 ++++++++-- .../Modules/Functions/Workers/Builds.php | 12 +- .../Sites/Http/Deployments/Direct/Create.php | 231 ++++++++++++++++++ .../Platform/Modules/Sites/Services/Http.php | 2 + 7 files changed, 731 insertions(+), 84 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 02d97fffc7..0de652e3fb 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -4888,7 +4888,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 496, + "weight": 497, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -28209,7 +28209,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 502, + "weight": 503, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28283,7 +28283,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 497, + "weight": 498, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28350,7 +28350,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 499, + "weight": 500, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28428,7 +28428,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 500, + "weight": 501, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28541,7 +28541,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 498, + "weight": 499, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28619,7 +28619,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 501, + "weight": 502, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28670,7 +28670,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 503, + "weight": 504, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28730,7 +28730,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 504, + "weight": 505, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -29085,6 +29085,103 @@ } } }, + "\/sites\/direct": { + "post": { + "summary": "Create direct deployment", + "operationId": "sitesCreateDirectDeployment", + "tags": [ + "sites" + ], + "description": "Create a deployment directly from a repository branch.", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createDirectDeployment", + "group": "deployments", + "weight": 483, + "cookies": false, + "type": "", + "demo": "sites\/create-direct-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "Site ID.", + "x-example": "" + }, + "repository": { + "type": "string", + "description": "Repository name of the template.", + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Branch to create deployment from.", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "siteId", + "repository", + "owner", + "rootDirectory", + "branch" + ] + } + } + } + } + } + }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -29158,7 +29255,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29208,7 +29305,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 491, + "weight": 492, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29308,7 +29405,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 492, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29368,7 +29465,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 493, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -30637,7 +30734,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30708,7 +30805,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30770,7 +30867,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -30841,7 +30938,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 494, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -30923,7 +31020,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -30982,7 +31079,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31073,7 +31170,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31142,7 +31239,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31233,7 +31330,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -39984,7 +40081,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -40064,7 +40161,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40153,7 +40250,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40213,7 +40310,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40283,7 +40380,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 510, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 09d53dbdf0..d8d7178eb2 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -20014,6 +20014,104 @@ } } }, + "\/sites\/direct": { + "post": { + "summary": "Create direct deployment", + "operationId": "sitesCreateDirectDeployment", + "tags": [ + "sites" + ], + "description": "Create a deployment directly from a repository branch.", + "responses": { + "202": { + "description": "Deployment", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/deployment" + } + } + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createDirectDeployment", + "group": "deployments", + "weight": 483, + "cookies": false, + "type": "", + "demo": "sites\/create-direct-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "Site ID.", + "x-example": "" + }, + "repository": { + "type": "string", + "description": "Repository name of the template.", + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Branch to create deployment from.", + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "x-example": false + } + }, + "required": [ + "siteId", + "repository", + "owner", + "rootDirectory", + "branch" + ] + } + } + } + } + } + }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -20088,7 +20186,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -21349,7 +21447,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21421,7 +21519,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21484,7 +21582,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21556,7 +21654,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21616,7 +21714,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21708,7 +21806,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21778,7 +21876,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -21870,7 +21968,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -30024,7 +30122,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30105,7 +30203,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30195,7 +30293,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30256,7 +30354,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30327,7 +30425,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 510, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 6d5721c73b..22823cd40e 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -5052,7 +5052,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 496, + "weight": 497, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -28351,7 +28351,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 502, + "weight": 503, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28424,7 +28424,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 497, + "weight": 498, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28494,7 +28494,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 499, + "weight": 500, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28577,7 +28577,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 500, + "weight": 501, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28697,7 +28697,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 498, + "weight": 499, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28778,7 +28778,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 501, + "weight": 502, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28831,7 +28831,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 503, + "weight": 504, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28891,7 +28891,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 504, + "weight": 505, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -29264,6 +29264,111 @@ ] } }, + "\/sites\/direct": { + "post": { + "summary": "Create direct deployment", + "operationId": "sitesCreateDirectDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "sites" + ], + "description": "Create a deployment directly from a repository branch.", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createDirectDeployment", + "group": "deployments", + "weight": 483, + "cookies": false, + "type": "", + "demo": "sites\/create-direct-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "Site ID.", + "default": null, + "x-example": "" + }, + "repository": { + "type": "string", + "description": "Repository name of the template.", + "default": null, + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "default": null, + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "default": null, + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Branch to create deployment from.", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": true, + "x-example": false + } + }, + "required": [ + "siteId", + "repository", + "owner", + "rootDirectory", + "branch" + ] + } + } + ] + } + }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -29337,7 +29442,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29387,7 +29492,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 491, + "weight": 492, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29481,7 +29586,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 492, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29539,7 +29644,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 493, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -30811,7 +30916,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30882,7 +30987,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30946,7 +31051,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -31013,7 +31118,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 494, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -31091,7 +31196,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -31150,7 +31255,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31240,7 +31345,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31307,7 +31412,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31399,7 +31504,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -39916,7 +40021,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -39996,7 +40101,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40080,7 +40185,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40140,7 +40245,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40211,7 +40316,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 510, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index 98077f1050..c7a5a20c24 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -20226,6 +20226,112 @@ ] } }, + "\/sites\/direct": { + "post": { + "summary": "Create direct deployment", + "operationId": "sitesCreateDirectDeployment", + "consumes": [ + "application\/json" + ], + "produces": [ + "application\/json" + ], + "tags": [ + "sites" + ], + "description": "Create a deployment directly from a repository branch.", + "responses": { + "202": { + "description": "Deployment", + "schema": { + "$ref": "#\/definitions\/deployment" + } + } + }, + "deprecated": false, + "x-appwrite": { + "method": "createDirectDeployment", + "group": "deployments", + "weight": 483, + "cookies": false, + "type": "", + "demo": "sites\/create-direct-deployment.md", + "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", + "rate-limit": 0, + "rate-time": 3600, + "rate-key": "url:{url},ip:{ip}", + "scope": "sites.write", + "platforms": [ + "server" + ], + "packaging": false, + "auth": { + "Project": [], + "Key": [] + } + }, + "security": [ + { + "Project": [], + "Key": [] + } + ], + "parameters": [ + { + "name": "payload", + "in": "body", + "schema": { + "type": "object", + "properties": { + "siteId": { + "type": "string", + "description": "Site ID.", + "default": null, + "x-example": "" + }, + "repository": { + "type": "string", + "description": "Repository name of the template.", + "default": null, + "x-example": "" + }, + "owner": { + "type": "string", + "description": "The name of the owner of the template.", + "default": null, + "x-example": "" + }, + "rootDirectory": { + "type": "string", + "description": "Path to site code in the template repo.", + "default": null, + "x-example": "" + }, + "branch": { + "type": "string", + "description": "Branch to create deployment from.", + "default": null, + "x-example": "" + }, + "activate": { + "type": "boolean", + "description": "Automatically activate the deployment when it is finished building.", + "default": true, + "x-example": false + } + }, + "required": [ + "siteId", + "repository", + "owner", + "rootDirectory", + "branch" + ] + } + } + ] + } + }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -20300,7 +20406,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 495, + "weight": 496, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -21566,7 +21672,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 484, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21638,7 +21744,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 483, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21703,7 +21809,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 485, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21771,7 +21877,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 488, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21831,7 +21937,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 486, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21922,7 +22028,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 487, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21990,7 +22096,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 489, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -22083,7 +22189,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 490, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -30036,7 +30142,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 507, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30117,7 +30223,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 505, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30202,7 +30308,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 506, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30263,7 +30369,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 508, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30335,7 +30441,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 509, + "weight": 510, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 9547a752ef..2ac05eae77 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -311,19 +311,27 @@ class Builds extends Action $templateRepositoryName = $template->getAttribute('repositoryName', ''); $templateOwnerName = $template->getAttribute('ownerName', ''); $templateVersion = $template->getAttribute('version', ''); + $templateBranch = $template->getAttribute('branch', ''); $templateRootDirectory = $template->getAttribute('rootDirectory', ''); $templateRootDirectory = \rtrim($templateRootDirectory, '/'); $templateRootDirectory = \ltrim($templateRootDirectory, '.'); $templateRootDirectory = \ltrim($templateRootDirectory, '/'); - if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateVersion)) { + if (!empty($templateRepositoryName) && !empty($templateOwnerName) && (!empty($templateVersion) || !empty($templateBranch))) { $stdout = ''; $stderr = ''; // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template'; - $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); + + if(empty($templateVersion)) { + $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateBranch, GitHub::CLONE_TYPE_BRANCH, $tmpTemplateDirectory, $templateRootDirectory); + } else { + // True template + $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); + } + $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); if ($exit !== 0) { diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php new file mode 100644 index 0000000000..092a7702df --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php @@ -0,0 +1,231 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/sites/direct') + ->desc('Create direct deployment') + ->groups(['api', 'sites']) + ->label('scope', 'sites.write') + ->label('resourceType', RESOURCE_TYPE_SITES) + ->label('event', 'sites.[siteId].deployments.[deploymentId].create') + ->label('audits.event', 'deployment.create') + ->label('audits.resource', 'site/{request.siteId}') + ->label('sdk', new Method( + namespace: 'sites', + group: 'deployments', + name: 'createDirectDeployment', + description: <<param('siteId', '', new UID(), 'Site ID.') + ->param('repository', '', new Text(128, 0), 'Repository name of the template.') + ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') + ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') + ->param('branch', '', new Text(128, 0), 'Branch to create deployment from.') + ->param('activate', true, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->inject('dbForPlatform') + ->inject('project') + ->inject('queueForEvents') + ->inject('queueForBuilds') + ->inject('gitHub') + ->callback($this->action(...)); + } + + public function action( + string $siteId, + string $repository, + string $owner, + string $rootDirectory, + string $branch, + bool $activate, + Request $request, + Response $response, + Database $dbForProject, + Database $dbForPlatform, + Document $project, + Event $queueForEvents, + Build $queueForBuilds, + GitHub $github + ) { + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $template = new Document([ + 'repositoryName' => $repository, + 'ownerName' => $owner, + 'rootDirectory' => $rootDirectory, + 'branch' => $branch + ]); + + + if (!empty($site->getAttribute('providerRepositoryId'))) { + $installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId')); + + $deployment = $this->redeployVcsSite( + request: $request, + site: $site, + project: $project, + installation: $installation, + dbForProject: $dbForProject, + dbForPlatform: $dbForPlatform, + queueForBuilds: $queueForBuilds, + template: $template, + github: $github, + activate: $activate, + ); + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + + return; + } + + $branchUrl = "https://github.com/$owner/$repository/tree/$branch"; + $repositoryUrl = "https://github.com/$owner/$repository"; + + try { + $commitDetails = $github->getLatestCommit($owner, $repository, $branch); + } catch (\Throwable $error) { + // Ignore; deployment can continue + } + + $commands = []; + if (!empty($site->getAttribute('installCommand', ''))) { + $commands[] = $site->getAttribute('installCommand', ''); + } + if (!empty($site->getAttribute('buildCommand', ''))) { + $commands[] = $site->getAttribute('buildCommand', ''); + } + + $deploymentId = ID::unique(); + $deployment = $dbForProject->createDocument('deployments', new Document([ + '$id' => $deploymentId, + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + 'resourceId' => $site->getId(), + 'resourceInternalId' => $site->getSequence(), + 'resourceType' => 'sites', + 'buildCommands' => \implode(' && ', $commands), + 'buildOutput' => $site->getAttribute('outputDirectory', ''), + 'adapter' => $site->getAttribute('adapter', ''), + 'fallbackFile' => $site->getAttribute('fallbackFile', ''), + 'providerRepositoryName' => $repository, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerBranchUrl' => $branchUrl, + 'providerBranch' => $branch, + 'providerCommitHash' => $commitDetails['commitHash'] ?? '', + 'providerCommitAuthorUrl' => $commitDetails['commitAuthorUrl'] ?? '', + 'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '', + 'providerCommitMessage' => mb_strimwidth($commitDetails['commitMessage'] ?? '', 0, 255, '...'), + 'providerCommitUrl' => $commitDetails['commitUrl'] ?? '', + 'type' => 'vcs', + 'activate' => $activate, + ])); + + $site = $site + ->setAttribute('latestDeploymentId', $deployment->getId()) + ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) + ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) + ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); + $dbForProject->updateDocument('sites', $site->getId(), $site); + + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $domain = ID::unique() . "." . $sitesDomain; + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); + + Authorization::skip( + fn () => $dbForPlatform->createDocument('rules', new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getSequence(), + 'domain' => $domain, + 'type' => 'deployment', + 'trigger' => 'deployment', + 'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(), + 'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(), + 'deploymentResourceType' => 'site', + 'deploymentResourceId' => $site->getId(), + 'deploymentResourceInternalId' => $site->getSequence(), + 'status' => 'verified', + 'certificateId' => '', + 'owner' => 'Appwrite', + 'region' => $project->getAttribute('region') + ])) + ); + + $queueForBuilds + ->setType(BUILD_TYPE_DEPLOYMENT) + ->setResource($site) + ->setDeployment($deployment) + ->setTemplate($template); + + $queueForEvents + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_ACCEPTED) + ->dynamic($deployment, Response::MODEL_DEPLOYMENT); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index 6bd151f97e..f19b9b6d71 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -6,6 +6,7 @@ use Appwrite\Platform\Modules\Sites\Http\Deployments\Create as CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Delete as DeleteDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Download\Get as DownloadDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Duplicate\Create as CreateDuplicateDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\Direct\Create as CreateDirectDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Get as GetDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Status\Update as UpdateDeploymentStatus; use Appwrite\Platform\Modules\Sites\Http\Deployments\Template\Create as CreateTemplateDeployment; @@ -60,6 +61,7 @@ class Http extends Service $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); $this->addAction(CreateDuplicateDeployment::getName(), new CreateDuplicateDeployment()); $this->addAction(UpdateDeploymentStatus::getName(), new UpdateDeploymentStatus()); + $this->addAction(CreateDirectDeployment::getName(), new CreateDirectDeployment()); // Logs $this->addAction(GetLog::getName(), new GetLog()); From 4858317891faa2a5a0bdcaf4ea3f976d223ae125 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sat, 13 Sep 2025 18:02:13 +0530 Subject: [PATCH 02/35] fix: format, lints --- .../Platform/Modules/Functions/Workers/Builds.php | 4 ++-- .../Modules/Sites/Http/Deployments/Direct/Create.php | 8 ++++---- src/Appwrite/Platform/Modules/Sites/Services/Http.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 2ac05eae77..62ee96da47 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -324,8 +324,8 @@ class Builds extends Action // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template'; - - if(empty($templateVersion)) { + + if (empty($templateVersion)) { $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateBranch, GitHub::CLONE_TYPE_BRANCH, $tmpTemplateDirectory, $templateRootDirectory); } else { // True template diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php index 092a7702df..d1df994149 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php @@ -106,7 +106,7 @@ class Create extends Base 'rootDirectory' => $rootDirectory, 'branch' => $branch ]); - + if (!empty($site->getAttribute('providerRepositoryId'))) { $installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId')); @@ -137,11 +137,11 @@ class Create extends Base $branchUrl = "https://github.com/$owner/$repository/tree/$branch"; $repositoryUrl = "https://github.com/$owner/$repository"; - + try { - $commitDetails = $github->getLatestCommit($owner, $repository, $branch); + $commitDetails = $github->getLatestCommit($owner, $repository, $branch); } catch (\Throwable $error) { - // Ignore; deployment can continue + // Ignore; deployment can continue } $commands = []; diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index f19b9b6d71..437356d4e2 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -4,9 +4,9 @@ namespace Appwrite\Platform\Modules\Sites\Services; use Appwrite\Platform\Modules\Sites\Http\Deployments\Create as CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Delete as DeleteDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\Direct\Create as CreateDirectDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Download\Get as DownloadDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Duplicate\Create as CreateDuplicateDeployment; -use Appwrite\Platform\Modules\Sites\Http\Deployments\Direct\Create as CreateDirectDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Get as GetDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Status\Update as UpdateDeploymentStatus; use Appwrite\Platform\Modules\Sites\Http\Deployments\Template\Create as CreateTemplateDeployment; From 2b1e3bd9477941431f3f5055ea118b1fe62e9067 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sun, 14 Sep 2025 15:15:44 +0530 Subject: [PATCH 03/35] delete the new route and just do it in template route --- .../Http/Deployments/Template/Create.php | 26 ++++++++++++++++--- .../Modules/Functions/Workers/Builds.php | 13 +++------- .../Http/Deployments/Template/Create.php | 26 ++++++++++++++++--- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 4d93c8e8cd..885e23bfa1 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -65,7 +65,9 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.') + ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) + ->param('type', '', new Text(128, 0), 'Type for the reference provided. Can be commit, branch, or version', true) + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -84,6 +86,8 @@ class Create extends Base string $owner, string $rootDirectory, string $version, + string $type, + string $reference, bool $activate, Request $request, Response $response, @@ -100,11 +104,22 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } + if (empty($version) && empty($type) && empty($reference)) { + throw new Exception("Either version or type & reference must be provided"); + } + + $referenceType = !empty($version) ? GitHub::CLONE_TYPE_TAG : $type; + $referenceValue = !empty($version) ? $version : $reference; + + $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$referenceValue" : ""; + $repositoryUrl = "https://github.com/$owner/$repository"; + $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'version' => $version + 'referenceType' => $referenceType, + 'referenceValue' => $referenceValue, ]); if (!empty($function->getAttribute('providerRepositoryId'))) { @@ -146,7 +161,12 @@ class Create extends Base 'resourceType' => 'functions', 'entrypoint' => $function->getAttribute('entrypoint', ''), 'buildCommands' => $function->getAttribute('commands', ''), - 'type' => 'manual', + 'providerRepositoryName' => $repository, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerBranchUrl' => $branchUrl, + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $referenceValue : '', + 'type' => 'vcs', 'activate' => $activate, ])); diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 62ee96da47..4bb49a42c8 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -310,27 +310,22 @@ class Builds extends Action // Non-VCS + Template $templateRepositoryName = $template->getAttribute('repositoryName', ''); $templateOwnerName = $template->getAttribute('ownerName', ''); - $templateVersion = $template->getAttribute('version', ''); - $templateBranch = $template->getAttribute('branch', ''); + $templateReferenceType = $template->getAttribute('referenceType', ''); + $templateReferenceValue = $template->getAttribute('referenceValue', ''); $templateRootDirectory = $template->getAttribute('rootDirectory', ''); $templateRootDirectory = \rtrim($templateRootDirectory, '/'); $templateRootDirectory = \ltrim($templateRootDirectory, '.'); $templateRootDirectory = \ltrim($templateRootDirectory, '/'); - if (!empty($templateRepositoryName) && !empty($templateOwnerName) && (!empty($templateVersion) || !empty($templateBranch))) { + if (!empty($templateRepositoryName) && !empty($templateOwnerName) && !empty($templateReferenceType) && !empty($templateReferenceValue)) { $stdout = ''; $stderr = ''; // Clone template repo $tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template'; - if (empty($templateVersion)) { - $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateBranch, GitHub::CLONE_TYPE_BRANCH, $tmpTemplateDirectory, $templateRootDirectory); - } else { - // True template - $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateVersion, GitHub::CLONE_TYPE_TAG, $tmpTemplateDirectory, $templateRootDirectory); - } + $gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateReferenceValue, $templateReferenceType, $tmpTemplateDirectory, $templateRootDirectory); $exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index a2040d830b..0799e0b51f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -67,7 +67,9 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the site template.') + ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) + ->param('type', '', new Text(128, 0), 'Type for the reference provided. Can be commit, branch, or version', true) + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -86,6 +88,8 @@ class Create extends Base string $owner, string $rootDirectory, string $version, + string $type, + string $reference, bool $activate, Request $request, Response $response, @@ -102,11 +106,22 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } + if (empty($version) && empty($type) && empty($reference)) { + throw new Exception("Either version or type & reference must be provided"); + } + + $referenceType = !empty($version) ? GitHub::CLONE_TYPE_TAG : $type; + $referenceValue = !empty($version) ? $version : $reference; + + $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$referenceValue" : ""; + $repositoryUrl = "https://github.com/$owner/$repository"; + $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'version' => $version + 'referenceType' => $referenceType, + 'referenceValue' => $referenceValue ]); if (!empty($site->getAttribute('providerRepositoryId'))) { @@ -157,9 +172,14 @@ class Create extends Base 'resourceType' => 'sites', 'buildCommands' => \implode(' && ', $commands), 'buildOutput' => $site->getAttribute('outputDirectory', ''), + 'providerRepositoryName' => $repository, + 'providerRepositoryOwner' => $owner, + 'providerRepositoryUrl' => $repositoryUrl, + 'providerBranchUrl' => $branchUrl, + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $referenceValue : '', 'adapter' => $site->getAttribute('adapter', ''), 'fallbackFile' => $site->getAttribute('fallbackFile', ''), - 'type' => 'manual', + 'type' => 'vcs', 'activate' => $activate, ])); From cb03bfe74ccd1ac43737053113b27a82e8bd3cbd Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sun, 14 Sep 2025 15:22:51 +0530 Subject: [PATCH 04/35] specs and sdk --- .../specs/open-api3-latest-console.json | 179 ++++---------- app/config/specs/open-api3-latest-server.json | 154 +++--------- app/config/specs/swagger2-latest-console.json | 195 +++++---------- app/config/specs/swagger2-latest-server.json | 170 ++++--------- .../Sites/Http/Deployments/Direct/Create.php | 231 ------------------ .../Platform/Modules/Sites/Services/Http.php | 2 - 6 files changed, 186 insertions(+), 745 deletions(-) delete mode 100644 src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 0de652e3fb..96cec5c5a6 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -4888,7 +4888,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 497, + "weight": 496, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -12932,6 +12932,16 @@ "description": "Version (tag) for the repo linked to the function template.", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -12941,8 +12951,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -28209,7 +28218,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 503, + "weight": 502, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28283,7 +28292,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 498, + "weight": 497, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28350,7 +28359,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 500, + "weight": 499, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28428,7 +28437,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 501, + "weight": 500, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28541,7 +28550,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 499, + "weight": 498, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28619,7 +28628,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 502, + "weight": 501, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28670,7 +28679,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 504, + "weight": 503, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28730,7 +28739,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 505, + "weight": 504, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -29085,103 +29094,6 @@ } } }, - "\/sites\/direct": { - "post": { - "summary": "Create direct deployment", - "operationId": "sitesCreateDirectDeployment", - "tags": [ - "sites" - ], - "description": "Create a deployment directly from a repository branch.", - "responses": { - "202": { - "description": "Deployment", - "content": { - "application\/json": { - "schema": { - "$ref": "#\/components\/schemas\/deployment" - } - } - } - } - }, - "deprecated": false, - "x-appwrite": { - "method": "createDirectDeployment", - "group": "deployments", - "weight": 483, - "cookies": false, - "type": "", - "demo": "sites\/create-direct-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", - "scope": "sites.write", - "platforms": [ - "server" - ], - "packaging": false, - "auth": { - "Project": [] - } - }, - "security": [ - { - "Project": [], - "Key": [] - } - ], - "requestBody": { - "content": { - "application\/json": { - "schema": { - "type": "object", - "properties": { - "siteId": { - "type": "string", - "description": "Site ID.", - "x-example": "" - }, - "repository": { - "type": "string", - "description": "Repository name of the template.", - "x-example": "" - }, - "owner": { - "type": "string", - "description": "The name of the owner of the template.", - "x-example": "" - }, - "rootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "x-example": "" - }, - "branch": { - "type": "string", - "description": "Branch to create deployment from.", - "x-example": "" - }, - "activate": { - "type": "boolean", - "description": "Automatically activate the deployment when it is finished building.", - "x-example": false - } - }, - "required": [ - "siteId", - "repository", - "owner", - "rootDirectory", - "branch" - ] - } - } - } - } - } - }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -29255,7 +29167,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 496, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29305,7 +29217,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 492, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29405,7 +29317,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 493, + "weight": 492, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29465,7 +29377,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 494, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -30304,9 +30216,19 @@ }, "version": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", + "description": "Version (tag) for the repo linked to the function template.", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -30316,8 +30238,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -30734,7 +30655,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 485, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30805,7 +30726,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 484, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -30867,7 +30788,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 486, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -30938,7 +30859,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 495, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -31020,7 +30941,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 489, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -31079,7 +31000,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 487, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31170,7 +31091,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 488, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31239,7 +31160,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 490, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31330,7 +31251,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 491, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -40081,7 +40002,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 508, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -40161,7 +40082,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 506, + "weight": 505, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40250,7 +40171,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 507, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40310,7 +40231,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 509, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40380,7 +40301,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 510, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index d8d7178eb2..3a1afab16b 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -11714,6 +11714,16 @@ "description": "Version (tag) for the repo linked to the function template.", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -11723,8 +11733,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -20014,104 +20023,6 @@ } } }, - "\/sites\/direct": { - "post": { - "summary": "Create direct deployment", - "operationId": "sitesCreateDirectDeployment", - "tags": [ - "sites" - ], - "description": "Create a deployment directly from a repository branch.", - "responses": { - "202": { - "description": "Deployment", - "content": { - "application\/json": { - "schema": { - "$ref": "#\/components\/schemas\/deployment" - } - } - } - } - }, - "deprecated": false, - "x-appwrite": { - "method": "createDirectDeployment", - "group": "deployments", - "weight": 483, - "cookies": false, - "type": "", - "demo": "sites\/create-direct-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", - "scope": "sites.write", - "platforms": [ - "server" - ], - "packaging": false, - "auth": { - "Project": [], - "Key": [] - } - }, - "security": [ - { - "Project": [], - "Key": [] - } - ], - "requestBody": { - "content": { - "application\/json": { - "schema": { - "type": "object", - "properties": { - "siteId": { - "type": "string", - "description": "Site ID.", - "x-example": "" - }, - "repository": { - "type": "string", - "description": "Repository name of the template.", - "x-example": "" - }, - "owner": { - "type": "string", - "description": "The name of the owner of the template.", - "x-example": "" - }, - "rootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "x-example": "" - }, - "branch": { - "type": "string", - "description": "Branch to create deployment from.", - "x-example": "" - }, - "activate": { - "type": "boolean", - "description": "Automatically activate the deployment when it is finished building.", - "x-example": false - } - }, - "required": [ - "siteId", - "repository", - "owner", - "rootDirectory", - "branch" - ] - } - } - } - } - } - }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -20186,7 +20097,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 496, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -21012,9 +20923,19 @@ }, "version": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", + "description": "Version (tag) for the repo linked to the function template.", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -21024,8 +20945,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -21447,7 +21367,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 485, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21519,7 +21439,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 484, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21582,7 +21502,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 486, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21654,7 +21574,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 489, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21714,7 +21634,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 487, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -21806,7 +21726,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 488, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -21876,7 +21796,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 490, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -21968,7 +21888,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 491, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -30122,7 +30042,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 508, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30203,7 +30123,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 506, + "weight": 505, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30293,7 +30213,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 507, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30354,7 +30274,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 509, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30425,7 +30345,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 510, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 22823cd40e..27604767b9 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -5052,7 +5052,7 @@ "x-appwrite": { "method": "getResource", "group": null, - "weight": 497, + "weight": 496, "cookies": false, "type": "", "demo": "console\/get-resource.md", @@ -12928,9 +12928,21 @@ "version": { "type": "string", "description": "Version (tag) for the repo linked to the function template.", - "default": null, + "default": "", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "default": "", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": "", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -12941,8 +12953,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -28351,7 +28362,7 @@ "x-appwrite": { "method": "listRules", "group": null, - "weight": 503, + "weight": 502, "cookies": false, "type": "", "demo": "proxy\/list-rules.md", @@ -28424,7 +28435,7 @@ "x-appwrite": { "method": "createAPIRule", "group": null, - "weight": 498, + "weight": 497, "cookies": false, "type": "", "demo": "proxy\/create-api-rule.md", @@ -28494,7 +28505,7 @@ "x-appwrite": { "method": "createFunctionRule", "group": null, - "weight": 500, + "weight": 499, "cookies": false, "type": "", "demo": "proxy\/create-function-rule.md", @@ -28577,7 +28588,7 @@ "x-appwrite": { "method": "createRedirectRule", "group": null, - "weight": 501, + "weight": 500, "cookies": false, "type": "", "demo": "proxy\/create-redirect-rule.md", @@ -28697,7 +28708,7 @@ "x-appwrite": { "method": "createSiteRule", "group": null, - "weight": 499, + "weight": 498, "cookies": false, "type": "", "demo": "proxy\/create-site-rule.md", @@ -28778,7 +28789,7 @@ "x-appwrite": { "method": "getRule", "group": null, - "weight": 502, + "weight": 501, "cookies": false, "type": "", "demo": "proxy\/get-rule.md", @@ -28831,7 +28842,7 @@ "x-appwrite": { "method": "deleteRule", "group": null, - "weight": 504, + "weight": 503, "cookies": false, "type": "", "demo": "proxy\/delete-rule.md", @@ -28891,7 +28902,7 @@ "x-appwrite": { "method": "updateRuleVerification", "group": null, - "weight": 505, + "weight": 504, "cookies": false, "type": "", "demo": "proxy\/update-rule-verification.md", @@ -29264,111 +29275,6 @@ ] } }, - "\/sites\/direct": { - "post": { - "summary": "Create direct deployment", - "operationId": "sitesCreateDirectDeployment", - "consumes": [ - "application\/json" - ], - "produces": [ - "application\/json" - ], - "tags": [ - "sites" - ], - "description": "Create a deployment directly from a repository branch.", - "responses": { - "202": { - "description": "Deployment", - "schema": { - "$ref": "#\/definitions\/deployment" - } - } - }, - "deprecated": false, - "x-appwrite": { - "method": "createDirectDeployment", - "group": "deployments", - "weight": 483, - "cookies": false, - "type": "", - "demo": "sites\/create-direct-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", - "scope": "sites.write", - "platforms": [ - "server" - ], - "packaging": false, - "auth": { - "Project": [] - } - }, - "security": [ - { - "Project": [], - "Key": [] - } - ], - "parameters": [ - { - "name": "payload", - "in": "body", - "schema": { - "type": "object", - "properties": { - "siteId": { - "type": "string", - "description": "Site ID.", - "default": null, - "x-example": "" - }, - "repository": { - "type": "string", - "description": "Repository name of the template.", - "default": null, - "x-example": "" - }, - "owner": { - "type": "string", - "description": "The name of the owner of the template.", - "default": null, - "x-example": "" - }, - "rootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "default": null, - "x-example": "" - }, - "branch": { - "type": "string", - "description": "Branch to create deployment from.", - "default": null, - "x-example": "" - }, - "activate": { - "type": "boolean", - "description": "Automatically activate the deployment when it is finished building.", - "default": true, - "x-example": false - } - }, - "required": [ - "siteId", - "repository", - "owner", - "rootDirectory", - "branch" - ] - } - } - ] - } - }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -29442,7 +29348,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 496, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -29492,7 +29398,7 @@ "x-appwrite": { "method": "listTemplates", "group": "templates", - "weight": 492, + "weight": 491, "cookies": false, "type": "", "demo": "sites\/list-templates.md", @@ -29586,7 +29492,7 @@ "x-appwrite": { "method": "getTemplate", "group": "templates", - "weight": 493, + "weight": 492, "cookies": false, "type": "", "demo": "sites\/get-template.md", @@ -29644,7 +29550,7 @@ "x-appwrite": { "method": "listUsage", "group": null, - "weight": 494, + "weight": 493, "cookies": false, "type": "", "demo": "sites\/list-usage.md", @@ -30490,10 +30396,22 @@ }, "version": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "default": null, + "description": "Version (tag) for the repo linked to the function template.", + "default": "", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "default": "", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": "", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -30504,8 +30422,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -30916,7 +30833,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 485, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -30987,7 +30904,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 484, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -31051,7 +30968,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 486, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -31118,7 +31035,7 @@ "x-appwrite": { "method": "getUsage", "group": null, - "weight": 495, + "weight": 494, "cookies": false, "type": "", "demo": "sites\/get-usage.md", @@ -31196,7 +31113,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 489, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -31255,7 +31172,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 487, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -31345,7 +31262,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 488, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -31412,7 +31329,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 490, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -31504,7 +31421,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 491, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -40021,7 +39938,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 508, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -40101,7 +40018,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 506, + "weight": 505, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -40185,7 +40102,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 507, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -40245,7 +40162,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 509, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -40316,7 +40233,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 510, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index c7a5a20c24..a13c8324a4 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -11735,9 +11735,21 @@ "version": { "type": "string", "description": "Version (tag) for the repo linked to the function template.", - "default": null, + "default": "", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "default": "", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": "", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -11748,8 +11760,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -20226,112 +20237,6 @@ ] } }, - "\/sites\/direct": { - "post": { - "summary": "Create direct deployment", - "operationId": "sitesCreateDirectDeployment", - "consumes": [ - "application\/json" - ], - "produces": [ - "application\/json" - ], - "tags": [ - "sites" - ], - "description": "Create a deployment directly from a repository branch.", - "responses": { - "202": { - "description": "Deployment", - "schema": { - "$ref": "#\/definitions\/deployment" - } - } - }, - "deprecated": false, - "x-appwrite": { - "method": "createDirectDeployment", - "group": "deployments", - "weight": 483, - "cookies": false, - "type": "", - "demo": "sites\/create-direct-deployment.md", - "edit": "https:\/\/github.com\/appwrite\/appwrite\/edit\/masterCreate a deployment directly from a repository branch.", - "rate-limit": 0, - "rate-time": 3600, - "rate-key": "url:{url},ip:{ip}", - "scope": "sites.write", - "platforms": [ - "server" - ], - "packaging": false, - "auth": { - "Project": [], - "Key": [] - } - }, - "security": [ - { - "Project": [], - "Key": [] - } - ], - "parameters": [ - { - "name": "payload", - "in": "body", - "schema": { - "type": "object", - "properties": { - "siteId": { - "type": "string", - "description": "Site ID.", - "default": null, - "x-example": "" - }, - "repository": { - "type": "string", - "description": "Repository name of the template.", - "default": null, - "x-example": "" - }, - "owner": { - "type": "string", - "description": "The name of the owner of the template.", - "default": null, - "x-example": "" - }, - "rootDirectory": { - "type": "string", - "description": "Path to site code in the template repo.", - "default": null, - "x-example": "" - }, - "branch": { - "type": "string", - "description": "Branch to create deployment from.", - "default": null, - "x-example": "" - }, - "activate": { - "type": "boolean", - "description": "Automatically activate the deployment when it is finished building.", - "default": true, - "x-example": false - } - }, - "required": [ - "siteId", - "repository", - "owner", - "rootDirectory", - "branch" - ] - } - } - ] - } - }, "\/sites\/frameworks": { "get": { "summary": "List frameworks", @@ -20406,7 +20311,7 @@ "x-appwrite": { "method": "listSpecifications", "group": "frameworks", - "weight": 496, + "weight": 495, "cookies": false, "type": "", "demo": "sites\/list-specifications.md", @@ -21241,10 +21146,22 @@ }, "version": { "type": "string", - "description": "Version (tag) for the repo linked to the site template.", - "default": null, + "description": "Version (tag) for the repo linked to the function template.", + "default": "", "x-example": "" }, + "type": { + "type": "string", + "description": "Type for the reference provided. Can be commit, branch, or version", + "default": "", + "x-example": "" + }, + "reference": { + "type": "string", + "description": "Reference value, can be a commit hash, branch name, or release tag", + "default": "", + "x-example": "" + }, "activate": { "type": "boolean", "description": "Automatically activate the deployment when it is finished building.", @@ -21255,8 +21172,7 @@ "required": [ "repository", "owner", - "rootDirectory", - "version" + "rootDirectory" ] } } @@ -21672,7 +21588,7 @@ "x-appwrite": { "method": "listLogs", "group": "logs", - "weight": 485, + "weight": 484, "cookies": false, "type": "", "demo": "sites\/list-logs.md", @@ -21744,7 +21660,7 @@ "x-appwrite": { "method": "getLog", "group": "logs", - "weight": 484, + "weight": 483, "cookies": false, "type": "", "demo": "sites\/get-log.md", @@ -21809,7 +21725,7 @@ "x-appwrite": { "method": "deleteLog", "group": "logs", - "weight": 486, + "weight": 485, "cookies": false, "type": "", "demo": "sites\/delete-log.md", @@ -21877,7 +21793,7 @@ "x-appwrite": { "method": "listVariables", "group": "variables", - "weight": 489, + "weight": 488, "cookies": false, "type": "", "demo": "sites\/list-variables.md", @@ -21937,7 +21853,7 @@ "x-appwrite": { "method": "createVariable", "group": "variables", - "weight": 487, + "weight": 486, "cookies": false, "type": "", "demo": "sites\/create-variable.md", @@ -22028,7 +21944,7 @@ "x-appwrite": { "method": "getVariable", "group": "variables", - "weight": 488, + "weight": 487, "cookies": false, "type": "", "demo": "sites\/get-variable.md", @@ -22096,7 +22012,7 @@ "x-appwrite": { "method": "updateVariable", "group": "variables", - "weight": 490, + "weight": 489, "cookies": false, "type": "", "demo": "sites\/update-variable.md", @@ -22189,7 +22105,7 @@ "x-appwrite": { "method": "deleteVariable", "group": "variables", - "weight": 491, + "weight": 490, "cookies": false, "type": "", "demo": "sites\/delete-variable.md", @@ -30142,7 +30058,7 @@ "x-appwrite": { "method": "list", "group": "files", - "weight": 508, + "weight": 507, "cookies": false, "type": "", "demo": "tokens\/list.md", @@ -30223,7 +30139,7 @@ "x-appwrite": { "method": "createFileToken", "group": "files", - "weight": 506, + "weight": 505, "cookies": false, "type": "", "demo": "tokens\/create-file-token.md", @@ -30308,7 +30224,7 @@ "x-appwrite": { "method": "get", "group": "tokens", - "weight": 507, + "weight": 506, "cookies": false, "type": "", "demo": "tokens\/get.md", @@ -30369,7 +30285,7 @@ "x-appwrite": { "method": "update", "group": "tokens", - "weight": 509, + "weight": 508, "cookies": false, "type": "", "demo": "tokens\/update.md", @@ -30441,7 +30357,7 @@ "x-appwrite": { "method": "delete", "group": "tokens", - "weight": 510, + "weight": 509, "cookies": false, "type": "", "demo": "tokens\/delete.md", diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php deleted file mode 100644 index d1df994149..0000000000 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Direct/Create.php +++ /dev/null @@ -1,231 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/sites/direct') - ->desc('Create direct deployment') - ->groups(['api', 'sites']) - ->label('scope', 'sites.write') - ->label('resourceType', RESOURCE_TYPE_SITES) - ->label('event', 'sites.[siteId].deployments.[deploymentId].create') - ->label('audits.event', 'deployment.create') - ->label('audits.resource', 'site/{request.siteId}') - ->label('sdk', new Method( - namespace: 'sites', - group: 'deployments', - name: 'createDirectDeployment', - description: <<param('siteId', '', new UID(), 'Site ID.') - ->param('repository', '', new Text(128, 0), 'Repository name of the template.') - ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') - ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') - ->param('branch', '', new Text(128, 0), 'Branch to create deployment from.') - ->param('activate', true, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) - ->inject('request') - ->inject('response') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('project') - ->inject('queueForEvents') - ->inject('queueForBuilds') - ->inject('gitHub') - ->callback($this->action(...)); - } - - public function action( - string $siteId, - string $repository, - string $owner, - string $rootDirectory, - string $branch, - bool $activate, - Request $request, - Response $response, - Database $dbForProject, - Database $dbForPlatform, - Document $project, - Event $queueForEvents, - Build $queueForBuilds, - GitHub $github - ) { - $site = $dbForProject->getDocument('sites', $siteId); - - if ($site->isEmpty()) { - throw new Exception(Exception::SITE_NOT_FOUND); - } - - $template = new Document([ - 'repositoryName' => $repository, - 'ownerName' => $owner, - 'rootDirectory' => $rootDirectory, - 'branch' => $branch - ]); - - - if (!empty($site->getAttribute('providerRepositoryId'))) { - $installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId')); - - $deployment = $this->redeployVcsSite( - request: $request, - site: $site, - project: $project, - installation: $installation, - dbForProject: $dbForProject, - dbForPlatform: $dbForPlatform, - queueForBuilds: $queueForBuilds, - template: $template, - github: $github, - activate: $activate, - ); - - $queueForEvents - ->setParam('siteId', $site->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($deployment, Response::MODEL_DEPLOYMENT); - - return; - } - - $branchUrl = "https://github.com/$owner/$repository/tree/$branch"; - $repositoryUrl = "https://github.com/$owner/$repository"; - - try { - $commitDetails = $github->getLatestCommit($owner, $repository, $branch); - } catch (\Throwable $error) { - // Ignore; deployment can continue - } - - $commands = []; - if (!empty($site->getAttribute('installCommand', ''))) { - $commands[] = $site->getAttribute('installCommand', ''); - } - if (!empty($site->getAttribute('buildCommand', ''))) { - $commands[] = $site->getAttribute('buildCommand', ''); - } - - $deploymentId = ID::unique(); - $deployment = $dbForProject->createDocument('deployments', new Document([ - '$id' => $deploymentId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - 'resourceId' => $site->getId(), - 'resourceInternalId' => $site->getSequence(), - 'resourceType' => 'sites', - 'buildCommands' => \implode(' && ', $commands), - 'buildOutput' => $site->getAttribute('outputDirectory', ''), - 'adapter' => $site->getAttribute('adapter', ''), - 'fallbackFile' => $site->getAttribute('fallbackFile', ''), - 'providerRepositoryName' => $repository, - 'providerRepositoryOwner' => $owner, - 'providerRepositoryUrl' => $repositoryUrl, - 'providerBranchUrl' => $branchUrl, - 'providerBranch' => $branch, - 'providerCommitHash' => $commitDetails['commitHash'] ?? '', - 'providerCommitAuthorUrl' => $commitDetails['commitAuthorUrl'] ?? '', - 'providerCommitAuthor' => $commitDetails['commitAuthor'] ?? '', - 'providerCommitMessage' => mb_strimwidth($commitDetails['commitMessage'] ?? '', 0, 255, '...'), - 'providerCommitUrl' => $commitDetails['commitUrl'] ?? '', - 'type' => 'vcs', - 'activate' => $activate, - ])); - - $site = $site - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); - - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = ID::unique() . "." . $sitesDomain; - - // TODO: @christyjacob remove once we migrate the rules in 1.7.x - $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); - - Authorization::skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getSequence(), - 'domain' => $domain, - 'type' => 'deployment', - 'trigger' => 'deployment', - 'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(), - 'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(), - 'deploymentResourceType' => 'site', - 'deploymentResourceId' => $site->getId(), - 'deploymentResourceInternalId' => $site->getSequence(), - 'status' => 'verified', - 'certificateId' => '', - 'owner' => 'Appwrite', - 'region' => $project->getAttribute('region') - ])) - ); - - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($site) - ->setDeployment($deployment) - ->setTemplate($template); - - $queueForEvents - ->setParam('siteId', $site->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response - ->setStatusCode(Response::STATUS_CODE_ACCEPTED) - ->dynamic($deployment, Response::MODEL_DEPLOYMENT); - } -} diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index 437356d4e2..6bd151f97e 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Sites\Services; use Appwrite\Platform\Modules\Sites\Http\Deployments\Create as CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Delete as DeleteDeployment; -use Appwrite\Platform\Modules\Sites\Http\Deployments\Direct\Create as CreateDirectDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Download\Get as DownloadDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Duplicate\Create as CreateDuplicateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Get as GetDeployment; @@ -61,7 +60,6 @@ class Http extends Service $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); $this->addAction(CreateDuplicateDeployment::getName(), new CreateDuplicateDeployment()); $this->addAction(UpdateDeploymentStatus::getName(), new UpdateDeploymentStatus()); - $this->addAction(CreateDirectDeployment::getName(), new CreateDirectDeployment()); // Logs $this->addAction(GetLog::getName(), new GetLog()); From fca27279660b7f13ca652a0f7ae9a417fe087cf6 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Sun, 14 Sep 2025 15:28:25 +0530 Subject: [PATCH 05/35] add validation for type --- .../Modules/Functions/Http/Deployments/Template/Create.php | 3 ++- .../Modules/Sites/Http/Deployments/Template/Create.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 885e23bfa1..6312c24d86 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -21,6 +21,7 @@ use Utopia\Platform\Scope\HTTP; use Utopia\Swoole\Request; use Utopia\Validator\Boolean; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; class Create extends Base @@ -66,7 +67,7 @@ class Create extends Base ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.') ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) - ->param('type', '', new Text(128, 0), 'Type for the reference provided. Can be commit, branch, or version', true) + ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or version', true) ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 0799e0b51f..3721740e7f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -23,6 +23,7 @@ use Utopia\Swoole\Request; use Utopia\System\System; use Utopia\Validator\Boolean; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; class Create extends Base @@ -68,7 +69,7 @@ class Create extends Base ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) - ->param('type', '', new Text(128, 0), 'Type for the reference provided. Can be commit, branch, or version', true) + ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or version', true) ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') From de8538b6222ec78e87dc9eaa81aa5d40b0b7e4cc Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Thu, 18 Sep 2025 12:27:24 +0530 Subject: [PATCH 06/35] use request filters for 1.8.0 to convert into and --- app/controllers/general.php | 4 +++ .../Http/Deployments/Template/Create.php | 21 ++++-------- .../Http/Deployments/Template/Create.php | 21 ++++-------- src/Appwrite/Utopia/Request/Filters/V21.php | 34 +++++++++++++++++++ 4 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 src/Appwrite/Utopia/Request/Filters/V21.php diff --git a/app/controllers/general.php b/app/controllers/general.php index 8abca96742..c663091677 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -23,6 +23,7 @@ use Appwrite\Utopia\Request\Filters\V17 as RequestV17; use Appwrite\Utopia\Request\Filters\V18 as RequestV18; use Appwrite\Utopia\Request\Filters\V19 as RequestV19; use Appwrite\Utopia\Request\Filters\V20 as RequestV20; +use Appwrite\Utopia\Request\Filters\V21 as RequestV21; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Filters\V16 as ResponseV16; use Appwrite\Utopia\Response\Filters\V17 as ResponseV17; @@ -906,6 +907,9 @@ App::init() $dbForProject = $getProjectDB($project); $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); } + if (version_compare($requestFormat, '1.8.1', '<')) { + $request->addFilter(new RequestV21()); + } } $domain = $request->getHostname(); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 6312c24d86..5e0266a09e 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -66,9 +66,8 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to function code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) - ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or version', true) - ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) + ->param('type', '', new WhiteList(['commit', 'branch', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag') + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -86,7 +85,6 @@ class Create extends Base string $repository, string $owner, string $rootDirectory, - string $version, string $type, string $reference, bool $activate, @@ -105,22 +103,15 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } - if (empty($version) && empty($type) && empty($reference)) { - throw new Exception("Either version or type & reference must be provided"); - } - - $referenceType = !empty($version) ? GitHub::CLONE_TYPE_TAG : $type; - $referenceValue = !empty($version) ? $version : $reference; - - $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$referenceValue" : ""; + $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$reference" : ""; $repositoryUrl = "https://github.com/$owner/$repository"; $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'referenceType' => $referenceType, - 'referenceValue' => $referenceValue, + 'referenceType' => $type, + 'referenceValue' => $reference, ]); if (!empty($function->getAttribute('providerRepositoryId'))) { @@ -166,7 +157,7 @@ class Create extends Base 'providerRepositoryOwner' => $owner, 'providerRepositoryUrl' => $repositoryUrl, 'providerBranchUrl' => $branchUrl, - 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $referenceValue : '', + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $reference : '', 'type' => 'vcs', 'activate' => $activate, ])); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 3721740e7f..d7196eb3d5 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -68,9 +68,8 @@ class Create extends Base ->param('repository', '', new Text(128, 0), 'Repository name of the template.') ->param('owner', '', new Text(128, 0), 'The name of the owner of the template.') ->param('rootDirectory', '', new Text(128, 0), 'Path to site code in the template repo.') - ->param('version', '', new Text(128, 0), 'Version (tag) for the repo linked to the function template.', true) - ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or version', true) - ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag', true) + ->param('type', '', new WhiteList(['branch', 'commit', 'tag']), 'Type for the reference provided. Can be commit, branch, or tag') + ->param('reference', '', new Text(128, 0), 'Reference value, can be a commit hash, branch name, or release tag') ->param('activate', false, new Boolean(), 'Automatically activate the deployment when it is finished building.', true) ->inject('request') ->inject('response') @@ -88,7 +87,6 @@ class Create extends Base string $repository, string $owner, string $rootDirectory, - string $version, string $type, string $reference, bool $activate, @@ -107,22 +105,15 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } - if (empty($version) && empty($type) && empty($reference)) { - throw new Exception("Either version or type & reference must be provided"); - } - - $referenceType = !empty($version) ? GitHub::CLONE_TYPE_TAG : $type; - $referenceValue = !empty($version) ? $version : $reference; - - $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$referenceValue" : ""; + $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$reference" : ""; $repositoryUrl = "https://github.com/$owner/$repository"; $template = new Document([ 'repositoryName' => $repository, 'ownerName' => $owner, 'rootDirectory' => $rootDirectory, - 'referenceType' => $referenceType, - 'referenceValue' => $referenceValue + 'referenceType' => $type, + 'referenceValue' => $reference ]); if (!empty($site->getAttribute('providerRepositoryId'))) { @@ -177,7 +168,7 @@ class Create extends Base 'providerRepositoryOwner' => $owner, 'providerRepositoryUrl' => $repositoryUrl, 'providerBranchUrl' => $branchUrl, - 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $referenceValue : '', + 'providerBranch' => $type == GitHub::CLONE_TYPE_BRANCH ? $reference : '', 'adapter' => $site->getAttribute('adapter', ''), 'fallbackFile' => $site->getAttribute('fallbackFile', ''), 'type' => 'vcs', diff --git a/src/Appwrite/Utopia/Request/Filters/V21.php b/src/Appwrite/Utopia/Request/Filters/V21.php new file mode 100644 index 0000000000..3ef0becf1d --- /dev/null +++ b/src/Appwrite/Utopia/Request/Filters/V21.php @@ -0,0 +1,34 @@ +convertVersionToTypeAndReference($content); + break; + } + return $content; + } + + /** + * Convert version parameter to type and reference for backwards compatibility + * with 1.8.0 template deployment endpoints + */ + protected function convertVersionToTypeAndReference(array $content): array + { + if (!empty($content['version'])) { + $content['type'] = 'tag'; + $content['reference'] = $content['version']; + unset($content['version']); + } + return $content; + } +} From 8e32fb05d1361778e64b583f468e2cd6d7de77f1 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Thu, 18 Sep 2025 12:28:08 +0530 Subject: [PATCH 07/35] specs --- .../specs/open-api3-latest-console.json | 40 +++++++++------ app/config/specs/open-api3-latest-server.json | 40 +++++++++------ app/config/specs/swagger2-latest-console.json | 50 +++++++++++-------- app/config/specs/swagger2-latest-server.json | 50 +++++++++++-------- 4 files changed, 104 insertions(+), 76 deletions(-) diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 96cec5c5a6..e052f542c9 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -12927,15 +12927,17 @@ "description": "Path to function code in the template repo.", "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", @@ -12951,7 +12953,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } @@ -30214,15 +30218,17 @@ "description": "Path to site code in the template repo.", "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", @@ -30238,7 +30244,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 3a1afab16b..b111f8e3e8 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -11709,15 +11709,17 @@ "description": "Path to function code in the template repo.", "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", @@ -11733,7 +11735,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } @@ -20921,15 +20925,17 @@ "description": "Path to site code in the template repo.", "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", @@ -20945,7 +20951,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 27604767b9..b359eaf310 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -12925,22 +12925,23 @@ "default": null, "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "default": "", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "default": null, + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", "description": "Reference value, can be a commit hash, branch name, or release tag", - "default": "", + "default": null, "x-example": "" }, "activate": { @@ -12953,7 +12954,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } @@ -30394,22 +30397,23 @@ "default": null, "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "default": "", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "default": null, + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", "description": "Reference value, can be a commit hash, branch name, or release tag", - "default": "", + "default": null, "x-example": "" }, "activate": { @@ -30422,7 +30426,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index a13c8324a4..b806a9e6c7 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -11732,22 +11732,23 @@ "default": null, "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "default": "", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "default": null, + "x-example": "commit", + "enum": [ + "commit", + "branch", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", "description": "Reference value, can be a commit hash, branch name, or release tag", - "default": "", + "default": null, "x-example": "" }, "activate": { @@ -11760,7 +11761,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } @@ -21144,22 +21147,23 @@ "default": null, "x-example": "" }, - "version": { - "type": "string", - "description": "Version (tag) for the repo linked to the function template.", - "default": "", - "x-example": "" - }, "type": { "type": "string", - "description": "Type for the reference provided. Can be commit, branch, or version", - "default": "", - "x-example": "" + "description": "Type for the reference provided. Can be commit, branch, or tag", + "default": null, + "x-example": "branch", + "enum": [ + "branch", + "commit", + "tag" + ], + "x-enum-name": null, + "x-enum-keys": [] }, "reference": { "type": "string", "description": "Reference value, can be a commit hash, branch name, or release tag", - "default": "", + "default": null, "x-example": "" }, "activate": { @@ -21172,7 +21176,9 @@ "required": [ "repository", "owner", - "rootDirectory" + "rootDirectory", + "type", + "reference" ] } } From 9fe3e94a669240e3a8669d9da28cdea51561358a Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Thu, 18 Sep 2025 13:12:46 +0530 Subject: [PATCH 08/35] e2e tests --- .../e2e/Services/Functions/FunctionsBase.php | 28 ++++ .../Functions/FunctionsCustomServerTest.php | 117 +++++++++++++- tests/e2e/Services/Sites/SitesBase.php | 29 ++++ .../Services/Sites/SitesCustomServerTest.php | 149 +++++++++++++++++- 4 files changed, 321 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 27b67d851d..4eb31c08ac 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -268,6 +268,34 @@ trait FunctionsBase 'x-appwrite-project' => $this->getProject()['$id'], ])); + // Fetch latest commit from GitHub API if template has provider info + if ( + isset($template['body']['providerOwner']) && + isset($template['body']['providerRepositoryId']) + ) { + $owner = $template['body']['providerOwner']; + $repo = $template['body']['providerRepositoryId']; + + // GitHub API to get latest commit from main branch + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repo}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + $template['body']['latestCommit'] = $commitData['sha']; + } + } + } + return $template; } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 0d63791151..8a774ed8bb 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -400,7 +400,8 @@ class FunctionsCustomServerTest extends Scope 'repository' => $starterTemplate['body']['providerRepositoryId'], 'owner' => $starterTemplate['body']['providerOwner'], 'rootDirectory' => $phpRuntime['providerRootDirectory'], - 'version' => $starterTemplate['body']['providerVersion'], + 'type' => 'tag', + 'reference' => $starterTemplate['body']['providerVersion'], ] ); @@ -502,6 +503,120 @@ class FunctionsCustomServerTest extends Scope $function = $this->cleanupFunction($functionId); } + public function testCreateFunctionAndDeploymentFromTemplateBranch() + { + $starterTemplate = $this->getTemplate('starter'); + $this->assertEquals(200, $starterTemplate['headers']['status-code']); + + $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + return $runtime['name'] === 'node-22'; + }))[0]; + + // If this fails, the template has variables, and this test needs to be updated + $this->assertEmpty($starterTemplate['body']['variables']); + + $function = $this->createFunction( + [ + 'functionId' => ID::unique(), + 'name' => $starterTemplate['body']['name'] . ' - Branch Test', + 'runtime' => 'node-22', + 'execute' => $starterTemplate['body']['permissions'], + 'entrypoint' => $phpRuntime['entrypoint'], + 'events' => $starterTemplate['body']['events'], + 'schedule' => $starterTemplate['body']['cron'], + 'timeout' => $starterTemplate['body']['timeout'], + 'commands' => $phpRuntime['commands'], + 'scopes' => $starterTemplate['body']['scopes'], + ] + ); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + + $functionId = $function['body']['$id'] ?? ''; + + // Deploy using branch + $deployment = $this->createTemplateDeployment( + $functionId, + [ + 'resourceId' => ID::unique(), + 'activate' => true, + 'repository' => $starterTemplate['body']['providerRepositoryId'], + 'owner' => $starterTemplate['body']['providerOwner'], + 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'type' => 'branch', + 'reference' => 'main', + ] + ); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + + $function = $this->cleanupFunction($functionId); + } + + public function testCreateFunctionAndDeploymentFromTemplateCommit() + { + $starterTemplate = $this->getTemplate('starter'); + $this->assertEquals(200, $starterTemplate['headers']['status-code']); + + // Ensure we have the latest commit + $this->assertArrayHasKey('latestCommit', $starterTemplate['body']); + $latestCommit = $starterTemplate['body']['latestCommit']; + + $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + return $runtime['name'] === 'node-22'; + }))[0]; + + // If this fails, the template has variables, and this test needs to be updated + $this->assertEmpty($starterTemplate['body']['variables']); + + $function = $this->createFunction( + [ + 'functionId' => ID::unique(), + 'name' => $starterTemplate['body']['name'] . ' - Commit Test', + 'runtime' => 'node-22', + 'execute' => $starterTemplate['body']['permissions'], + 'entrypoint' => $phpRuntime['entrypoint'], + 'events' => $starterTemplate['body']['events'], + 'schedule' => $starterTemplate['body']['cron'], + 'timeout' => $starterTemplate['body']['timeout'], + 'commands' => $phpRuntime['commands'], + 'scopes' => $starterTemplate['body']['scopes'], + ] + ); + + $this->assertEquals(201, $function['headers']['status-code']); + $this->assertNotEmpty($function['body']['$id']); + + $functionId = $function['body']['$id'] ?? ''; + + // Deploy using commit + $deployment = $this->createTemplateDeployment( + $functionId, + [ + 'resourceId' => ID::unique(), + 'activate' => true, + 'repository' => $starterTemplate['body']['providerRepositoryId'], + 'owner' => $starterTemplate['body']['providerOwner'], + 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'type' => 'commit', + 'reference' => $latestCommit, + ] + ); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + + $function = $this->cleanupFunction($functionId); + } + /** * @depends testUpdateFunction */ diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 93c55b82b7..004032452b 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -329,6 +329,35 @@ trait SitesBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ]); + + // Fetch latest commit from GitHub API if template has provider info + if ( + isset($template['body']['providerOwner']) && + isset($template['body']['providerRepositoryId']) + ) { + $owner = $template['body']['providerOwner']; + $repo = $template['body']['providerRepositoryId']; + + // GitHub API to get latest commit from main branch + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repo}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + $template['body']['latestCommit'] = $commitData['sha']; + } + } + } + return $template; } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index c8301b9428..524e8a09f3 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1563,7 +1563,154 @@ class SitesCustomServerTest extends Scope 'repository' => $template['providerRepositoryId'], 'owner' => $template['providerOwner'], 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], - 'version' => $template['providerVersion'], + 'type' => 'tag', + 'reference' => $template['providerVersion'], + 'activate' => true + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals(0, $deployment['body']['sourceSize']); + $this->assertEquals(0, $deployment['body']['buildSize']); + $this->assertEquals(0, $deployment['body']['totalSize']); + + $this->assertEventually(function () use ($siteId) { + $site = $this->getSite($siteId); + $this->assertNotEmpty($site['body']['deploymentId']); + }, 50000, 500); + + $domain = $this->setupSiteDomain($siteId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("Hello, Astronaut!", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/about'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("About Me", $response['body']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); + + $this->cleanupSite($siteId); + } + + public function testCreateSiteFromTemplateBranch() + { + $template = $this->getTemplate('playground-for-astro'); + $this->assertEquals(200, $template['headers']['status-code']); + + $template = $template['body']; + + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Astro Blog - Branch Test', + 'framework' => $template['frameworks'][0]['key'], + 'adapter' => $template['frameworks'][0]['adapter'], + 'buildRuntime' => $template['frameworks'][0]['buildRuntime'], + 'outputDirectory' => $template['frameworks'][0]['outputDirectory'], + 'buildCommand' => $template['frameworks'][0]['buildCommand'], + 'installCommand' => $template['frameworks'][0]['installCommand'], + 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], + ]); + + $this->assertNotEmpty($siteId); + + // Deploy using branch + $deployment = $this->createTemplateDeployment($siteId, [ + 'repository' => $template['providerRepositoryId'], + 'owner' => $template['providerOwner'], + 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], + 'type' => 'branch', + 'reference' => 'main', + 'activate' => true + ]); + + $this->assertEquals(202, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['$id']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertEquals(0, $deployment['body']['sourceSize']); + $this->assertEquals(0, $deployment['body']['buildSize']); + $this->assertEquals(0, $deployment['body']['totalSize']); + + $this->assertEventually(function () use ($siteId) { + $site = $this->getSite($siteId); + $this->assertNotEmpty($site['body']['deploymentId']); + }, 50000, 500); + + $domain = $this->setupSiteDomain($siteId); + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("Hello, Astronaut!", $response['body']); + + $response = $proxyClient->call(Client::METHOD_GET, '/about'); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringContainsString("Astro Blog", $response['body']); + $this->assertStringContainsString("About Me", $response['body']); + + $deployment = $this->getDeployment($siteId, $deployment['body']['$id']); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); + + $this->cleanupSite($siteId); + } + + public function testCreateSiteFromTemplateCommit() + { + $template = $this->getTemplate('playground-for-astro'); + $this->assertEquals(200, $template['headers']['status-code']); + + // Ensure we have the latest commit + $this->assertArrayHasKey('latestCommit', $template['body']); + $latestCommit = $template['body']['latestCommit']; + + $template = $template['body']; + + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Astro Blog - Commit Test', + 'framework' => $template['frameworks'][0]['key'], + 'adapter' => $template['frameworks'][0]['adapter'], + 'buildRuntime' => $template['frameworks'][0]['buildRuntime'], + 'outputDirectory' => $template['frameworks'][0]['outputDirectory'], + 'buildCommand' => $template['frameworks'][0]['buildCommand'], + 'installCommand' => $template['frameworks'][0]['installCommand'], + 'fallbackFile' => $template['frameworks'][0]['fallbackFile'], + ]); + + $this->assertNotEmpty($siteId); + + // Deploy using commit + $deployment = $this->createTemplateDeployment($siteId, [ + 'repository' => $template['providerRepositoryId'], + 'owner' => $template['providerOwner'], + 'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'], + 'type' => 'commit', + 'reference' => $latestCommit, 'activate' => true ]); From 2a145bc284d1edff27014187b316493af85448b6 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Fri, 19 Sep 2025 18:18:44 +0530 Subject: [PATCH 09/35] address reviews regarding tests --- .../e2e/Services/Functions/FunctionsBase.php | 41 ++++----- .../Functions/FunctionsCustomServerTest.php | 87 ++++++++++++------- tests/e2e/Services/Sites/SitesBase.php | 41 ++++----- .../Services/Sites/SitesCustomServerTest.php | 9 +- 4 files changed, 97 insertions(+), 81 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index 4eb31c08ac..7403b23a73 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -268,35 +268,30 @@ trait FunctionsBase 'x-appwrite-project' => $this->getProject()['$id'], ])); - // Fetch latest commit from GitHub API if template has provider info - if ( - isset($template['body']['providerOwner']) && - isset($template['body']['providerRepositoryId']) - ) { - $owner = $template['body']['providerOwner']; - $repo = $template['body']['providerRepositoryId']; + return $template; + } - // GitHub API to get latest commit from main branch - $ch = curl_init("https://api.github.com/repos/{$owner}/{$repo}/commits/main"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'User-Agent: Appwrite', - 'Accept: application/vnd.github.v3+json' - ]); + protected function helperGetLatestCommit(string $owner, string $repository): ?string + { + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); - if ($httpCode === 200) { - $commitData = json_decode($response, true); - if (isset($commitData['sha'])) { - $template['body']['latestCommit'] = $commitData['sha']; - } + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + return $commitData['sha']; } } - return $template; + return null; } protected function createExecution(string $functionId, mixed $params = []): mixed diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 8a774ed8bb..5672bd9817 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -361,7 +361,7 @@ class FunctionsCustomServerTest extends Scope $starterTemplate = $this->getTemplate('starter'); $this->assertEquals(200, $starterTemplate['headers']['status-code']); - $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { return $runtime['name'] === 'node-22'; }))[0]; @@ -374,15 +374,15 @@ class FunctionsCustomServerTest extends Scope 'name' => $starterTemplate['body']['name'], 'runtime' => 'node-22', 'execute' => $starterTemplate['body']['permissions'], - 'entrypoint' => $phpRuntime['entrypoint'], + 'entrypoint' => $runtime['entrypoint'], 'events' => $starterTemplate['body']['events'], 'schedule' => $starterTemplate['body']['cron'], 'timeout' => $starterTemplate['body']['timeout'], - 'commands' => $phpRuntime['commands'], + 'commands' => $runtime['commands'], 'scopes' => $starterTemplate['body']['scopes'], 'templateRepository' => $starterTemplate['body']['providerRepositoryId'], 'templateOwner' => $starterTemplate['body']['providerOwner'], - 'templateRootDirectory' => $phpRuntime['providerRootDirectory'], + 'templateRootDirectory' => $runtime['providerRootDirectory'], 'templateVersion' => $starterTemplate['body']['providerVersion'], ] ); @@ -399,7 +399,7 @@ class FunctionsCustomServerTest extends Scope 'activate' => true, 'repository' => $starterTemplate['body']['providerRepositoryId'], 'owner' => $starterTemplate['body']['providerOwner'], - 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'rootDirectory' => $runtime['providerRootDirectory'], 'type' => 'tag', 'reference' => $starterTemplate['body']['providerVersion'], ] @@ -408,11 +408,20 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + // Wait for deployment to be ready + $deploymentId = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); + + // Verify deployment sizes + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals(0, $deployment['body']['sourceSize']); - $this->assertEquals(0, $deployment['body']['buildSize']); - $this->assertEquals(0, $deployment['body']['totalSize']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); $deployments = $this->listDeployments($functionId); @@ -422,16 +431,7 @@ class FunctionsCustomServerTest extends Scope $lastDeployment = $deployments['body']['deployments'][0]; $this->assertNotEmpty($lastDeployment['$id']); - $this->assertEquals(0, $lastDeployment['sourceSize']); - - $deploymentId = $lastDeployment['$id']; - - $this->assertEventually(function () use ($functionId, $deploymentId) { - $deployment = $this->getDeployment($functionId, $deploymentId); - - $this->assertEquals(200, $deployment['headers']['status-code']); - $this->assertEquals('ready', $deployment['body']['status']); - }, 50000, 1000); + $this->assertGreaterThan(0, $lastDeployment['sourceSize']); $function = $this->getFunction($functionId); @@ -508,7 +508,7 @@ class FunctionsCustomServerTest extends Scope $starterTemplate = $this->getTemplate('starter'); $this->assertEquals(200, $starterTemplate['headers']['status-code']); - $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { return $runtime['name'] === 'node-22'; }))[0]; @@ -521,11 +521,11 @@ class FunctionsCustomServerTest extends Scope 'name' => $starterTemplate['body']['name'] . ' - Branch Test', 'runtime' => 'node-22', 'execute' => $starterTemplate['body']['permissions'], - 'entrypoint' => $phpRuntime['entrypoint'], + 'entrypoint' => $runtime['entrypoint'], 'events' => $starterTemplate['body']['events'], 'schedule' => $starterTemplate['body']['cron'], 'timeout' => $starterTemplate['body']['timeout'], - 'commands' => $phpRuntime['commands'], + 'commands' => $runtime['commands'], 'scopes' => $starterTemplate['body']['scopes'], ] ); @@ -543,7 +543,7 @@ class FunctionsCustomServerTest extends Scope 'activate' => true, 'repository' => $starterTemplate['body']['providerRepositoryId'], 'owner' => $starterTemplate['body']['providerOwner'], - 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'rootDirectory' => $runtime['providerRootDirectory'], 'type' => 'branch', 'reference' => 'main', ] @@ -552,8 +552,18 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + $deploymentId = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); + + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); $function = $this->cleanupFunction($functionId); } @@ -563,11 +573,14 @@ class FunctionsCustomServerTest extends Scope $starterTemplate = $this->getTemplate('starter'); $this->assertEquals(200, $starterTemplate['headers']['status-code']); - // Ensure we have the latest commit - $this->assertArrayHasKey('latestCommit', $starterTemplate['body']); - $latestCommit = $starterTemplate['body']['latestCommit']; + // Get latest commit using helper function + $latestCommit = $this->helperGetLatestCommit( + $starterTemplate['body']['providerOwner'], + $starterTemplate['body']['providerRepositoryId'] + ); + $this->assertNotNull($latestCommit); - $phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { + $runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) { return $runtime['name'] === 'node-22'; }))[0]; @@ -580,11 +593,11 @@ class FunctionsCustomServerTest extends Scope 'name' => $starterTemplate['body']['name'] . ' - Commit Test', 'runtime' => 'node-22', 'execute' => $starterTemplate['body']['permissions'], - 'entrypoint' => $phpRuntime['entrypoint'], + 'entrypoint' => $runtime['entrypoint'], 'events' => $starterTemplate['body']['events'], 'schedule' => $starterTemplate['body']['cron'], 'timeout' => $starterTemplate['body']['timeout'], - 'commands' => $phpRuntime['commands'], + 'commands' => $runtime['commands'], 'scopes' => $starterTemplate['body']['scopes'], ] ); @@ -602,7 +615,7 @@ class FunctionsCustomServerTest extends Scope 'activate' => true, 'repository' => $starterTemplate['body']['providerRepositoryId'], 'owner' => $starterTemplate['body']['providerOwner'], - 'rootDirectory' => $phpRuntime['providerRootDirectory'], + 'rootDirectory' => $runtime['providerRootDirectory'], 'type' => 'commit', 'reference' => $latestCommit, ] @@ -611,8 +624,18 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(202, $deployment['headers']['status-code']); $this->assertNotEmpty($deployment['body']['$id']); - $deployment = $this->getDeployment($functionId, $deployment['body']['$id']); + $deploymentId = $deployment['body']['$id']; + $this->assertEventually(function () use ($functionId, $deploymentId) { + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals('ready', $deployment['body']['status']); + }, 50000, 500); + + $deployment = $this->getDeployment($functionId, $deploymentId); $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertGreaterThan(0, $deployment['body']['sourceSize']); + $this->assertGreaterThan(0, $deployment['body']['buildSize']); + $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; + $this->assertEquals($totalSize, $deployment['body']['totalSize']); $function = $this->cleanupFunction($functionId); } diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index 004032452b..7eb5d9699c 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -330,35 +330,30 @@ trait SitesBase 'x-appwrite-project' => $this->getProject()['$id'], ]); - // Fetch latest commit from GitHub API if template has provider info - if ( - isset($template['body']['providerOwner']) && - isset($template['body']['providerRepositoryId']) - ) { - $owner = $template['body']['providerOwner']; - $repo = $template['body']['providerRepositoryId']; + return $template; + } - // GitHub API to get latest commit from main branch - $ch = curl_init("https://api.github.com/repos/{$owner}/{$repo}/commits/main"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'User-Agent: Appwrite', - 'Accept: application/vnd.github.v3+json' - ]); + protected function helperGetLatestCommit(string $owner, string $repository): ?string + { + $ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'User-Agent: Appwrite', + 'Accept: application/vnd.github.v3+json' + ]); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); - if ($httpCode === 200) { - $commitData = json_decode($response, true); - if (isset($commitData['sha'])) { - $template['body']['latestCommit'] = $commitData['sha']; - } + if ($httpCode === 200) { + $commitData = json_decode($response, true); + if (isset($commitData['sha'])) { + return $commitData['sha']; } } - return $template; + return null; } protected function deleteSite(string $siteId): mixed diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 524e8a09f3..f7015ccb48 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1684,9 +1684,12 @@ class SitesCustomServerTest extends Scope $template = $this->getTemplate('playground-for-astro'); $this->assertEquals(200, $template['headers']['status-code']); - // Ensure we have the latest commit - $this->assertArrayHasKey('latestCommit', $template['body']); - $latestCommit = $template['body']['latestCommit']; + // Get latest commit using helper function + $latestCommit = $this->helperGetLatestCommit( + $template['body']['providerOwner'], + $template['body']['providerRepositoryId'] + ); + $this->assertNotNull($latestCommit); $template = $template['body']; From 1a19e01d69d563aff5942ea836cfdb8b484875ee Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Fri, 19 Sep 2025 18:30:42 +0530 Subject: [PATCH 10/35] attempt to fix tests --- tests/e2e/General/UsageTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 8f5477331a..dc49d27aea 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -28,6 +28,7 @@ class UsageTest extends Scope FunctionsBase::createVariable insteadof SitesBase; FunctionsBase::getVariable insteadof SitesBase; FunctionsBase::listVariables insteadof SitesBase; + FunctionsBase::helperGetLatestCommit insteadof SitesBase; FunctionsBase::updateVariable insteadof SitesBase; FunctionsBase::deleteVariable insteadof SitesBase; FunctionsBase::getDeployment insteadof SitesBase; From 7d4486b9e691789bed9a68e62df2f455e18d57ce Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Fri, 19 Sep 2025 18:42:25 +0530 Subject: [PATCH 11/35] more fixes to tests --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 5672bd9817..a3f8768676 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -500,7 +500,7 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals($deployment['body']['$id'], $function['body']['deploymentId']); $this->assertEquals($deployment['body']['$createdAt'], $function['body']['deploymentCreatedAt']); - $function = $this->cleanupFunction($functionId); + $this->cleanupFunction($functionId); } public function testCreateFunctionAndDeploymentFromTemplateBranch() @@ -565,7 +565,7 @@ class FunctionsCustomServerTest extends Scope $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; $this->assertEquals($totalSize, $deployment['body']['totalSize']); - $function = $this->cleanupFunction($functionId); + $this->cleanupFunction($functionId); } public function testCreateFunctionAndDeploymentFromTemplateCommit() @@ -637,7 +637,7 @@ class FunctionsCustomServerTest extends Scope $totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize']; $this->assertEquals($totalSize, $deployment['body']['totalSize']); - $function = $this->cleanupFunction($functionId); + $this->cleanupFunction($functionId); } /** From 5963b2baf799d99c5bcb7610595b5ed70fc4ef48 Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Tue, 23 Sep 2025 18:00:14 +0530 Subject: [PATCH 12/35] change request filter to versions below 1.9.0 --- app/controllers/general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index c663091677..f0294e9274 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -907,7 +907,7 @@ App::init() $dbForProject = $getProjectDB($project); $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); } - if (version_compare($requestFormat, '1.8.1', '<')) { + if (version_compare($requestFormat, '1.9.0', '<')) { $request->addFilter(new RequestV21()); } } From 458100cd69f426ba0ae43c31efb050535a14e559 Mon Sep 17 00:00:00 2001 From: Steven Nguyen <1477010+stnguyen90@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:42:18 -0700 Subject: [PATCH 13/35] Upgrade base image to version 0.10.5 Ensure libwebp is installed in the base image when imagemagick is installed so that imagemagick works with webp. --- Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2a3e176838..1f5df230d9 100755 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM appwrite/base:0.10.4 AS final +FROM appwrite/base:0.10.5 AS final LABEL maintainer="team@appwrite.io" @@ -28,8 +28,6 @@ RUN \ apk add boost boost-dev; \ fi -RUN apk add libwebp - WORKDIR /usr/src/code COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor From ce539972a54053ed14993c0586d393363219bfbb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 21:51:50 +0000 Subject: [PATCH 14/35] Initial plan From de4f473da192ed954ee734bb456a7d545c8aa974 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 21:56:12 +0000 Subject: [PATCH 15/35] Initial plan for adding webp test cases Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com> --- tests/resources/logo.webp | Bin 0 -> 17200 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/resources/logo.webp diff --git a/tests/resources/logo.webp b/tests/resources/logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..f9bde111dbbd3ff06345f7fa53a29c2013d42f9f GIT binary patch literal 17200 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 literal 0 HcmV?d00001 From 4a8651ab39bbe45293e4830da0a457f0fd4b7e73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 21:58:02 +0000 Subject: [PATCH 16/35] Add webp test cases for upload/view and preview Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com> --- tests/e2e/Services/Storage/StorageBase.php | 105 +++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 6879645a22..8b7ceef45f 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -869,6 +869,111 @@ trait StorageBase return $data; } + /** + * @depends testCreateBucketFile + */ + public function testUploadWebpImage(): array + { + /** + * Test for SUCCESS - Upload and view webp image + */ + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => ID::unique(), + 'name' => 'Test Bucket Webp', + 'fileSecurity' => true, + 'maximumFileSize' => 2000000, //2MB + 'allowedFileExtensions' => ['webp'], + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $bucket['headers']['status-code']); + $this->assertNotEmpty($bucket['body']['$id']); + + $bucketId = $bucket['body']['$id']; + + // Upload webp file + $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.webp'), 'image/webp', 'logo.webp'), + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $file['headers']['status-code']); + $this->assertNotEmpty($file['body']['$id']); + $this->assertEquals('logo.webp', $file['body']['name']); + $this->assertEquals('image/webp', $file['body']['mimeType']); + + $fileId = $file['body']['$id']; + + // View webp file + $view = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/view', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $view['headers']['status-code']); + $this->assertEquals('image/webp', $view['headers']['content-type']); + $this->assertNotEmpty($view['body']); + + return ['bucketId' => $bucketId, 'fileId' => $fileId]; + } + + /** + * @depends testCreateBucketFile + */ + public function testPreviewNonWebpAsWebp(array $data): array + { + $bucketId = $data['bucketId']; + + // Upload a PNG image + $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $file['headers']['status-code']); + $this->assertNotEmpty($file['body']['$id']); + + $fileId = $file['body']['$id']; + + // Preview PNG as webp + $preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'width' => 300, + 'height' => 300, + 'output' => 'webp', + ]); + + $this->assertEquals(200, $preview['headers']['status-code']); + $this->assertEquals('image/webp', $preview['headers']['content-type']); + $this->assertNotEmpty($preview['body']); + + return $data; + } + /** * @depends testUpdateBucketFile */ From d1511d5ba1734a0839100c6edfd17107e49095cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:26:30 +0000 Subject: [PATCH 17/35] Refactor webp tests per review feedback Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com> --- tests/e2e/Services/Storage/StorageBase.php | 120 +++++++-------------- tests/resources/logo-after.webp | Bin 0 -> 6236 bytes tests/resources/logo.webp | Bin 17200 -> 0 bytes 3 files changed, 36 insertions(+), 84 deletions(-) create mode 100644 tests/resources/logo-after.webp delete mode 100644 tests/resources/logo.webp diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 8b7ceef45f..2012be7375 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -30,7 +30,7 @@ trait StorageBase 'name' => 'Test Bucket', 'fileSecurity' => true, 'maximumFileSize' => 2000000, //2MB - 'allowedFileExtensions' => ['jpg', 'png', 'jfif'], + 'allowedFileExtensions' => ['jpg', 'png', 'jfif', 'webp'], 'permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -263,7 +263,39 @@ trait StorageBase $this->assertEquals(400, $res['headers']['status-code']); $this->assertEquals(Exception::STORAGE_INVALID_APPWRITE_ID, $res['body']['type']); - return ['bucketId' => $bucketId, 'fileId' => $file['body']['$id'], 'largeFileId' => $largeFile['body']['$id'], 'largeBucketId' => $bucket2['body']['$id']]; + /** + * Test for SUCCESS - Upload and view webp image + */ + $webpFile = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo-after.webp'), 'image/webp', 'logo-after.webp'), + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + $this->assertEquals(201, $webpFile['headers']['status-code']); + $this->assertNotEmpty($webpFile['body']['$id']); + $this->assertEquals('logo-after.webp', $webpFile['body']['name']); + $this->assertEquals('image/webp', $webpFile['body']['mimeType']); + + $webpFileId = $webpFile['body']['$id']; + + // View webp file + $webpView = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $webpFileId . '/view', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $webpView['headers']['status-code']); + $this->assertEquals('image/webp', $webpView['headers']['content-type']); + $this->assertNotEmpty($webpView['body']); + + return ['bucketId' => $bucketId, 'fileId' => $file['body']['$id'], 'largeFileId' => $largeFile['body']['$id'], 'largeBucketId' => $bucket2['body']['$id'], 'webpFileId' => $webpFileId]; } public function testCreateBucketFileZstdCompression(): array @@ -872,90 +904,10 @@ trait StorageBase /** * @depends testCreateBucketFile */ - public function testUploadWebpImage(): array - { - /** - * Test for SUCCESS - Upload and view webp image - */ - $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'bucketId' => ID::unique(), - 'name' => 'Test Bucket Webp', - 'fileSecurity' => true, - 'maximumFileSize' => 2000000, //2MB - 'allowedFileExtensions' => ['webp'], - 'permissions' => [ - Permission::read(Role::any()), - Permission::create(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - $this->assertEquals(201, $bucket['headers']['status-code']); - $this->assertNotEmpty($bucket['body']['$id']); - - $bucketId = $bucket['body']['$id']; - - // Upload webp file - $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'fileId' => ID::unique(), - 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.webp'), 'image/webp', 'logo.webp'), - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - $this->assertEquals(201, $file['headers']['status-code']); - $this->assertNotEmpty($file['body']['$id']); - $this->assertEquals('logo.webp', $file['body']['name']); - $this->assertEquals('image/webp', $file['body']['mimeType']); - - $fileId = $file['body']['$id']; - - // View webp file - $view = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/view', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders())); - - $this->assertEquals(200, $view['headers']['status-code']); - $this->assertEquals('image/webp', $view['headers']['content-type']); - $this->assertNotEmpty($view['body']); - - return ['bucketId' => $bucketId, 'fileId' => $fileId]; - } - - /** - * @depends testCreateBucketFile - */ - public function testPreviewNonWebpAsWebp(array $data): array + public function testFilePreview(array $data): array { $bucketId = $data['bucketId']; - - // Upload a PNG image - $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ - 'content-type' => 'multipart/form-data', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'fileId' => ID::unique(), - 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), - 'permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::any()), - Permission::delete(Role::any()), - ], - ]); - $this->assertEquals(201, $file['headers']['status-code']); - $this->assertNotEmpty($file['body']['$id']); - - $fileId = $file['body']['$id']; + $fileId = $data['fileId']; // Preview PNG as webp $preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview', array_merge([ diff --git a/tests/resources/logo-after.webp b/tests/resources/logo-after.webp new file mode 100644 index 0000000000000000000000000000000000000000..b91c5dc1fa912333d58fba3f42bbce0f8559ae73 GIT binary patch literal 6236 zcmV-i7^CM>Nk&Fg7ytlQMM6+kP&il$0000G0000R0RS5T06|PpNO=PQ00DPkrfu8c z+?{(zqY)AFN2O!~Qf%9L-sV}AW&o8cNKg?nl#D!qHi47@gh@=y2@{Kh-oI@8%iSm= z=)qOPaQVyk-FttlfQbIXaU;o*RAumlUok6QU?KLAY1z*WDYJdvkuraL+EC)LpY){Y zp44T`{_)M63}nm~zujp~h_?(y%x}NDle!G=(pTYq8VWqhn3Il#8<(}yhJy3&nJ6#^ z0cO*aqI*)Kzj1jC`OBF1?;5#E`0Yh@Qm3{EeP)Z%ATDDuI{Y^-Hw5(OKQ{zYbWa*W zWPRE4)ywR(Mt$QHgy4&NNeC?o^Z74WNn-gc`NzFH4YD$pr^9NAN>ZKw5cH(zp45p9 z!+@`h3PP}-se%xEaSsTI0pTiRkPwi`U&%l2O4vRMJSQC{1630K{D-E;W3Wns8=e72 z85M+JKT`!E`0XAEVB@fbZ}1Sn8jBa`H!ea5K+7194yI9oJpVE2VH>5AfW~EjTSf&T z*w2x_lD|FvPHJGrXA4#SO1{lJ{|Z8Ion(xJ5Nys|3kPaQLFmD7cpnj!?4mG$;RDLQ zVM6eH2Eq`AF$fc!f1iNRhhc<5WrbjwQ%N}?ST$5~sq?Z0he`@TA}0jP<4Uf|-(-8( zuvJl&+*}{6s_7%4Re2@lylfFu$$nK;a<=rVm&cV{=Df+)sHm4MpX7XjVbxH{RryCkRmDj)RkE>0TFJ2-7VwXR zmdKS{k-y2NhR0g?*)}Pa+%;43kN*0kk1cl_UaiT=C?n(ZQqpxJn`4IZ@x42L6-Lg>aNYV%KVo0 z*0%qW-$gV%kN+#~pXT49k1%i25BHvxUc-NHy+D4x{;zw!|E~3i{Cz;oPAk8#Q)djXX;=3|LQ&gKb!w$|Be0Q{9nH}uMghOV{h1UE8({~D`f1KdC<|! zu@p<#0Il+OXzV-|r{v=OY@L$tCmix-^oj3w>7KRRgK*mP;&o7o?46SCAuCw`6W}r^ zB3%p~hco$lxECtRZ~3ITYrOH;kD|6s{gWfFWtygWT%s#?#&;;3W1~}+w(}W^j z=S5IwW#IT3ve0SKO1BW(iepf+T3)toYU81!P7sNAoeq(sak3(m=Ft9V#*wK(>V^*PqOsdx20%Y$hAl$z(9x#|M7z$1 zD2p{9CtsrME?hlHye*<)8LXoEGn+mTcJmzPQrCIou^&Zh{8^xSTSe%f&BH+)Op<$^ z_TQC5U=KU#|JvB*aZ?i=hTQ0Kh_g}x9@5iD>~&G<-R))6N#LrW)b?BcE(;s7TKsWs~mMO+YdtEM+g5n zl2)`|GA=#ux!Ofu1=$J8$Rb4bek{;EEuQZ2S-gp@ld@gsLzH5~i`?vE*#7^20GC%8eI9U= z@%?N?MIezsghZn_y=QC`@7kZ%CTtda@I`i=8#HhnJ#_$7!xVV&Gp+PYvC&vbLNzLD2WJpe2J{FIlAA+0T^ zxX|v^e_aHH{}`(w?~8|`8Tb}hGdYIZSM3?t4MCG%{PbQUc8q%wMJaYMI<=Z)EkccW z*N<(D-}J}%vGv35WagA{X7MPbnZ>Z0)AMTfMJ+rr&hLjeA2gr0lh z{*zjFHP#@ZQ#()!%w1=UiabUR9T0TJ@ltvap^6$8(4;lmw7eSj7EQ?xoDd02M% zb2N+u#9AqMoYZ-zwMDd|1qWI}6S?>0L0T-Y+uxnKmCvAKEA`3KVGD{~aZ)Gg4phtR zYkGs!Nsi&-vd*(T)CRB^Bj|nBcwSD2b0Tq0*6jGv-e|j4y|fjM!5F8^<2ooNo#r2J zW+xT{G#Z>Df05cTB@f&rbzRf168Ubp^^F8(%~k5%f*M{4g(oZUUv_^CgMx)|zu~)v zN!obbh7KGoTVz;b><+65Pu}x7{Cn?QLeMzpA^>|RU9E`hf@f*4={F=nqP0dv2O?$}yt0ygxkt)zJ6gQFM$4S9$ zxY~T=k%$8!bNM0ea{ZJ+mua;SK-fiWnv6T|Kwx((FYbUG9v>M;Sc=aH_L|Gd4*xj3imzO2slyD z7e8dNeq%{gF5oN^2Iq^gdlMfhm`5e%gd((A1bO$nN}7A=PMqYK{TI6beu7AMS@a4( z*6f}oceOHm3s!`lPYc&^@r_Y^-G}gGdgV5}mO@?PsqHexdlN0<9ROo;`o4@m{|4s%sC=R+(M zhfoL@Me>7I2uFYa93K{Xh0oPG!HNCnPxnPEP;ggvY0*@_Ydl z3Io2EL@{)XXz%qPe3p$IT}c2^$ZUFMuXGtudL>V&%Pp&aj~0VWgAwcY_&lnzeUrrS zE;X0yNAcJF*`AJD!7f^Yg1AN^nBE`X8g3#wu4uBDj>XJKYUGK8VIza_ikdSsZci-! z>|Ju|7J)2LI;N=wvimZz^r^| zgW2EU)NiJs{K8(M#M0*fsUp2xcTVHCmn8z@k^7|Y|Kb^IMbL0AG;mAkAV7}jJ_OJP z{A$Q7{u~6AIz~6W!Fj)UBJcu0|NCmrlPJrxWLeg4NpG=V07_*i*ZtKQ668oVl|;16 zAeLT#r}Z6G*ufnpouQg){`O(wS9@}9W$82XS1)!!a4d?ck1bHRy%Rhhq1PNZd62RF zK?z3+q7`)UkGllyt^?7HwgbS~KMO9_Uqi$^uhn!jWjS-u+w25WqAj3H3EgJ;LEodU zKU{1|?KeH*Hs43sVr&BOVy6D(adsZrTq zKGJMNV>c8ER|-77k|tx2j!&j4T7q&|HyU;C^+b^(ao%H>$l9dUirByx9P2Ou+45S3 zzFDpQPu}?^vC3&&LP={Pl?6x|#Ae668Vf<{lZTWPpC4@rnYajhX~*t6PC8L6ny})P z4I`+2T=ASU2f}gXZ7oZ5>@aZQV%s9a7hrW*MUsE4wrk|)uETbe;%QcP?rs{+_qV|3irS#;V-0$P$bS4bOs z#D*aqFbH|`!#V8Cn0y%e`KHOI!$d6Wyn>}Zpe7Tnwq%}_y(#YI6Rj#=+Tb(hKFji= z6E$)3q+U(1@%a?5^aOl<9T(e=V#cknjK7irWJ(bWn22l~tYXnF7+vVM%22RPz|W8( zsylO$h24CS=sxfW8JQ|-oH3`L0{kYE5!QkeAXQcMTOE)}{o#A!AVM46@zwhVAM2_? z7Qh44f45o047Tv#D06v8W}Z$a%;x3mUMVva&@xk-ep!$I@il+{6kGrQ6K!RNF2L%r zisb)UY}d)pT~o%d#~OpCotl#i{@n^D4i3WSn2?Wq+FM#}1&g3lxN%aP=E)smY#`6f zv#6w5pq#;zb&led+f)1H^X(2`SC1d47CeH6&{wRiI3?SZrrgpLqPr+HG|#otJb3ur zv<0#OLc7 zLKdLLjCZs&$UH%^E_g!xENUA8+D@f$Z?gf-QIT(JSg9{+R?NJlfB{T~d7gB2bFLtgiW0rTaCI zC^&d?>t=8d)~EjrQ+lU135SYtIU7z@9S)bl*0k9PwIjUG&TLo@v_KK&C^Wdm*CQ{Hc@J#EeCz74@p}+Z%krc{FtJENESs96hf}r zQi}mhVXRA%dl;S)44z_Hc@g2GNkn;f7;eCoy>%7d@)@qp^mkbmSBeIGPyF+fl9f<; z^cGCZ!Stq~88X-*T1ASdj_>!{N4*nzh=w~A0S^SO+h9sNgUG40+q$vkquyYzlQV~Db1m6v9HI$lZ+M{m)`h@#B z=n{8|O>27W9Y31|L;8PC_qZZKHldtkr%nHxwxL1NPHg^g6iTTU9aYakFRrnckxIEO zXk-^ZE3Se&L)Zb8)b9V)O&*A+2$$uYzhx{3;RaYJ;3>(I3%DRRu=zMh$j@onhgWCT zmj?IcNC!&V;(Ym&v@~fa#fTUiMDsD!dx2(A#fQY?!l(x={2@ld%n+dzmnnp9$y!QL zZU(Y8MWeHZ7l>!`=rC-1`1t5Zj|?C`eVu>)$$9PeijsbcueiD0!~*KkycDn>yMOgu zY^|V{pr9UB@`bgKkiSqMwYCFBgUKn(Lzt1%FvAEsUeF!XqR-LsfEBJ>dhmL50QjKX zl+M$gsTA2aXjJW^?XO@S;r7D-SW|Z#_F&b3|An-gY*lNb%DaUtXbUplIOdcY%C6cto+5^!mRhQ2E$tkrIr(j%tx)AG97;_8t;9 zutw--y6f&ZfRK&i)huO3 zR?tuumnt047liVwBkNt;3Lr-R`HKzKY1$I7DZUC%_iE6+D{v~W=FEWoV>FU)P< zfuy7b37?)*I0~<_anKBKpGnaiDq-NX2{D8um#&|1P<8X8M&WdccG&;P9Pyko!s G0000WrY4pE literal 0 HcmV?d00001 diff --git a/tests/resources/logo.webp b/tests/resources/logo.webp deleted file mode 100644 index f9bde111dbbd3ff06345f7fa53a29c2013d42f9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17200 zcmeHtQ*bU^vu14Dwr$(Vj&1GOwr$(CZSQ!;wr#!1eCJHnslR5X=IY$c)ZbTK>!Pc> z*IM0ALs?ovf=Uz!NJCsiNnMGH2=5>M?gKa%n5F@&0ZiZ*RkB2JQDGqoDix9g477#a zmjm~~oPe?QvApKb%kH7SEg#Y04}~A-SN>J~>)f_t9<1U^{jEWPKc#@VJNlZzqrizD zeoyw(?tTB3;JE+w_urp?Xlw2{|Kx|gzZ^jMUH*OgCD>tquK50QXK>0y6a!F04Sz zTzM-ipi`Iref)2q{QE)tfBPHqzatrB7U9f=G(j_IM;KD~V3G7B#p;o|+*KrqU}8Bg z@y_>2S9r@fEMPA(8Lp2>xXNYK=I^D%B=Yo$UH&CF5E6mygd6c?%_GWx_~sE0`Ajmsfvw)!**n8I>){kE7XWQrX%O(s+B zHLoo2PUpQnZFsl?KOhlt5@aTjy8_uyRNI8oPxb~QjSV8cCyRfX8HUXOc2zs6N=O1C zKb6}eeM*UA99gqVs>rIQ&G=%JWf&TRL}s+3+rL+dzwPWe6lyzOs0SUq^!F!bQa(75 zfF!8?G?VyXSX-HEgbZIMdaOtlH@c8p=7FM~Ag`X5?0I;H7-tx6WR8Wmb=@|kyYrwN z!+xk?tj`=B{0G)EI~I3EKJkDw*^a6599>x4|69l?kfy+X-~pVKd(A7E`6}9zE+aMB_@m}VyUV-{-kBq`Np-=F>0=D`F2@xt zpJp{Tg1{P?UNdOg<1KGH94rz~_(W+4b88!I(y&`izKS9}E5xCxtU5o+9(sn&4;5uJ zGA3xm`YsF}K8!(V%oW7)3zSI+zF>RD0_cZ`tU3II#RG}3|TC&WC97_h#Kw)(~E@QNxixO&Hw~`>(vw&xF@R}?p3}fjpgmy(=n&OnX zu~uAdO^KLUY!r2+yPuHpa-kcSi?IU;S+mRt$mf7>cbTgR(yn`YOMQYiQzvY-;gq2K zFlU`Z1}ePOj+wQaq7@M;&{0o3b9eX-!$nMmXl}SR;X=#6@1mM6lYo+dDINKyESTnf zy|tB@mh6qO;*3$OFX$%Me05>h7EUWE;DNM+)HDc(=dq}>k^|mCT6CM)(pi{vhC0sT zVfxbm=7BjGrvwq0;M;=zrA&H}QNUoj`MfF794~VyL!(gapS;^s=N%k~VyBJ-NlU~# zt7_5Vp~}FjE`ypHU7A*p5@s8a^?}6PE48Avm4<3`bm>NKAco^CF}<0gQ{>ZhrMY>8?TKNOXL|xlL^?6f2 zab0R<25HOH6>CJw;mD)0l-?JiF454vZac!Q_q97|wXPRY*sHnJS*!c3_sS+@&S3sS9@%B~;qVEp4>y_&zIBFNGBrZ?s13JMsSu92)lsJ^qgn`mX~1 zce%>@BFgL;s!AtE5ny7Y>NSLx$rwmwe;`H)W?kG=>ptCNrE-7jJOy4)s~(ajeO;3#tK%L>F=|!^SY;BZ~Iq;jv#Vz{5lWVJ_9OK zm3g{QoixW)3^?(DwOte-;`OSqN?B0l+2 zGK8db#HD=84^V3ay zlJI){JvHYP8gkI`8j=^kaO^d8*t4^7A?cuy(Kb^vB{i|-vycv|&mjAXLQL%a!< z^sTIztFRZG9J1nSxX>HhuEyYS!gBSf$b66;<1><59>jm9lmB@^|Ki^vB) z#v{;|^T&(uV13l`p=Z|&Q}!1VL`W$M zW-vH^GU6XiZECE2RTx=Cw5y4?;0!|6xS*@m!hP&Sq9t9MLoQAq)1imAhD~IW02epR zKk8JjSM4hMWqSbN;q{Asck5iMY;QV*un5Jkb)wX{)F_peTbALDkB12yDTir!@}u8bHsRZ|kU zD;8OZo?S~Y8=P>}c{GuMjA%@Wg32nTaz4l7%IK8CZ?%L*^Xj+8uh)S`{7~N-T{Sf=X z?WZa1Lbs7=cMH*PyiabCPbyH~2|m}$fE9<`D)7k9IHxcXv|aH3Ca+sds- z__cY8LLIp`k{yW>iYY`qj{bSvl~!lR3M-wSQVYhv5P?(-rTGjnmu6-+gj!OK9LL~ur`+r_oInA z6^|^$0%{06new1yDN97!EJ0RQ!{W7*#>AkaA$IxCar9T z2UJd@S|`h4JDD2kPg*B9J^}j0&7*>Yl}TOX5O0S?rUo|bs750h9adW!@fmE|DGm$W zYa2%1T!wtQ+>|LcvS%=2-Cg|+@KM>B`nU8u@9`DAhTeqKn{N6jCF957^1q}iV~WH4 zAMBj`R!R5b#jcZ4*e~oR&J(q$892;;%lJ>{h3`@~`_}DYKzW)uoiUz;yK3AFsly3$ z;Lx|%y_+6ihK*kwBm!cr(*t##QrPm%K7R$uefLqoPG)?R5)<*ejz9zx*_YTPV;7)CR*pjB=TH*yUqA)5D- zqpXYvne1RdDs1#HcTLQ-1c{G3d%02v*(H^s&|d=yD4MN}4v(2;=%;4669eUk=V!6M zt^u8tFkqn0zUCKNvQz9%Ol> z=o5fS5fx_;!`(L0bGYbB3$<sj)1L6;jw1MZ``=C)lO7c4CnAQZr-+}`4Yw4y~n z)PTDwG|(6By-3EdY0}%;li!sye{}8wp_HlMkOjzUlYmRfE_N0{GuQiSqjUy{%NS5d&u5!`G(hk-oLPG(!N*D%$KL(JqlxsJzeU z{ABAg7&~{!8nF=r^;61l znNP%2<0E1&Lc@EL*6a`1O}vk#g7#FJpmt?fARk;d?$yyU0zzT0a* zJIY>DT!v%^{hgx(0Uj$+LXdwHH~b5V;S8N*AczcS=f|pneOdICMMFJnG;Q&{Bm{YV zfm3%+T<2)A4)XCwS^66&S5XByBCwtghZ zbbu2)t`#W_Ph476P<`FblM85Fx5wkfhc%%m^pDl#X#_FnZ=zB5f~ou1UgcIXK!nLnYy>iMK7rcoVJ!_q#N?eBQV0px z7(ChzoRws&xpxCc$B!>ba4Oo+wa+lftOD`i{*;ligVitNu|y4uh5bHrEqP|<$07eT zw$l=wK|Bs#FlXJ`mEz<%f8~VqObPOhU>~&yay!jquYF2GP!oQ}Vp#V_MQ#ZwwoG~N zW(!UVkA~r_Ha;QCGUtx;xs@zYMg&O~z01IV0cH&%P}L^PW~x!~tGO!d%XQ9iAEeF( zcAVe;Zqpx>4w_=iilhlV=VmO|0E z3GC>9=`s~KnYZ*+`WYW0R60S4z8eOs7#|U3Dkf@5qjOPk-@~OES}Toeg9~2Fr%|ca z!M*Bfgs+IR#sNEC4QAH?2L3ZP|e?ZRee9myb` zzP%8&3mQ9;n6!X(JI_ORKP#18r{~bXwMKbJ&K|YoDTyH$dzLU+HZ%?8Q3GeWz<;1w zv@LPNgRexYBrH>8ebSv6ITjH$ePPf5k@rZr$%kCB=%Yus_ZhAb{MA3~A%m6th$dj; zHa9XxEiw-fdm#erVC-F^Y8fRiRcK-tx|xnv=1*jdv-Bx@gf?HiSf5?2`J7$a`z$C( z0QxQa%R-EfTt6XZ5WRMav}DR#hH)QXq|lKkPCMCyB{;xas5rI${=%Z;m9|>pTT56* zn93)V_Kh&C5QF`jazi!;hRgeyLL}Fre#yE5B(YC$rHCrOSK7j&QA`Y%QfQ}AAQ)nM zQF^fEmasy>F}SB}Y@r3AU1s~Kue>0#gu=l|q3v3-c&`K^9^yuhJH(!!mkZ{~v6F^o z+V>1F@V*-JjlcM~7!OR*!TvZ%B#}2|IMA4!1oyrga?F3?r58r|b!E`NZkhliU*_l< za5Iz`3ufLdjWh-%KK7KXU8%Y6{S`~Pgd}JWbCw6Dk)0Q0+Rs^0R`B&WvBwZS^p5E` zD}r?_6{{$6t;H*-*&WMg{o!@9c!6o5nDk8G-7v0}GP80-uo31Gjk@|`tuj*vU1RMw z4X)7UqxcCv)8xX&H`33)ve@UQ7*j|=wo8HrN5R|ZBTaJ|0%QBz@$P6NnYApqMB%2x zyYd3&A$+bqRGjxiHxCS0;8ySCQDBppFDmUO#^B0fFj(2je`q)q$6Z`q?sP_M10^VC zKGh9|I<`3Kr6nJiMO`r%@9+LbQA_O0`P5Uk`!L5`^?pWD9k2NKcDN}T!%?Y2V{xDt zuW=*piGWBUrK(!c%huGYnY9&scS_Q0*RPmIitV0PwTevnoZJX9Y>~m>zpt$yhZr}9 z8Zv*h&rmM)dpd}rX9$i50x@-?a+v`Eh2iXGz46`kKgY)Y#O`c=ZsJ{@ zgaU0buDQAxXn?KkmeDnkqEbFW7q?avGsE9(3A{_H&}HG49locvwnOvA%`O$mc59QF zop{L2?ydnT<~CY|N2ny8`eB?4zEu=1Lf>6xn4ougHCK=k8h*cctxhfw+>8=hK@GM8 zULgo9^sivg^x_rt_Q@F{Te++A}CtA?1Yi13x7oKOD!Z z6wAKtQp))SX4jePOV-ra?VXV(f~st>3!m%WEEy<9a{dxiRg%eFzF{Z6{%}o`Pf5U= z3_aech5vm}Nyc;(``I-o$RbOi`ul4<^Z`nq4o0T*w!p=Ll!r>_otG6UiI}pB8x(wS z?=Qr`(yU2Lagn9t>UO1y2p`I6F4Gnhut%!bj*%o5CLT6r0_c6_|*cmOf?JGrtK zmP9_n%0@4iE?(E${a)aR$GXE7#w_^;i0NE_9u09%?`iHTms z!-Ch+!I{+3*$ai9U#KKL%K-g>`jERr)Pt(#Ke}F5^Te6iF@{wrU|?UzK_yeepN(!1vqdDL;n2iDS>`jbV_6x zh`b=goP&5!N3Z^z>NP0|EFyH=%^3-kTD<}OC00E^xp|BJ-x<$8A&_h%+Vh7NIRA_| zC5K&1vH@#s`tFpJ*I7T+nUY+02km6vZWEpF8Cl8t+faPdo_cc^FwfYatt(BM#y3RK zB|T@qX+~BAyr^cUYmG%7eKYXfpE-5r_6tk|M2V1sOPW*?AjW^)dx-E%%u*uTK3*tU zw24*-$H}pGo*Et%rhXnu>L^N*1$bd-!r%s^5+=;dJ&;Vg|9g zvw!MG*G(&ZVTo2@81=@2PyrdNfryS`9%Mbe~gFnq)i*L=@gTEMnAg z<}TkM<K!+KcRn3$~ZyDKWGHJWvtpMK*mXQ`9fL0Qegiz zohJ9odSxA!jSO(AmD&_}4MsP%W&2I^C7>xmnPGx@Q?|XhcF4M^UYl#6opR@PK?tZ) znd9y+a>u_|NWJy&*b9ot+#GYxr%@mm&!O$MyIq~JuH1-s?A1P;gSrC2gtc8^kUq#0 zY-fSTBOEXvg*~3)>fRTQmwXQt{_w(DoTLdLlVi3gFpG>U=EYGo~R2aE*N;&MBeyQq5u%>Y+PK7Vlc z_%jOC6$42sWF9cH0;OE9_n>T1nRrhgWf1+Tpw$r~CCgw3wtR$gxo|&uSxFG|y9gc` z4?ljnMfQM2Ek!F9frZ;DTqqow^7*nA;^esblSD`ShLgeT(to=(Q{@4c@DafKxP?+t zp=2Ll9-d>*-M#|IkGN~tiX%7T!?B!(3tWL;Smk5mI7zof2%d2rmw7s;%KeVR)c*b5 z{h}h(cypG--kg#b(OqCDVeliea6c~j-|kUN`~H1qW4 zwVqyPcX(i>PzlaYsxF+@rUek3VyWGc+XeKgQ_aoPK1?P$+-a$YgRR-OJL9`6Co;-`*(qf`{hz0^(NR06B zo+|60&YN5&tfPK&Pe2OGgHkI#W)REI%7Azy=k)-HAyb`ne3OHN!vU3UMNUD4fLyD1}0qv&KC(ECh= z=3lW8(&XkD*@^B0Ax!>l+oWp9a0ARchXalg_5O`yILDiMbY#7C9i#^9CoJ6c_N|5e zQ;m|Pgr2oFrH{wmk(#_03WP<~g}oLr2_rw7F7AWEzv1KXNQ&1fcGSGC(nQN770 zBU^=X&~G9xrV*3#(0fd-HzOSo;j}x{g)7H5D1hCaagJ&%)pg6S|Zj(v*Yj ze?c3UoLc?E^E>!z2q;UGXI&zLT-K#@Sz11M7i2phg=GhW!fQz?bsFFbzp=R*pr^;j z{xnO}#(pmUuzY@&cjlS#CJs%0jk@vcTa(}>JpCr^&l_n&^RdWUZzr3CY<_5+J*kpf zwzsxR8eg1`F9a0ovI>5d8G)VJLoKR1^Iz+kvIj;DoqAAguX3=u zLq3ApUyL(aXn4oWcb7CTrLbKQ(O9?%B7B9@w*18r{VjtvPT`Ig(@7YQY zK_dil!%>hMMtu#a?U@ns)IR$CZy1Alf#4#=MUqX&%gB(YrIS$kE23i)H?c3l8(Rvk6`0aJo0Fc~5jRwc#UTnF~*nieuhYkU|96;4;O#%^|OB!24%=z+bqJY|^c zrFiNP$Bo3Hmd@ns51wgUrELiLwaSzS-kdFu)~EOWd)<5Hts8Ac^FPtwY>B?)PXobT z3@hEAp} zN~7tfyD-zQ45Jwje^!(1XO6&XGd+`@u_5Hha=Cx)8r#|i!%rIo1#Ca$+~nhyse}2{ z_hQbiR0j(U?~!)SO40`&8V%2*@@G^Od4SvVK%VKU#joOIen@UCmadr;l((h>O_{rD zq|}PNjvzX%W+qfV!bcCd7Eyt*Bsd(6end!kYfE3^5Emc<`qs(WW6E{ zlh}c8qa^c-PJrlC@Q0Z4(ic_MXLD*hsSiv4b1)f2XRrtx!)HivoEjj-5sqK z)zq8fZzal_%GP2swi#MG*>vlz7dv;In4Vj_I&G{ z5UY@76Pp!fmFy6@9D_)EIZ!ipy2Ee_nyDLQHF`y4o`^COV0&Ut5mwv{f4x$Xdgt+~ z8_L@^?FeV>25Y_(rrj32cr;ck7RjNnap~u*qe@)SObzGnUpd1oYoTS-kyD@TTec)- zFAqf8xUEM0^p&W)?*n8=I#Hmv)n6@H&;&j~C&^lH^dS0=xun0AG8Vq_5v$s7^xC+& zbE+{Z$7zYez!{@a?^I?R%T8b4oSrZtuLrgM8la%MV;_CexT=#+hW@D{Dc}WY6VAAQ z+!rm{Q^9m4Msjo~#)&!0LDj{VV`7B;g~pyaC(>_GI@hNzrP_pDmOlOp)`k`(kr54_ z(ODJMtxQTESS$!&Tjn?zX?k*&=)vhOclIIWv+&w(`@ECx7OK$j|9)Wh#7&%d z_H7mGuR}oeSY!n${a3i>9NS{sBN4E_H7jc5B5AEQw~ftdd`nJEI;4<%MjFjAhvEsB zxT;Y?BG4T;2u~|@LyJLZ$2RHq5LFyQz-LO1LPjlaIxDe_w75|3z`znKb;w5uDYYO2dl&PTlv)YEF)a6Psu!`HqM z%ODzoR0}$gg#lJI~{Hkjz28Yq-4wzra6F z26=-P`0eymJ47f_7NBw=&k%t3*F;y|(JwJ&cQxvO&5`K`&x$46ray`sU6aq+s znEG8dSj_AM3m66RBmuPpk2E4yp{&{ce|!7xw!Rp(Yqg-2)c`@`M^$#*9^3l%lZdDQ z1#2)S&k~lKWEVL8z51RPMMEG=05i$qV1;VIN900e#QRLEE8g>>s=wRXmuQWT zo^K~=ivbNXm@;&Bd_8V|!#0EgUmxYh(B1P9U6m(XRjC1`E8~Tm?Bc4c6<%0}7mGX) zBu7?Q3#Klt53v5S!YW%H?$6X>CdpC@qUi!14K#4oKrbhf*Lb9E&*L#$$sM!@&!tf_ z{@Pb^o{u28pKOg0y^^HjcAyVM_cM*rd!W|g)ZFiSi*hquAXQtYGoNc9BUnFEbZBA@ zJ9zdF4IUr~HQ|C+Hxq3%-1V)TJ;$m2`oK$rJWi`bV^PE?JdC9%jVJ>em4JA)adMDt-X_43;lIC>prn$!erS3~&sC zo*NBa4*=v6Y}CM5WQF%kEgCfAMw(4xEoW1wzHFY{?#DayqFfjp`|uh&kg`fQGyKcw z0Sv<0(-;>)Bmed!!)~Ob%IcYpChJZrR;Hu~BqeS2zr3?D8%v{?NCllIDg<8Gl;Eqr zUsOV8tfbJ5F*$%ai)`9sLQedC2*xY$x=!niOW3aJ-p2YvoV~=EK%_G(9cK0+Kbe`E zSmRIeqc4RoL}pN|Nrj&g!W0ASX5bARsAUjls|F+s4EYbdu$Yv3T20rqt(U_uu;AVxX&?*vV})SVIBws*YBl=A3^FJF;QS%${od7wLMNZ*!bNw}<;tVytedAd^#NdFBRvGXX_;)2x_+0o+$+?N?+!_| zq7u$D-Yo;AeUT80MAy$HW>J@*mjcYT^cPM^CEn*N8IL*@CvU0?4M$k8M7~E#bOib*EXNj|YRpqd zW7vg;B4d06TOPt1z=!uyf`F@DaYQ~~AJlZRGU=4arwNb-7VZhVP z!wb2v(Zn}XH~&icPKq$TEo$L_!-YN8{1R+gIeg+vjkBn2H~8%mM>j4HceoY4NZJ2{ z9P`B$s}N~_4!-)fhG@3pDZQaY;9 z{Ig^jBZk35R{0fMK^w5Y(ec%&i2u6z*kVU$_-#hHD|QLvu~&pJB@R%Qs905v4>}$j z_QEq@{sHyF$>RU`P+TaEjFjUB;m$Fb(N1mi*73(9lBvw0Z3#tF*aTK*du^qpri1C{ zu{Ziydmv@lbk7MO^DL&Cj@>4KxA1s%OcpP&1G7Z#ra=kvHtJ(a%&8>{EGEE;B#h** z5nYd6d8~U+`Ou3_YraGoPm?e7rKOHSk;RL>-V6C$xwjvg~w(M!_ zE3+*(`;&fln?U^*YLQ?caul5WBT@9ZM+o_GRuARb+jjV26eT_18aD>u+-z-iT6hbg z$tmcUVC{g!vF#kd)z>|!I_2o9ClUFSoLb-$3)@^L7eHtIP0S z;)>g}nG&a>4;%i;XPJFi;Eu&dB4hh zt(6AhFT-!i?To+JEAL*Y*Gdf1`3u9#fNCK?eVOhH2_)TyNm^2Z$37)9+p0Ca;Sh!J z>6Y&~1Y3ex^J4r3zAEpAsNGO{4JgItDsxx7@r|C*B8C#s5F7;h$k!4 z4G0K3)l7-uAEZ0R}7i-8`B0vjzKNKIcV-l`9jeXQnu zB(vP)(MCog1zWQp&0DUv={0sTjHMcH8nJ~Fl|QNT1-iGS==7+lY$Dhm1gPxfO^&aq zmfEd`)a(9I4w;7$=V+w~inj;kYl)r9%O-mU4#Js|;iy9(y};R*0Ib#9-Pw<^z7Ooq z+Xl}JMi*Df_=sbVk)1|oacp|(E?^1$mJB+`Gc?3#$Na3%%Y{VyAfwb-M<-5I2^@X2 zuX4JSUPWDd=%j_Hf$ZUxcuxfz$gCLfcZ;e!hnzW=d^uK8*;pS6*!jx}b!SdAW+R}K znFk3vs&&IH{OZfFq>G%Bt^HVtGB>PPe0>bVZ*4tNk zxy`k&)8YX3fzAwF*Rz`7bOmp&U*Z-aiDW?)gY&S?>!92 zQ4nk@M*pniDo_)-m;JR0M;*u4yrIMKXPH*H< z*ny2l;nw1plH`n)HbPKe zzk}ppX!SNw*{=|1Zu;z(k=FxEQ>!J7mQ;t?kd-gy0Ug6+>RvI& z+2utn$Ur!KYJ^649C-W+r5kJdBf44pDwS7T!yKYS%K`K$XKpmB)B~R82nfCnw6ka{ zdk9#iB8xyzoK0uEldZ4Mx{In+Aw)_oA2-wtQ}AVL6KlX;Ho9G`h&Qn*9njPgv>Zqs zlLk(Veg;{Z6u*5hP5)fZZ?NVL9(A{`dK9!66acDzXz@^ox#S-#f)_P#vAh_>M+;7nFloSkW^Ju?7Uh752TQ zD;yxU-R~S&Cj#|*sxQqoy5A&naYQ1t@9w|$?129uFGx+o`l$P-@5{Z;3E4v?q?uC1 zPU^~jS@dK2n)#(#3OlpQF8w)*Cd(Yl)F!AgWVO@4Yryv2)r2owz(Q$yX-q%a6G6EA zcBr0RLhpCh2jpqb&F3?> zjVCbMdgmzS9^=FiAN=6;LTRkMxE{R`%lxI89ZP$+i_5qXLQ+s>gcOG6Rzy-#C{9GX zV&OY2Wf?aKJ6fGcR(QCAA`6qOFi00Rr$vd=4>KFTN!Tld%hS;jYVB;1uQ@-VP-GYD zo+i5}*or-v&F}x@h9kH7!V$6HOG;MNz@dZ>perjTm!KyR?;vv_ZWOa@U@4biI`i_9 zK~vsMaXmv_y(ZPZ{?7OO1}pct`t2lZ@l7AK;$Q~s-bG3L0D3AXE%Zf6GGNuMiJ?d!FEbC zUX-0IntRKH_GPj-?^uMqWFNc(CIAutSpVjnGOrQIx|sDNyk&Q~eunpC%z(Ubd#hq3hC{ z-i`-tAh3|oos{2|H_5A*4AJk?^#TDZSJB(#wt3A4-}UhgO86vb%KK|WGlhMqVVPBx z^ve1d7^E;mEhp~Zo>3r$=+e@|H1$gQ7nfeS?q#G^cusU-w0b;%jh3r?eWWb`yeGN0 zG;)NCS?8SZfTLvMoIq3f+8$AZO`UoKh8rkdJ2L_<02T*(zeDcoXRq}6=L$DcUWVNe zY)7*%x++___>Wpe)}Orh{X+VZ0#_=tBZ)F|Xjx)zJSe1dZVCQHJw)E(tv}M7?s>sHgaO_Cj`VXkRhnN<9$lducp`=`6h#;S_fx75GexIDmoah+j^ zlB0yDZt(Uo@LdLYwj<$SVrIBk;FtXW$yyyjxx*suZxWr{nTB2m>M<$3^}hy>zzLX4 zPQ^6z2*Q&f6f7z0t*i*r|Cz|i1Pd^CE{(lzx8N{#p+lQjX>!06xHl`GW`J8VJ~SH^ zPSh>US4r$msIAiVa9bitdUWBQPZa@wiwkUk>_+pvO3<8t@v20qTSdR|l_Q0RK_;9#&YQCLxze( zeQ>_#UuVl(iCfjk9s-AV^hfa3cz%qEY4^;a5;B*yd(8Kjr-bV7z+QpSk8%EV+M7!o zJSN{=p2$m8O@04<;CFgTtt-8jlePFlk0P_vKSdoZ`)-&=5SR8x&*+2tD22T*c$NU4 z+W@&lf|XqFoFhsYbdEC-T+`B;fzL_kw|9R74vBA*3+HFk6*U4Fj%VAYozZ_lLnE)` zUej^t&#Rh%=zT<;Cc3*w0>&zPHgR*?maa5zjPu*_^rr53K6kgIrvn}=0vO#`vSU*u9v%466eK7pw731)?Egy@tIV|C5VCwVZ}w{%*kcr@*6mhE#V>v zV!6vc@HrUyybcaBOGFUT^w%$1N~0}|>>j*9*;HFf5ih<&$#Q1QkCos}bD0SpqjK}( z%(eF<>jIIlITs1gxSsKR-_#d=bnAiFNoAXCFStm~+AvAKS`fMz$M=))aRyve?W@+2 zSf)~ED@dq1FN+0mTl*5$%bncIp7Er1YKSwRoaCNJfDIjy3W!WGSZj|;$ayS$xz(wi zCjqwKbs%f@Cw@17oyUP#YPPFS=LWxkLVid}7-LD2bPSSS^+!ttu?cxmoQ?D|6kY&a z=mo>c3Vk&v5ZSJQWR!vNK$JnJNkZRluX_XjQF-gR7I`fRx{e#n0N4j`3~lbd*bW7Y z)?kxRI(9~#Db(cYbnum*NOXr;f^o{r{1v`J@r6$K9^f2D`OGWspX4HLlUT{ z$ds?DMx4-oW`d+{@AeHxGjWt%SlM{EAAHvam<1fc42D?= zv6#fiB?Iy&;ps5^gNv>L)dg!TkvtXKMV>0YO{sw#b?Gd@BOXw%)qPY?nP1J%QW^ZU=;tpHUK9hIAMgnH`m=Y8Gi}diSn@8mI1@Iqv*;d zN(hpQsrOzS`52x~5WnmOuif!Ctzg(a$r%>gL*u$o-KOXJz!CSsdY20T3r7 zPMhc_VzT>RD5~l1+i$*avRp^tSOzN?l994>ft>E%Ur!hfsN}%|e%PO0}@4x`UiCfcM zJ;4OCHm&g1OXjPBUY=-i9ci;PF~@uC${H=q53BipY=MlUg(1lbKaQZ4TEtaHB=%O~RgBw3xru#GA%-_ceoVD1>7f7m0K@UXXYB~4E;$+!_)ypUH3>1azdI!tvbn>sJ9r*sP zVsD%L;d`Qy7l`}}oRWIHQengHZo2~N*Wm~(2tLou312Gh@<3Z~P`YcyIicNOHuH-VB(uiStHPbO+(EZX$qK&OY=zCk3T%Ts9Imxuzozl#(De(zqqKiONP9 u{gzu<_1;wsUQ9$|1VZE?QXN*0Z#OMu+eDgkm`#Ca2-hL}FSz~R%lr@fTw_N7 From 10a9845898b57a7e2e8b9f1ab30b9d91b1e79005 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Mon, 3 Nov 2025 16:53:44 +0530 Subject: [PATCH 18/35] fix: Throw error when file token expiry is in the past --- phpunit.xml | 1 + .../Http/Tokens/Buckets/Files/Create.php | 11 +++++-- .../Modules/Tokens/Http/Tokens/Update.php | 11 +++++-- .../Tokens/TokensConsoleClientTest.php | 29 ++++++++++++++++-- .../Tokens/TokensCustomServerTest.php | 30 ++++++++++++++++--- 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 4c4e55ea4e..a8578995c1 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -31,6 +31,7 @@ ./tests/e2e/Services/Locale ./tests/e2e/Services/Projects ./tests/e2e/Services/Storage + ./tests/e2e/Services/Tokens ./tests/e2e/Services/Webhooks ./tests/e2e/Services/Messaging ./tests/e2e/Services/Migrations diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php index fe7a0187e9..0d905838a7 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php @@ -17,7 +17,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Nullable; +use Utopia\Validator\Text; class Create extends Action { @@ -61,7 +61,7 @@ class Create extends Action )) ->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).') ->param('fileId', '', new UID(), 'File unique ID.') - ->param('expire', null, new Nullable(new DatetimeValidator()), 'Token expiry date', true) + ->param('expire', null, new Text(100), 'Token expiry date', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -70,6 +70,13 @@ class Create extends Action public function action(string $bucketId, string $fileId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents): void { + if (!is_null($expire)) { + $validator = new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_DAYS, offset: 0); + if (!$validator->isValid($expire)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); + } + } + /** * @var Document $bucket diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php index 7a15708011..b1b670532d 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php @@ -14,7 +14,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Nullable; +use Utopia\Validator\Text; class Update extends Action { @@ -57,7 +57,7 @@ class Update extends Action contentType: ContentType::JSON )) ->param('tokenId', '', new UID(), 'Token unique ID.') - ->param('expire', null, new Nullable(new DatetimeValidator()), 'File token expiry date', true) + ->param('expire', null, new Text(100), 'File token expiry date', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -66,6 +66,13 @@ class Update extends Action public function action(string $tokenId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents) { + if (!is_null($expire)) { + $validator = new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_DAYS, offset: 0); + if (!$validator->isValid($expire)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); + } + } + $token = $dbForProject->getDocument('resourceTokens', $tokenId); if ($token->isEmpty()) { diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index c0f94a55bf..ab3790abca 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -9,7 +9,6 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -use Utopia\Database\DateTime; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -63,10 +62,23 @@ class TokensConsoleClientTest extends Scope $fileId = $file['body']['$id']; + // Failure case: Expire date is in the past $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'] - ], $this->getHeaders())); + ], $this->getHeaders()), [ + 'expire' => '2022-11-02', + ]); + $this->assertEquals(400, $token['headers']['status-code']); + $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + + // Success case: No expire date + $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), [ + 'expire' => null, + ]); $this->assertEquals(201, $token['headers']['status-code']); $this->assertEquals('files', $token['body']['resourceType']); @@ -107,8 +119,19 @@ class TokensConsoleClientTest extends Scope { $tokenId = $data['tokenId']; + // Failure case: Expire date is in the past + $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'expire' => '2022-11-02', + ]); + $this->assertEquals(400, $token['headers']['status-code']); + $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + // Finite expiry - $expiry = DateTime::addSeconds(new \DateTime(), 3600); + $expiry = date('Y-m-d', strtotime("tomorrow")); $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'] diff --git a/tests/e2e/Services/Tokens/TokensCustomServerTest.php b/tests/e2e/Services/Tokens/TokensCustomServerTest.php index fe8fa2bad9..eaf30049f9 100644 --- a/tests/e2e/Services/Tokens/TokensCustomServerTest.php +++ b/tests/e2e/Services/Tokens/TokensCustomServerTest.php @@ -7,7 +7,6 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -use Utopia\Database\DateTime; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -61,6 +60,17 @@ class TokensCustomServerTest extends Scope $fileId = $file['body']['$id']; + // Failure case: Expire date is in the past + $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), [ + 'expire' => '2022-11-02', + ]); + $this->assertEquals(400, $token['headers']['status-code']); + $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + + // Success case: No expire date $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'] @@ -83,8 +93,19 @@ class TokensCustomServerTest extends Scope { $tokenId = $data['tokenId']; - // Finite expiry - $expiry = DateTime::addSeconds(new \DateTime(), 3600); + // Failure case: Expire date is in the past + $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'expire' => '2022-11-02', + ]); + $this->assertEquals(400, $token['headers']['status-code']); + $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + + // Success case: Finite expiry + $expiry = date('Y-m-d', strtotime("tomorrow")); $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -94,9 +115,10 @@ class TokensCustomServerTest extends Scope ]); $dateValidator = new DatetimeValidator(); + $this->assertEquals(200, $token['headers']['status-code']); $this->assertTrue($dateValidator->isValid($token['body']['expire'])); - // Infinite expiry + // Success case: Infinite expiry $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], From 60e57eb9c59185ae6f83d5855cdbb0927870552b Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Mon, 3 Nov 2025 17:29:48 +0530 Subject: [PATCH 19/35] lint --- .../Modules/Tokens/Http/Tokens/Buckets/Files/Create.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php index 0d905838a7..aa06753420 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php @@ -76,7 +76,7 @@ class Create extends Action throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); } } - + /** * @var Document $bucket From df8fb23e75182fc20b6880b97311c634dd638390 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:13:21 +0000 Subject: [PATCH 20/35] Fix test assertion for file count with offset Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com> --- tests/e2e/Services/Storage/StorageBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 2012be7375..a89cd711cb 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -448,7 +448,7 @@ trait StorageBase ], ]); $this->assertEquals(200, $files['headers']['status-code']); - $this->assertEquals(0, count($files['body']['files'])); + $this->assertEquals(1, count($files['body']['files'])); $files = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([ 'content-type' => 'application/json', From 2d332264c67f5fc8bd8bddd3213fc79c6333f2c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 06:16:41 +0000 Subject: [PATCH 21/35] Replace with simple webp image for testing Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com> --- tests/resources/logo-after.webp | Bin 6236 -> 634 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/resources/logo-after.webp b/tests/resources/logo-after.webp index b91c5dc1fa912333d58fba3f42bbce0f8559ae73..4a8791784a819b23d95ba2eef1de7fde040e3c9c 100644 GIT binary patch literal 634 zcmV-=0)_ojNk&F;0ssJ4MM6+kP&goF0ssJTAOM{KDv$wW06tM7k3}P*As3mv05}B% zv$t@!kcR;P{Xpv!@<7$3@yGRF!D+xBg?^|10Q?>O0N`ix0X_lbXZ!=i8X!NQdFB3@ z`~&#M_RsE}B>&XCP&$A=RZ5Gds?ur|MJru3R+CU6nxFeN>6BHzR~aLP!2Jh-Oj)OS zUTc+*>VRhxh#-3HF#M%evvD53)-#T$_?XU21=)c35Rr>47o8+2svO*!BE4hGj)15&1k5K zJFntgTznMm4pBQ{{K^81fgBPI$P`H;&vzh z{w6)I_+NkGcJ{-!^<|Uc5C5?1{_M?}{RrUj*_4jC`&}ex`zNVj9-;_Xa7F+NT;e^Wjb`{XDmxYC z%hCkeMpiZ<3^%-y|B&b74a!C5iC1PFI4iiOYUYiA_M0~o^3%=#7Z&_J(fWPJe>iJb UFbRwZDTesrE^7?y000000F0V5`2YX_ literal 6236 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 G0000WrY4pE From 8a9d0a3f337f3230a33d16259186d48eb43dc0e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 19:30:21 +0000 Subject: [PATCH 22/35] Rename logo-after.webp to image.webp Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com> --- tests/e2e/Services/Storage/StorageBase.php | 4 ++-- tests/resources/{logo-after.webp => image.webp} | Bin 2 files changed, 2 insertions(+), 2 deletions(-) rename tests/resources/{logo-after.webp => image.webp} (100%) diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index a89cd711cb..c67cfcc99a 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -271,7 +271,7 @@ trait StorageBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'fileId' => ID::unique(), - 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo-after.webp'), 'image/webp', 'logo-after.webp'), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/image.webp'), 'image/webp', 'image.webp'), 'permissions' => [ Permission::read(Role::any()), Permission::update(Role::any()), @@ -280,7 +280,7 @@ trait StorageBase ]); $this->assertEquals(201, $webpFile['headers']['status-code']); $this->assertNotEmpty($webpFile['body']['$id']); - $this->assertEquals('logo-after.webp', $webpFile['body']['name']); + $this->assertEquals('image.webp', $webpFile['body']['name']); $this->assertEquals('image/webp', $webpFile['body']['mimeType']); $webpFileId = $webpFile['body']['$id']; diff --git a/tests/resources/logo-after.webp b/tests/resources/image.webp similarity index 100% rename from tests/resources/logo-after.webp rename to tests/resources/image.webp From eae26d90af72481a5137c5bbe537192ef250c1cc Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 12 Nov 2025 09:25:35 +0545 Subject: [PATCH 23/35] Feat: enable Flutter 3.35 for sites --- app/config/template-runtimes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/template-runtimes.php b/app/config/template-runtimes.php index d1bb1a5b6a..1ad125d3d3 100644 --- a/app/config/template-runtimes.php +++ b/app/config/template-runtimes.php @@ -38,6 +38,6 @@ return [ ], 'FLUTTER' => [ 'name' => 'flutter', - 'versions' => ['3.32', '3.24'] + 'versions' => ['3.32', '3.24', '3.35'] ], ]; From 279682941a665e6500a26f5a3baa56cfd77e7cc2 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 12 Nov 2025 09:31:04 +0545 Subject: [PATCH 24/35] enable dart --- app/config/template-runtimes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/template-runtimes.php b/app/config/template-runtimes.php index 1ad125d3d3..2b2882cf10 100644 --- a/app/config/template-runtimes.php +++ b/app/config/template-runtimes.php @@ -14,7 +14,7 @@ return [ ], 'DART' => [ 'name' => 'dart', - 'versions' => ['3.8', '3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16'] + 'versions' => ['3.9', '3.8', '3.5', '3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16'] ], 'GO' => [ 'name' => 'go', From cfd62cf0a1cedd66a20fc7c1ee441905f8443756 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 12 Nov 2025 09:32:12 +0545 Subject: [PATCH 25/35] Update template-runtimes.php --- app/config/template-runtimes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/template-runtimes.php b/app/config/template-runtimes.php index 2b2882cf10..04eaba2c44 100644 --- a/app/config/template-runtimes.php +++ b/app/config/template-runtimes.php @@ -38,6 +38,6 @@ return [ ], 'FLUTTER' => [ 'name' => 'flutter', - 'versions' => ['3.32', '3.24', '3.35'] + 'versions' => ['3.35', '3.32', '3.24'] ], ]; From aacaadcce0b71791dfb9b0a1c8ac0dd0820e7d9e Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 12 Nov 2025 12:17:15 +0545 Subject: [PATCH 26/35] Fix: add default fallback to Flutter framework --- app/config/frameworks.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/config/frameworks.php b/app/config/frameworks.php index 0ab4a8a7db..022db67efb 100644 --- a/app/config/frameworks.php +++ b/app/config/frameworks.php @@ -282,6 +282,7 @@ return [ 'installCommand' => 'flutter pub get', 'outputDirectory' => './build/web', 'startCommand' => 'bash helpers/server.sh', + 'fallbackFile' => 'index.html' ], ], ], From 79d733486c9af746de131ae32fac415f2659c904 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 12 Nov 2025 12:35:31 +0545 Subject: [PATCH 27/35] Update Flutter framework default build runtime version to 3.35 --- app/config/frameworks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/frameworks.php b/app/config/frameworks.php index 022db67efb..47e26ac91e 100644 --- a/app/config/frameworks.php +++ b/app/config/frameworks.php @@ -273,7 +273,7 @@ return [ 'key' => 'flutter', 'name' => 'Flutter', 'screenshotSleep' => 5000, - 'buildRuntime' => 'flutter-3.29', + 'buildRuntime' => 'flutter-3.35', 'runtimes' => getVersions($templateRuntimes['FLUTTER']['versions'], 'flutter'), 'adapters' => [ 'static' => [ From d90b4c2f1930e42855ca9c6e2e2df9eefaa541dc Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 12 Nov 2025 12:43:34 +0545 Subject: [PATCH 28/35] update framework config --- app/config/templates/site.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/templates/site.php b/app/config/templates/site.php index f2396b66db..c8bb019123 100644 --- a/app/config/templates/site.php +++ b/app/config/templates/site.php @@ -84,7 +84,7 @@ const TEMPLATE_FRAMEWORKS = [ 'installCommand' => '', 'buildCommand' => 'flutter build web', 'outputDirectory' => './build/web', - 'buildRuntime' => 'flutter-3.29', + 'buildRuntime' => 'flutter-3.35', 'adapter' => 'static', 'fallbackFile' => '', ], From 08c9db7c60289a33f51c49351edcb85a70f346c8 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Wed, 12 Nov 2025 19:23:57 +0530 Subject: [PATCH 29/35] use inline validator --- .../Tokens/Http/Tokens/Buckets/Files/Create.php | 12 ++---------- .../Platform/Modules/Tokens/Http/Tokens/Update.php | 11 ++--------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php index aa06753420..e4de4c1380 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php @@ -17,7 +17,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Text; +use Utopia\Validator\Nullable; class Create extends Action { @@ -61,7 +61,7 @@ class Create extends Action )) ->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).') ->param('fileId', '', new UID(), 'File unique ID.') - ->param('expire', null, new Text(100), 'Token expiry date', true) + ->param('expire', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Token expiry date', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -70,14 +70,6 @@ class Create extends Action public function action(string $bucketId, string $fileId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents): void { - if (!is_null($expire)) { - $validator = new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_DAYS, offset: 0); - if (!$validator->isValid($expire)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); - } - } - - /** * @var Document $bucket * @var Document $file diff --git a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php index b1b670532d..fef2c38a81 100644 --- a/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php +++ b/src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php @@ -14,7 +14,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; -use Utopia\Validator\Text; +use Utopia\Validator\Nullable; class Update extends Action { @@ -57,7 +57,7 @@ class Update extends Action contentType: ContentType::JSON )) ->param('tokenId', '', new UID(), 'Token unique ID.') - ->param('expire', null, new Text(100), 'File token expiry date', true) + ->param('expire', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'File token expiry date', true) ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') @@ -66,13 +66,6 @@ class Update extends Action public function action(string $tokenId, ?string $expire, Response $response, Database $dbForProject, Event $queueForEvents) { - if (!is_null($expire)) { - $validator = new DatetimeValidator(requireDateInFuture: true, precision: DateTimeValidator::PRECISION_DAYS, offset: 0); - if (!$validator->isValid($expire)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Token expiry date must be a valid date, and at least 1 day from now'); - } - } - $token = $dbForProject->getDocument('resourceTokens', $tokenId); if ($token->isEmpty()) { From bc19e667759720ea604d49ebac5b078f750531a4 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Wed, 12 Nov 2025 20:27:42 +0530 Subject: [PATCH 30/35] fix tests --- tests/e2e/Services/Tokens/TokensConsoleClientTest.php | 4 ++-- tests/e2e/Services/Tokens/TokensCustomServerTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index ab3790abca..f1480faba0 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -70,7 +70,7 @@ class TokensConsoleClientTest extends Scope 'expire' => '2022-11-02', ]); $this->assertEquals(400, $token['headers']['status-code']); - $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']); // Success case: No expire date $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ @@ -128,7 +128,7 @@ class TokensConsoleClientTest extends Scope 'expire' => '2022-11-02', ]); $this->assertEquals(400, $token['headers']['status-code']); - $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']); // Finite expiry $expiry = date('Y-m-d', strtotime("tomorrow")); diff --git a/tests/e2e/Services/Tokens/TokensCustomServerTest.php b/tests/e2e/Services/Tokens/TokensCustomServerTest.php index eaf30049f9..779d5449b3 100644 --- a/tests/e2e/Services/Tokens/TokensCustomServerTest.php +++ b/tests/e2e/Services/Tokens/TokensCustomServerTest.php @@ -68,7 +68,7 @@ class TokensCustomServerTest extends Scope 'expire' => '2022-11-02', ]); $this->assertEquals(400, $token['headers']['status-code']); - $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']); // Success case: No expire date $token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ @@ -102,7 +102,7 @@ class TokensCustomServerTest extends Scope 'expire' => '2022-11-02', ]); $this->assertEquals(400, $token['headers']['status-code']); - $this->assertEquals('Token expiry date must be a valid date, and at least 1 day from now', $token['body']['message']); + $this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']); // Success case: Finite expiry $expiry = date('Y-m-d', strtotime("tomorrow")); From d411b741b32c810b02999ffc7a3c60833c49ab9f Mon Sep 17 00:00:00 2001 From: Atharva Deosthale Date: Wed, 12 Nov 2025 23:06:18 +0530 Subject: [PATCH 31/35] address comments --- .../Modules/Functions/Http/Deployments/Template/Create.php | 3 ++- .../Modules/Sites/Http/Deployments/Template/Create.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 5e0266a09e..2f150dbe18 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -103,7 +103,8 @@ class Create extends Base throw new Exception(Exception::FUNCTION_NOT_FOUND); } - $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$reference" : ""; + $branchUrl = "https://github.com/$owner/$repository/blob/$reference"; + $repositoryUrl = "https://github.com/$owner/$repository"; $template = new Document([ diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index d7196eb3d5..d55d09584d 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -105,7 +105,7 @@ class Create extends Base throw new Exception(Exception::SITE_NOT_FOUND); } - $branchUrl = $type == GitHub::CLONE_TYPE_BRANCH ? "https://github.com/$owner/$repository/tree/$reference" : ""; + $branchUrl = "https://github.com/$owner/$repository/blob/$reference"; $repositoryUrl = "https://github.com/$owner/$repository"; $template = new Document([ From 3a6d907b4207cf85ad98709857a893f24d0b15c3 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 13 Nov 2025 11:29:38 +0530 Subject: [PATCH 32/35] release flutter + dart and add screenshot api docs --- app/config/specs/open-api3-1.8.x-console.json | 16 +++-- app/config/specs/open-api3-1.8.x-server.json | 16 +++-- .../specs/open-api3-latest-console.json | 16 +++-- app/config/specs/open-api3-latest-server.json | 16 +++-- app/config/specs/swagger2-1.8.x-console.json | 16 +++-- app/config/specs/swagger2-1.8.x-server.json | 16 +++-- app/config/specs/swagger2-latest-console.json | 16 +++-- app/config/specs/swagger2-latest-server.json | 16 +++-- composer.lock | 36 +++++----- .../java/avatars/get-screenshot.md | 41 ++++++++++++ .../kotlin/avatars/get-screenshot.md | 32 +++++++++ .../examples/avatars/get-screenshot.md | 32 +++++++++ .../examples/avatars/get-screenshot.md | 65 +++++++++++++++++++ .../examples/avatars/get-screenshot.md | 0 .../examples/avatars/get-screenshot.md | 32 +++++++++ .../examples/avatars/get-screenshot.md | 6 ++ .../examples/avatars/get-screenshot.md | 32 +++++++++ .../examples/avatars/get-screenshot.md | 32 +++++++++ .../examples/avatars/get-screenshot.md | 31 +++++++++ .../examples/avatars/get-screenshot.md | 34 ++++++++++ .../examples/avatars/get-screenshot.md | 38 +++++++++++ .../examples/avatars/get-screenshot.md | 0 .../java/avatars/get-screenshot.md | 42 ++++++++++++ .../kotlin/avatars/get-screenshot.md | 33 ++++++++++ .../examples/avatars/get-screenshot.md | 31 +++++++++ .../examples/avatars/get-screenshot.md | 32 +++++++++ .../examples/avatars/get-screenshot.md | 7 ++ .../examples/avatars/get-screenshot.md | 33 ++++++++++ .../examples/avatars/get-screenshot.md | 33 ++++++++++ docs/sdks/dart/CHANGELOG.md | 5 ++ docs/sdks/flutter/CHANGELOG.md | 4 ++ 31 files changed, 709 insertions(+), 50 deletions(-) create mode 100644 docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/client-android/kotlin/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/client-apple/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/client-flutter/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/client-graphql/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/client-react-native/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/client-rest/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/client-web/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/console-web/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-dart/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-dotnet/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-go/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-graphql/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-kotlin/kotlin/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-nodejs/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-python/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-rest/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-ruby/examples/avatars/get-screenshot.md create mode 100644 docs/examples/1.8.x/server-swift/examples/avatars/get-screenshot.md diff --git a/app/config/specs/open-api3-1.8.x-console.json b/app/config/specs/open-api3-1.8.x-console.json index aa1e81dbeb..287c33e5ec 100644 --- a/app/config/specs/open-api3-1.8.x-console.json +++ b/app/config/specs/open-api3-1.8.x-console.json @@ -13105,6 +13105,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -13131,7 +13132,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -13746,6 +13748,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -13772,7 +13775,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -30954,6 +30958,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -30980,7 +30985,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -31601,6 +31607,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -31627,7 +31634,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] diff --git a/app/config/specs/open-api3-1.8.x-server.json b/app/config/specs/open-api3-1.8.x-server.json index c3df6ef373..ff4af79c91 100644 --- a/app/config/specs/open-api3-1.8.x-server.json +++ b/app/config/specs/open-api3-1.8.x-server.json @@ -12127,6 +12127,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -12153,7 +12154,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -12529,6 +12531,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -12555,7 +12558,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -21703,6 +21707,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -21729,7 +21734,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -22122,6 +22128,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -22148,7 +22155,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index aa1e81dbeb..287c33e5ec 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -13105,6 +13105,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -13131,7 +13132,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -13746,6 +13748,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -13772,7 +13775,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -30954,6 +30958,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -30980,7 +30985,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -31601,6 +31607,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -31627,7 +31634,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index c3df6ef373..ff4af79c91 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -12127,6 +12127,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -12153,7 +12154,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -12529,6 +12531,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -12555,7 +12558,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -21703,6 +21707,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -21729,7 +21734,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -22122,6 +22128,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -22148,7 +22155,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] diff --git a/app/config/specs/swagger2-1.8.x-console.json b/app/config/specs/swagger2-1.8.x-console.json index 158b308f87..4562120363 100644 --- a/app/config/specs/swagger2-1.8.x-console.json +++ b/app/config/specs/swagger2-1.8.x-console.json @@ -13041,6 +13041,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -13067,7 +13068,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -13683,6 +13685,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -13709,7 +13712,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -31058,6 +31062,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -31084,7 +31089,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -31708,6 +31714,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -31734,7 +31741,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] diff --git a/app/config/specs/swagger2-1.8.x-server.json b/app/config/specs/swagger2-1.8.x-server.json index 8b972be590..9603ed313f 100644 --- a/app/config/specs/swagger2-1.8.x-server.json +++ b/app/config/specs/swagger2-1.8.x-server.json @@ -12078,6 +12078,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -12104,7 +12105,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -12493,6 +12495,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -12519,7 +12522,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -21845,6 +21849,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -21871,7 +21876,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -22277,6 +22283,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -22303,7 +22310,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 158b308f87..4562120363 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -13041,6 +13041,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -13067,7 +13068,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -13683,6 +13685,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -13709,7 +13712,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -31058,6 +31062,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -31084,7 +31089,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -31708,6 +31714,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -31734,7 +31741,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index 8b972be590..9603ed313f 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -12078,6 +12078,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -12104,7 +12105,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -12493,6 +12495,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -12519,7 +12522,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -21845,6 +21849,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -21871,7 +21876,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] @@ -22277,6 +22283,7 @@ "dart-3.3", "dart-3.5", "dart-3.8", + "dart-3.9", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", @@ -22303,7 +22310,8 @@ "flutter-3.24", "flutter-3.27", "flutter-3.29", - "flutter-3.32" + "flutter-3.32", + "flutter-3.35" ], "x-enum-name": null, "x-enum-keys": [] diff --git a/composer.lock b/composer.lock index 753162cc89..77216d6346 100644 --- a/composer.lock +++ b/composer.lock @@ -756,16 +756,16 @@ }, { "name": "google/protobuf", - "version": "v4.33.0", + "version": "v4.33.1", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "b50269e23204e5ae859a326ec3d90f09efe3047d" + "reference": "0cd73ccf0cd26c3e72299cce1ea6144091a57e12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/b50269e23204e5ae859a326ec3d90f09efe3047d", - "reference": "b50269e23204e5ae859a326ec3d90f09efe3047d", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/0cd73ccf0cd26c3e72299cce1ea6144091a57e12", + "reference": "0cd73ccf0cd26c3e72299cce1ea6144091a57e12", "shasum": "" }, "require": { @@ -794,9 +794,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.0" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.1" }, - "time": "2025-10-15T20:10:28+00:00" + "time": "2025-11-12T21:58:05+00:00" }, { "name": "league/csv", @@ -3844,16 +3844,16 @@ }, { "name": "utopia-php/database", - "version": "3.2.0", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "f2d01b6b38057891184f62107bf70a55bc2ea068" + "reference": "8062cfc42ce93c0b28c325fa632f6392e5d6c0d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/f2d01b6b38057891184f62107bf70a55bc2ea068", - "reference": "f2d01b6b38057891184f62107bf70a55bc2ea068", + "url": "https://api.github.com/repos/utopia-php/database/zipball/8062cfc42ce93c0b28c325fa632f6392e5d6c0d2", + "reference": "8062cfc42ce93c0b28c325fa632f6392e5d6c0d2", "shasum": "" }, "require": { @@ -3896,9 +3896,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/3.2.0" + "source": "https://github.com/utopia-php/database/tree/3.3.0" }, - "time": "2025-11-06T05:41:54+00:00" + "time": "2025-11-12T08:20:59+00:00" }, { "name": "utopia-php/detector", @@ -5383,16 +5383,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.5.3", + "version": "1.5.4", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "1a7a3b89147aa8c1bde5247f8eeb7e4832c6016d" + "reference": "958947b6483a79e11c3812f23bb3056199fa4105" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/1a7a3b89147aa8c1bde5247f8eeb7e4832c6016d", - "reference": "1a7a3b89147aa8c1bde5247f8eeb7e4832c6016d", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/958947b6483a79e11c3812f23bb3056199fa4105", + "reference": "958947b6483a79e11c3812f23bb3056199fa4105", "shasum": "" }, "require": { @@ -5428,9 +5428,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/1.5.3" + "source": "https://github.com/appwrite/sdk-generator/tree/1.5.4" }, - "time": "2025-11-10T09:50:41+00:00" + "time": "2025-11-12T12:43:42+00:00" }, { "name": "doctrine/annotations", diff --git a/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md b/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md new file mode 100644 index 0000000000..077716f523 --- /dev/null +++ b/docs/examples/1.8.x/client-android/java/avatars/get-screenshot.md @@ -0,0 +1,41 @@ +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Avatars; + +Client client = new Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject(""); // Your project ID + +Avatars avatars = new Avatars(client); + +avatars.getScreenshot( + "https://example.com", // url + mapOf( "a" to "b" ), // headers (optional) + 1, // viewportWidth (optional) + 1, // viewportHeight (optional) + 0.1, // scale (optional) + theme.LIGHT, // theme (optional) + "", // userAgent (optional) + false, // fullpage (optional) + "", // locale (optional) + timezone.AFRICA_ABIDJAN, // timezone (optional) + -90, // latitude (optional) + -180, // longitude (optional) + 0, // accuracy (optional) + false, // touch (optional) + listOf(), // permissions (optional) + 0, // sleep (optional) + 0, // width (optional) + 0, // height (optional) + -1, // quality (optional) + output.JPG, // output (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + Log.d("Appwrite", result.toString()); + }) +); + diff --git a/docs/examples/1.8.x/client-android/kotlin/avatars/get-screenshot.md b/docs/examples/1.8.x/client-android/kotlin/avatars/get-screenshot.md new file mode 100644 index 0000000000..014ca90fd8 --- /dev/null +++ b/docs/examples/1.8.x/client-android/kotlin/avatars/get-screenshot.md @@ -0,0 +1,32 @@ +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Avatars + +val client = Client(context) + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +val avatars = Avatars(client) + +val result = avatars.getScreenshot( + url = "https://example.com", + headers = mapOf( "a" to "b" ), // (optional) + viewportWidth = 1, // (optional) + viewportHeight = 1, // (optional) + scale = 0.1, // (optional) + theme = theme.LIGHT, // (optional) + userAgent = "", // (optional) + fullpage = false, // (optional) + locale = "", // (optional) + timezone = timezone.AFRICA_ABIDJAN, // (optional) + latitude = -90, // (optional) + longitude = -180, // (optional) + accuracy = 0, // (optional) + touch = false, // (optional) + permissions = listOf(), // (optional) + sleep = 0, // (optional) + width = 0, // (optional) + height = 0, // (optional) + quality = -1, // (optional) + output = output.JPG, // (optional) +) \ No newline at end of file diff --git a/docs/examples/1.8.x/client-apple/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-apple/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..7f4ef5da5c --- /dev/null +++ b/docs/examples/1.8.x/client-apple/examples/avatars/get-screenshot.md @@ -0,0 +1,32 @@ +import Appwrite +import AppwriteEnums + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + +let avatars = Avatars(client) + +let bytes = try await avatars.getScreenshot( + url: "https://example.com", + headers: [:], // optional + viewportWidth: 1, // optional + viewportHeight: 1, // optional + scale: 0.1, // optional + theme: .light, // optional + userAgent: "", // optional + fullpage: false, // optional + locale: "", // optional + timezone: .africaAbidjan, // optional + latitude: -90, // optional + longitude: -180, // optional + accuracy: 0, // optional + touch: false, // optional + permissions: [], // optional + sleep: 0, // optional + width: 0, // optional + height: 0, // optional + quality: -1, // optional + output: .jpg // optional +) + diff --git a/docs/examples/1.8.x/client-flutter/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-flutter/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..768cb8f271 --- /dev/null +++ b/docs/examples/1.8.x/client-flutter/examples/avatars/get-screenshot.md @@ -0,0 +1,65 @@ +import 'package:appwrite/appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +Avatars avatars = Avatars(client); + +// Downloading file +UInt8List bytes = await avatars.getScreenshot( + url: 'https://example.com', + headers: {}, // optional + viewportWidth: 1, // optional + viewportHeight: 1, // optional + scale: 0.1, // optional + theme: .light, // optional + userAgent: '', // optional + fullpage: false, // optional + locale: '', // optional + timezone: .africaAbidjan, // optional + latitude: -90, // optional + longitude: -180, // optional + accuracy: 0, // optional + touch: false, // optional + permissions: [], // optional + sleep: 0, // optional + width: 0, // optional + height: 0, // optional + quality: -1, // optional + output: .jpg, // optional +) + +final file = File('path_to_file/filename.ext'); +file.writeAsBytesSync(bytes); + +// Displaying image preview +FutureBuilder( + future: avatars.getScreenshot( + url:'https://example.com' , + headers:{} , // optional + viewportWidth:1 , // optional + viewportHeight:1 , // optional + scale:0.1 , // optional + theme: .light, // optional + userAgent:'' , // optional + fullpage:false , // optional + locale:'' , // optional + timezone: .africaAbidjan, // optional + latitude:-90 , // optional + longitude:-180 , // optional + accuracy:0 , // optional + touch:false , // optional + permissions:[] , // optional + sleep:0 , // optional + width:0 , // optional + height:0 , // optional + quality:-1 , // optional + output: .jpg, // optional +), // Works for both public file and private file, for private files you need to be logged in + builder: (context, snapshot) { + return snapshot.hasData && snapshot.data != null + ? Image.memory(snapshot.data) + : CircularProgressIndicator(); + } +); diff --git a/docs/examples/1.8.x/client-graphql/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-graphql/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/1.8.x/client-react-native/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-react-native/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..7482b4cf0e --- /dev/null +++ b/docs/examples/1.8.x/client-react-native/examples/avatars/get-screenshot.md @@ -0,0 +1,32 @@ +import { Client, Avatars, , , } from "react-native-appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const avatars = new Avatars(client); + +const result = avatars.getScreenshot({ + url: 'https://example.com', + headers: {}, // optional + viewportWidth: 1, // optional + viewportHeight: 1, // optional + scale: 0.1, // optional + theme: .Light, // optional + userAgent: '', // optional + fullpage: false, // optional + locale: '', // optional + timezone: .AfricaAbidjan, // optional + latitude: -90, // optional + longitude: -180, // optional + accuracy: 0, // optional + touch: false, // optional + permissions: [], // optional + sleep: 0, // optional + width: 0, // optional + height: 0, // optional + quality: -1, // optional + output: .Jpg // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/client-rest/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-rest/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..b4c31ca100 --- /dev/null +++ b/docs/examples/1.8.x/client-rest/examples/avatars/get-screenshot.md @@ -0,0 +1,6 @@ +GET /v1/avatars/screenshots HTTP/1.1 +Host: cloud.appwrite.io +X-Appwrite-Response-Format: 1.8.0 +X-Appwrite-Project: +X-Appwrite-Session: +X-Appwrite-JWT: diff --git a/docs/examples/1.8.x/client-web/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/client-web/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..c4722be633 --- /dev/null +++ b/docs/examples/1.8.x/client-web/examples/avatars/get-screenshot.md @@ -0,0 +1,32 @@ +import { Client, Avatars, , , } from "appwrite"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const avatars = new Avatars(client); + +const result = avatars.getScreenshot({ + url: 'https://example.com', + headers: {}, // optional + viewportWidth: 1, // optional + viewportHeight: 1, // optional + scale: 0.1, // optional + theme: .Light, // optional + userAgent: '', // optional + fullpage: false, // optional + locale: '', // optional + timezone: .AfricaAbidjan, // optional + latitude: -90, // optional + longitude: -180, // optional + accuracy: 0, // optional + touch: false, // optional + permissions: [], // optional + sleep: 0, // optional + width: 0, // optional + height: 0, // optional + quality: -1, // optional + output: .Jpg // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/console-web/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/console-web/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..3a9437515d --- /dev/null +++ b/docs/examples/1.8.x/console-web/examples/avatars/get-screenshot.md @@ -0,0 +1,32 @@ +import { Client, Avatars, , , } from "@appwrite.io/console"; + +const client = new Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject(''); // Your project ID + +const avatars = new Avatars(client); + +const result = avatars.getScreenshot({ + url: 'https://example.com', + headers: {}, // optional + viewportWidth: 1, // optional + viewportHeight: 1, // optional + scale: 0.1, // optional + theme: .Light, // optional + userAgent: '', // optional + fullpage: false, // optional + locale: '', // optional + timezone: .AfricaAbidjan, // optional + latitude: -90, // optional + longitude: -180, // optional + accuracy: 0, // optional + touch: false, // optional + permissions: [], // optional + sleep: 0, // optional + width: 0, // optional + height: 0, // optional + quality: -1, // optional + output: .Jpg // optional +}); + +console.log(result); diff --git a/docs/examples/1.8.x/server-dart/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-dart/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..7630648f98 --- /dev/null +++ b/docs/examples/1.8.x/server-dart/examples/avatars/get-screenshot.md @@ -0,0 +1,31 @@ +import 'package:dart_appwrite/dart_appwrite.dart'; + +Client client = Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setSession(''); // The user session to authenticate with + +Avatars avatars = Avatars(client); + +UInt8List result = await avatars.getScreenshot( + url: 'https://example.com', + headers: {}, // (optional) + viewportWidth: 1, // (optional) + viewportHeight: 1, // (optional) + scale: 0.1, // (optional) + theme: .light, // (optional) + userAgent: '', // (optional) + fullpage: false, // (optional) + locale: '', // (optional) + timezone: .africaAbidjan, // (optional) + latitude: -90, // (optional) + longitude: -180, // (optional) + accuracy: 0, // (optional) + touch: false, // (optional) + permissions: [], // (optional) + sleep: 0, // (optional) + width: 0, // (optional) + height: 0, // (optional) + quality: -1, // (optional) + output: .jpg, // (optional) +); diff --git a/docs/examples/1.8.x/server-dotnet/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-dotnet/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..f5c3542a97 --- /dev/null +++ b/docs/examples/1.8.x/server-dotnet/examples/avatars/get-screenshot.md @@ -0,0 +1,34 @@ +using Appwrite; +using Appwrite.Enums; +using Appwrite.Models; +using Appwrite.Services; + +Client client = new Client() + .SetEndPoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .SetProject("") // Your project ID + .SetSession(""); // The user session to authenticate with + +Avatars avatars = new Avatars(client); + +byte[] result = await avatars.GetScreenshot( + url: "https://example.com", + headers: [object], // optional + viewportWidth: 1, // optional + viewportHeight: 1, // optional + scale: 0.1, // optional + theme: .Light, // optional + userAgent: "", // optional + fullpage: false, // optional + locale: "", // optional + timezone: .AfricaAbidjan, // optional + latitude: -90, // optional + longitude: -180, // optional + accuracy: 0, // optional + touch: false, // optional + permissions: new List(), // optional + sleep: 0, // optional + width: 0, // optional + height: 0, // optional + quality: -1, // optional + output: .Jpg // optional +); \ No newline at end of file diff --git a/docs/examples/1.8.x/server-go/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-go/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..ac425fbc4f --- /dev/null +++ b/docs/examples/1.8.x/server-go/examples/avatars/get-screenshot.md @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "github.com/appwrite/sdk-for-go/client" + "github.com/appwrite/sdk-for-go/avatars" +) + +client := client.New( + client.WithEndpoint("https://.cloud.appwrite.io/v1") + client.WithProject("") + client.WithSession("") +) + +service := avatars.New(client) + +response, error := service.GetScreenshot( + "https://example.com", + avatars.WithGetScreenshotHeaders(map[string]interface{}{}), + avatars.WithGetScreenshotViewportWidth(1), + avatars.WithGetScreenshotViewportHeight(1), + avatars.WithGetScreenshotScale(0.1), + avatars.WithGetScreenshotTheme("light"), + avatars.WithGetScreenshotUserAgent(""), + avatars.WithGetScreenshotFullpage(false), + avatars.WithGetScreenshotLocale(""), + avatars.WithGetScreenshotTimezone("africa/abidjan"), + avatars.WithGetScreenshotLatitude(-90), + avatars.WithGetScreenshotLongitude(-180), + avatars.WithGetScreenshotAccuracy(0), + avatars.WithGetScreenshotTouch(false), + avatars.WithGetScreenshotPermissions([]interface{}{}), + avatars.WithGetScreenshotSleep(0), + avatars.WithGetScreenshotWidth(0), + avatars.WithGetScreenshotHeight(0), + avatars.WithGetScreenshotQuality(-1), + avatars.WithGetScreenshotOutput("jpg"), +) diff --git a/docs/examples/1.8.x/server-graphql/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-graphql/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md b/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md new file mode 100644 index 0000000000..cf734af3b2 --- /dev/null +++ b/docs/examples/1.8.x/server-kotlin/java/avatars/get-screenshot.md @@ -0,0 +1,42 @@ +import io.appwrite.Client; +import io.appwrite.coroutines.CoroutineCallback; +import io.appwrite.services.Avatars; + +Client client = new Client() + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + .setSession(""); // The user session to authenticate with + +Avatars avatars = new Avatars(client); + +avatars.getScreenshot( + "https://example.com", // url + mapOf( "a" to "b" ), // headers (optional) + 1, // viewportWidth (optional) + 1, // viewportHeight (optional) + 0.1, // scale (optional) + .LIGHT, // theme (optional) + "", // userAgent (optional) + false, // fullpage (optional) + "", // locale (optional) + .AFRICA_ABIDJAN, // timezone (optional) + -90, // latitude (optional) + -180, // longitude (optional) + 0, // accuracy (optional) + false, // touch (optional) + listOf(), // permissions (optional) + 0, // sleep (optional) + 0, // width (optional) + 0, // height (optional) + -1, // quality (optional) + .JPG, // output (optional) + new CoroutineCallback<>((result, error) -> { + if (error != null) { + error.printStackTrace(); + return; + } + + System.out.println(result); + }) +); + diff --git a/docs/examples/1.8.x/server-kotlin/kotlin/avatars/get-screenshot.md b/docs/examples/1.8.x/server-kotlin/kotlin/avatars/get-screenshot.md new file mode 100644 index 0000000000..96032bb8a4 --- /dev/null +++ b/docs/examples/1.8.x/server-kotlin/kotlin/avatars/get-screenshot.md @@ -0,0 +1,33 @@ +import io.appwrite.Client +import io.appwrite.coroutines.CoroutineCallback +import io.appwrite.services.Avatars + +val client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + .setSession("") // The user session to authenticate with + +val avatars = Avatars(client) + +val result = avatars.getScreenshot( + url = "https://example.com", + headers = mapOf( "a" to "b" ), // optional + viewportWidth = 1, // optional + viewportHeight = 1, // optional + scale = 0.1, // optional + theme = "light", // optional + userAgent = "", // optional + fullpage = false, // optional + locale = "", // optional + timezone = "africa/abidjan", // optional + latitude = -90, // optional + longitude = -180, // optional + accuracy = 0, // optional + touch = false, // optional + permissions = listOf(), // optional + sleep = 0, // optional + width = 0, // optional + height = 0, // optional + quality = -1, // optional + output = "jpg" // optional +) diff --git a/docs/examples/1.8.x/server-nodejs/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-nodejs/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..5f7b40cece --- /dev/null +++ b/docs/examples/1.8.x/server-nodejs/examples/avatars/get-screenshot.md @@ -0,0 +1,31 @@ +const sdk = require('node-appwrite'); + +const client = new sdk.Client() + .setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + .setProject('') // Your project ID + .setSession(''); // The user session to authenticate with + +const avatars = new sdk.Avatars(client); + +const result = await avatars.getScreenshot({ + url: 'https://example.com', + headers: {}, // optional + viewportWidth: 1, // optional + viewportHeight: 1, // optional + scale: 0.1, // optional + theme: sdk..Light, // optional + userAgent: '', // optional + fullpage: false, // optional + locale: '', // optional + timezone: sdk..AfricaAbidjan, // optional + latitude: -90, // optional + longitude: -180, // optional + accuracy: 0, // optional + touch: false, // optional + permissions: [], // optional + sleep: 0, // optional + width: 0, // optional + height: 0, // optional + quality: -1, // optional + output: sdk..Jpg // optional +}); diff --git a/docs/examples/1.8.x/server-python/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-python/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..34bdf8ac7a --- /dev/null +++ b/docs/examples/1.8.x/server-python/examples/avatars/get-screenshot.md @@ -0,0 +1,32 @@ +from appwrite.client import Client +from appwrite.services.avatars import Avatars + +client = Client() +client.set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint +client.set_project('') # Your project ID +client.set_session('') # The user session to authenticate with + +avatars = Avatars(client) + +result = avatars.get_screenshot( + url = 'https://example.com', + headers = {}, # optional + viewport_width = 1, # optional + viewport_height = 1, # optional + scale = 0.1, # optional + theme = .LIGHT, # optional + user_agent = '', # optional + fullpage = False, # optional + locale = '', # optional + timezone = .AFRICA_ABIDJAN, # optional + latitude = -90, # optional + longitude = -180, # optional + accuracy = 0, # optional + touch = False, # optional + permissions = [], # optional + sleep = 0, # optional + width = 0, # optional + height = 0, # optional + quality = -1, # optional + output = .JPG # optional +) diff --git a/docs/examples/1.8.x/server-rest/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-rest/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..0ab16b59e6 --- /dev/null +++ b/docs/examples/1.8.x/server-rest/examples/avatars/get-screenshot.md @@ -0,0 +1,7 @@ +GET /v1/avatars/screenshots HTTP/1.1 +Host: cloud.appwrite.io +X-Appwrite-Response-Format: 1.8.0 +X-Appwrite-Project: +X-Appwrite-Session: +X-Appwrite-Key: +X-Appwrite-JWT: diff --git a/docs/examples/1.8.x/server-ruby/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-ruby/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..f2af537fe8 --- /dev/null +++ b/docs/examples/1.8.x/server-ruby/examples/avatars/get-screenshot.md @@ -0,0 +1,33 @@ +require 'appwrite' + +include Appwrite + +client = Client.new + .set_endpoint('https://.cloud.appwrite.io/v1') # Your API Endpoint + .set_project('') # Your project ID + .set_session('') # The user session to authenticate with + +avatars = Avatars.new(client) + +result = avatars.get_screenshot( + url: 'https://example.com', + headers: {}, # optional + viewport_width: 1, # optional + viewport_height: 1, # optional + scale: 0.1, # optional + theme: ::LIGHT, # optional + user_agent: '', # optional + fullpage: false, # optional + locale: '', # optional + timezone: ::AFRICA_ABIDJAN, # optional + latitude: -90, # optional + longitude: -180, # optional + accuracy: 0, # optional + touch: false, # optional + permissions: [], # optional + sleep: 0, # optional + width: 0, # optional + height: 0, # optional + quality: -1, # optional + output: ::JPG # optional +) diff --git a/docs/examples/1.8.x/server-swift/examples/avatars/get-screenshot.md b/docs/examples/1.8.x/server-swift/examples/avatars/get-screenshot.md new file mode 100644 index 0000000000..3aa1661093 --- /dev/null +++ b/docs/examples/1.8.x/server-swift/examples/avatars/get-screenshot.md @@ -0,0 +1,33 @@ +import Appwrite +import AppwriteEnums + +let client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") // Your API Endpoint + .setProject("") // Your project ID + .setSession("") // The user session to authenticate with + +let avatars = Avatars(client) + +let bytes = try await avatars.getScreenshot( + url: "https://example.com", + headers: [:], // optional + viewportWidth: 1, // optional + viewportHeight: 1, // optional + scale: 0.1, // optional + theme: .light, // optional + userAgent: "", // optional + fullpage: false, // optional + locale: "", // optional + timezone: .africaAbidjan, // optional + latitude: -90, // optional + longitude: -180, // optional + accuracy: 0, // optional + touch: false, // optional + permissions: [], // optional + sleep: 0, // optional + width: 0, // optional + height: 0, // optional + quality: -1, // optional + output: .jpg // optional +) + diff --git a/docs/sdks/dart/CHANGELOG.md b/docs/sdks/dart/CHANGELOG.md index 1a2cd6a5be..fdd7815cfc 100644 --- a/docs/sdks/dart/CHANGELOG.md +++ b/docs/sdks/dart/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 19.4.0 + +* Add `getScreenshot` method to `Avatars` service +* Fix passing of `null` values and stripping only non-nullable optional parameters from the request body + ## 19.3.0 * Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance diff --git a/docs/sdks/flutter/CHANGELOG.md b/docs/sdks/flutter/CHANGELOG.md index 5ab7d3269a..2f26f34edd 100644 --- a/docs/sdks/flutter/CHANGELOG.md +++ b/docs/sdks/flutter/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 20.3.1 + +* Fix passing of `null` values and stripping only non-nullable optional parameters from the request body + ## 20.3.0 * Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance From fcc07533e1a4caecbbfafec7355f263195fdddcb Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 13 Nov 2025 11:34:35 +0530 Subject: [PATCH 33/35] update changelog --- docs/sdks/dart/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sdks/dart/CHANGELOG.md b/docs/sdks/dart/CHANGELOG.md index fdd7815cfc..7fd7227f15 100644 --- a/docs/sdks/dart/CHANGELOG.md +++ b/docs/sdks/dart/CHANGELOG.md @@ -3,6 +3,8 @@ ## 19.4.0 * Add `getScreenshot` method to `Avatars` service +* Add enums `Theme`, `Output` and `Timezone` +* Update runtime enums to add support for `dart39` and `flutter335` runtimes * Fix passing of `null` values and stripping only non-nullable optional parameters from the request body ## 19.3.0 From b2991709dc9ef5c51cb71f27f20fe94f6993c35c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 13 Nov 2025 11:38:50 +0530 Subject: [PATCH 34/35] update version --- app/config/platforms.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/platforms.php b/app/config/platforms.php index 5d72a8914c..2b5c107648 100644 --- a/app/config/platforms.php +++ b/app/config/platforms.php @@ -60,7 +60,7 @@ return [ [ 'key' => 'flutter', 'name' => 'Flutter', - 'version' => '20.3.0', + 'version' => '20.3.1', 'url' => 'https://github.com/appwrite/sdk-for-flutter', 'package' => 'https://pub.dev/packages/appwrite', 'enabled' => true, @@ -376,7 +376,7 @@ return [ [ 'key' => 'dart', 'name' => 'Dart', - 'version' => '19.3.0', + 'version' => '19.4.0', 'url' => 'https://github.com/appwrite/sdk-for-dart', 'package' => 'https://pub.dev/packages/dart_appwrite', 'enabled' => true, From c1b50e99411291fea3c55be43a3942a7d13a44a7 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 13 Nov 2025 06:59:03 +0000 Subject: [PATCH 35/35] Upgrade utopia-php database --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 753162cc89..a91e4b1b03 100644 --- a/composer.lock +++ b/composer.lock @@ -3844,16 +3844,16 @@ }, { "name": "utopia-php/database", - "version": "3.2.0", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "f2d01b6b38057891184f62107bf70a55bc2ea068" + "reference": "e10b4faa4f3a3ef30a5f6d76acdb605469924aec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/f2d01b6b38057891184f62107bf70a55bc2ea068", - "reference": "f2d01b6b38057891184f62107bf70a55bc2ea068", + "url": "https://api.github.com/repos/utopia-php/database/zipball/e10b4faa4f3a3ef30a5f6d76acdb605469924aec", + "reference": "e10b4faa4f3a3ef30a5f6d76acdb605469924aec", "shasum": "" }, "require": { @@ -3896,9 +3896,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/3.2.0" + "source": "https://github.com/utopia-php/database/tree/3.4.0" }, - "time": "2025-11-06T05:41:54+00:00" + "time": "2025-11-13T06:34:20+00:00" }, { "name": "utopia-php/detector",