From 071f964e296da9e01ae706cf985cc171ed585f38 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Sat, 6 Nov 2021 14:30:32 +0100
Subject: [PATCH 001/107] Working on an overhaul for the teams documentation.
---
app/controllers/api/teams.php | 16 ++++++++--------
docs/references/teams/create-team.md | 2 +-
docs/references/teams/delete-team.md | 2 +-
docs/references/teams/list-teams.md | 4 +++-
docs/references/teams/update-team.md | 2 +-
5 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 128a03c2eb..746aeecfb8 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -32,7 +32,7 @@ App::post('/v1/teams')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM)
- ->param('name', null, new Text(128), 'Team name. Max length: 128 chars.')
+ ->param('name', null, new Text(128), 'The name of the team. Max length: 128 chars.')
->param('roles', ['owner'], new ArrayList(new Key()), 'Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.', true)
->inject('response')
->inject('user')
@@ -113,10 +113,10 @@ App::get('/v1/teams')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM_LIST)
- ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
- ->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
+ ->param('search', '', new Text(256), 'Enter any text to search. Max length: 256 chars.', true)
+ ->param('limit', 25, new Range(0, 100), 'Limit how many results will be returned. Returns 25 results by default. Maximum of 100 results allowed per request.', true)
+ ->param('offset', 0, new Range(0, 2000), 'Use this value to manage pagination. The default value is 0.', true)
+ ->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Use ASC to order results in ascending and DESC to order results in descending order.', true)
->inject('response')
->inject('projectDB')
->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) {
@@ -178,8 +178,8 @@ App::put('/v1/teams/:teamId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM)
- ->param('teamId', '', new UID(), 'Team unique ID.')
- ->param('name', null, new Text(128), 'Team name. Max length: 128 chars.')
+ ->param('teamId', '', new UID(), 'The unique team ID.')
+ ->param('name', null, new Text(128), 'The new team name. Max length: 128 chars.')
->inject('response')
->inject('projectDB')
->action(function ($teamId, $name, $response, $projectDB) {
@@ -214,7 +214,7 @@ App::delete('/v1/teams/:teamId')
->label('sdk.description', '/docs/references/teams/delete-team.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'The unique team ID.')
->inject('response')
->inject('projectDB')
->inject('events')
diff --git a/docs/references/teams/create-team.md b/docs/references/teams/create-team.md
index 91bd09e8f9..41a36693e8 100644
--- a/docs/references/teams/create-team.md
+++ b/docs/references/teams/create-team.md
@@ -1 +1 @@
-Create a new team. The user who creates the team will automatically be assigned as the owner of the team. The team owner can invite new members, who will be able add new owners and update or delete the team from your project.
\ No newline at end of file
+Use this endpoint to create a new team. The user who creates the team will automatically be assigned as the owner of the team. Only the users with the owner role can invite new members or add new owners. However, every member can delete or update the team.
\ No newline at end of file
diff --git a/docs/references/teams/delete-team.md b/docs/references/teams/delete-team.md
index 887dfcc06f..16672499ea 100644
--- a/docs/references/teams/delete-team.md
+++ b/docs/references/teams/delete-team.md
@@ -1 +1 @@
-Delete a team by its unique ID. Only team owners have write access for this resource.
\ No newline at end of file
+Delete a team using its unique ID. Every member of the team has access to this endpoint.
\ No newline at end of file
diff --git a/docs/references/teams/list-teams.md b/docs/references/teams/list-teams.md
index 04a3959e4c..feab2d45a4 100644
--- a/docs/references/teams/list-teams.md
+++ b/docs/references/teams/list-teams.md
@@ -1 +1,3 @@
-Get a list of all the current user teams. You can use the query params to filter your results. On admin mode, this endpoint will return a list of all of the project's teams. [Learn more about different API modes](/docs/admin).
\ No newline at end of file
+Get a list of all the teams in which the current user is a member. You can use the parameters to filter your results.
+
+On admin mode, this endpoint returns a list of all the teams in the current project. [Learn more about different API modes](/docs/admin).
\ No newline at end of file
diff --git a/docs/references/teams/update-team.md b/docs/references/teams/update-team.md
index afa84135a2..bc83e897f5 100644
--- a/docs/references/teams/update-team.md
+++ b/docs/references/teams/update-team.md
@@ -1 +1 @@
-Update a team by its unique ID. Only team owners have write access for this resource.
\ No newline at end of file
+Update a team using its unique ID. Every team member has access to this endpoint.
\ No newline at end of file
From 010fca8f18e31e764dfe1e8a54f75e2bdd68e676 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Wed, 10 Nov 2021 13:45:23 +0100
Subject: [PATCH 002/107] Fixed permissions misunderstanding
---
docs/references/teams/create-team.md | 2 +-
docs/references/teams/delete-team.md | 2 +-
docs/references/teams/update-team.md | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/references/teams/create-team.md b/docs/references/teams/create-team.md
index 41a36693e8..3ff3a30c8e 100644
--- a/docs/references/teams/create-team.md
+++ b/docs/references/teams/create-team.md
@@ -1 +1 @@
-Use this endpoint to create a new team. The user who creates the team will automatically be assigned as the owner of the team. Only the users with the owner role can invite new members or add new owners. However, every member can delete or update the team.
\ No newline at end of file
+Use this endpoint to create a new team. The user who creates the team will automatically be assigned as the owner of the team. Only the users with the owner role can invite new members, add new owners and delete or update the team.
\ No newline at end of file
diff --git a/docs/references/teams/delete-team.md b/docs/references/teams/delete-team.md
index 16672499ea..6b73736e9a 100644
--- a/docs/references/teams/delete-team.md
+++ b/docs/references/teams/delete-team.md
@@ -1 +1 @@
-Delete a team using its unique ID. Every member of the team has access to this endpoint.
\ No newline at end of file
+Delete a team using its unique ID. Only members with the owner role can delete the team.
\ No newline at end of file
diff --git a/docs/references/teams/update-team.md b/docs/references/teams/update-team.md
index bc83e897f5..0acb528e1c 100644
--- a/docs/references/teams/update-team.md
+++ b/docs/references/teams/update-team.md
@@ -1 +1 @@
-Update a team using its unique ID. Every team member has access to this endpoint.
\ No newline at end of file
+Update a team using its unique ID. Only members with the owner role can update the team.
\ No newline at end of file
From 5924026bb8e290defa7a602cac2540a54404da0e Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Wed, 10 Nov 2021 16:32:10 +0100
Subject: [PATCH 003/107] Updated create team membership docs
---
app/controllers/api/teams.php | 18 +++++++++---------
.../references/teams/create-team-membership.md | 6 +++---
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 746aeecfb8..e173f5faac 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -150,7 +150,7 @@ App::get('/v1/teams/:teamId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'The unique team ID.')
->inject('response')
->inject('projectDB')
->action(function ($teamId, $response, $projectDB) {
@@ -260,11 +260,11 @@ App::post('/v1/teams/:teamId/memberships')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
->label('abuse-limit', 10)
- ->param('teamId', '', new UID(), 'Team unique ID.')
- ->param('email', '', new Email(), 'New team member email.')
- ->param('roles', [], new ArrayList(new Key()), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
+ ->param('teamId', '', new UID(), 'The unique team ID.')
+ ->param('email', '', new Email(), 'The email address of the new team member.')
+ ->param('roles', [], new ArrayList(new Key()), 'An Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page
- ->param('name', '', new Text(128), 'New team member name. Max length: 128 chars.', true)
+ ->param('name', '', new Text(128), 'The name of the new team member. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('user')
@@ -472,7 +472,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'The unique team ID.')
->param('membershipId', '', new UID(), 'Membership ID.')
->param('roles', [], new ArrayList(new Key()), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
->inject('request')
@@ -534,7 +534,7 @@ App::get('/v1/teams/:teamId/memberships')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP_LIST)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'The unique team ID.')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
@@ -588,7 +588,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'The unique team ID.')
->param('membershipId', '', new UID(), 'Membership ID.')
->param('userId', '', new UID(), 'User unique ID.')
->param('secret', '', new Text(256), 'Secret key.')
@@ -734,7 +734,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.description', '/docs/references/teams/delete-team-membership.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'The unique team ID.')
->param('membershipId', '', new UID(), 'Membership ID.')
->inject('response')
->inject('projectDB')
diff --git a/docs/references/teams/create-team-membership.md b/docs/references/teams/create-team-membership.md
index c6d81de484..f0bf9e416c 100644
--- a/docs/references/teams/create-team-membership.md
+++ b/docs/references/teams/create-team-membership.md
@@ -1,5 +1,5 @@
-Use this endpoint to invite a new member to join your team. If initiated from Client SDK, an email with a link to join the team will be sent to the new member's email address if the member doesn't exist in the project it will be created automatically. If initiated from server side SDKs, new member will automatically be added to the team.
+Use this endpoint to invite a new member to join your team. If initiated from the client SDK, an email with a link to join the team will be sent to the member's email address and an account will be created for them should they not be signed up already. If initiated from server-side SDKs, the new member will automatically be added to the team.
-Use the 'URL' parameter to redirect the user from the invitation email back to your app. When the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team. While calling from side SDKs the redirect url can be empty string.
+Use the 'url' parameter to redirect the user from the invitation email back to your app. When the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team.
-Please note that in order to avoid a [Redirect Attacks](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URL's are the once from domains you have set when added your platforms in the console interface.
\ No newline at end of file
+Please note that to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URL's are the once from domains you have set when adding your platforms in the console interface.
\ No newline at end of file
From 4137bdb09820acfce921eff9ee7f816569cb00fd Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Wed, 10 Nov 2021 16:43:07 +0100
Subject: [PATCH 004/107] Updated update team membership roles docs
---
app/controllers/api/teams.php | 4 ++--
docs/references/teams/update-team-membership-roles.md | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
create mode 100644 docs/references/teams/update-team-membership-roles.md
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index e173f5faac..0565131665 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -473,8 +473,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
->param('teamId', '', new UID(), 'The unique team ID.')
- ->param('membershipId', '', new UID(), 'Membership ID.')
- ->param('roles', [], new ArrayList(new Key()), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
+ ->param('membershipId', '', new UID(), 'The membership ID.')
+ ->param('roles', [], new ArrayList(new Key()), 'An array of strings. Use this param to set the user\'s roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
->inject('request')
->inject('response')
->inject('user')
diff --git a/docs/references/teams/update-team-membership-roles.md b/docs/references/teams/update-team-membership-roles.md
new file mode 100644
index 0000000000..add91638bb
--- /dev/null
+++ b/docs/references/teams/update-team-membership-roles.md
@@ -0,0 +1 @@
+Use this endpoint to modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](/docs/permissions)
\ No newline at end of file
From 3ff2cf172fdfc31d0fbbb467cf561ced51504fcf Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Wed, 10 Nov 2021 17:13:51 +0100
Subject: [PATCH 005/107] Updated get team members doc
---
app/controllers/api/teams.php | 6 +++---
docs/references/teams/get-team-members.md | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 0565131665..cc5ffe52ea 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -535,10 +535,10 @@ App::get('/v1/teams/:teamId/memberships')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP_LIST)
->param('teamId', '', new UID(), 'The unique team ID.')
- ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('search', '', new Text(256), 'Search term to filter your results. Max length: 256 chars.', true)
+ ->param('limit', 25, new Range(0, 100), 'Limit how many results will be returned. By default will return a maximum of 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
- ->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
+ ->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order results by ASC or DESC order.', true)
->inject('response')
->inject('projectDB')
->action(function ($teamId, $search, $limit, $offset, $orderType, $response, $projectDB) {
diff --git a/docs/references/teams/get-team-members.md b/docs/references/teams/get-team-members.md
index ee939b9978..f8a5c7839f 100644
--- a/docs/references/teams/get-team-members.md
+++ b/docs/references/teams/get-team-members.md
@@ -1 +1 @@
-Get a team members by the team unique ID. All team members have read access for this list of resources.
\ No newline at end of file
+Lists a teams members using the team's unique ID. All team members have read access to this endpoint.
\ No newline at end of file
From 6ccceb9f326191749c0e93564b0bda3ff0fd6130 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Wed, 10 Nov 2021 17:21:36 +0100
Subject: [PATCH 006/107] Updated update team membership status doc
---
app/controllers/api/teams.php | 6 +++---
docs/references/teams/update-team-membership-status.md | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index cc5ffe52ea..f6fd001cfd 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -589,9 +589,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
->param('teamId', '', new UID(), 'The unique team ID.')
- ->param('membershipId', '', new UID(), 'Membership ID.')
- ->param('userId', '', new UID(), 'User unique ID.')
- ->param('secret', '', new Text(256), 'Secret key.')
+ ->param('membershipId', '', new UID(), 'The membership ID.')
+ ->param('userId', '', new UID(), 'The unique user ID.')
+ ->param('secret', '', new Text(256), 'The secret key.')
->inject('request')
->inject('response')
->inject('user')
diff --git a/docs/references/teams/update-team-membership-status.md b/docs/references/teams/update-team-membership-status.md
index ae2da76774..ab8f4ca36a 100644
--- a/docs/references/teams/update-team-membership-status.md
+++ b/docs/references/teams/update-team-membership-status.md
@@ -1 +1 @@
-Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email recieved by the user.
\ No newline at end of file
+Use this endpoint to allow a user to accept an invitation to join a team after being redirected back to your app from the invitation email received by the user.
\ No newline at end of file
From 4f7b699b0cad2621bd8c9db52f8566c5b283286d Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Wed, 10 Nov 2021 17:32:47 +0100
Subject: [PATCH 007/107] Updated delete team membership doc
---
app/controllers/api/teams.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index f6fd001cfd..bb5a966774 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -735,7 +735,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('teamId', '', new UID(), 'The unique team ID.')
- ->param('membershipId', '', new UID(), 'Membership ID.')
+ ->param('membershipId', '', new UID(), 'The membership ID.')
->inject('response')
->inject('projectDB')
->inject('audits')
From ab961597c42c4a5619f62ea64aab08d0df7592ee Mon Sep 17 00:00:00 2001
From: snyk-bot
Date: Sun, 28 Nov 2021 07:36:40 +0000
Subject: [PATCH 008/107] fix: upgrade chart.js from 3.5.1 to 3.6.0
Snyk has created this PR to upgrade chart.js from 3.5.1 to 3.6.0.
See this package in npm:
https://www.npmjs.com/package/chart.js
See this project in Snyk:
https://app.snyk.io/org/eldadfux/project/8574b5e4-6e89-4ade-bc02-2eaabc43eed0?utm_source=github&utm_medium=referral&page=upgrade-pr
---
package-lock.json | 14 +++++++-------
package.json | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 9d83c0ded3..27f8f09162 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.1.0",
"license": "BSD-3-Clause",
"dependencies": {
- "chart.js": "^3.5.1",
+ "chart.js": "^3.6.0",
"markdown-it": "^12.2.0",
"pell": "^1.0.6",
"prismjs": "^1.25.0",
@@ -549,9 +549,9 @@
}
},
"node_modules/chart.js": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
- "integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ=="
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.0.tgz",
+ "integrity": "sha512-iOzzDKePL+bj+ccIsVAgWQehCXv8xOKGbaU2fO/myivH736zcx535PGJzQGanvcSGVOqX6yuLZsN3ygcQ35UgQ=="
},
"node_modules/chokidar": {
"version": "2.1.8",
@@ -5484,9 +5484,9 @@
"dev": true
},
"chart.js": {
- "version": "3.5.1",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
- "integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ=="
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.0.tgz",
+ "integrity": "sha512-iOzzDKePL+bj+ccIsVAgWQehCXv8xOKGbaU2fO/myivH736zcx535PGJzQGanvcSGVOqX6yuLZsN3ygcQ35UgQ=="
},
"chokidar": {
"version": "2.1.8",
diff --git a/package.json b/package.json
index 6c71f23a67..135e4ff8a7 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"gulp-less": "^5.0.0"
},
"dependencies": {
- "chart.js": "^3.5.1",
+ "chart.js": "^3.6.0",
"markdown-it": "^12.2.0",
"pell": "^1.0.6",
"prismjs": "^1.25.0",
From 6ac92c6a598f909e696cb9eca0839def2f5c1593 Mon Sep 17 00:00:00 2001
From: Damodar Lohani
Date: Wed, 1 Dec 2021 13:02:55 +0545
Subject: [PATCH 009/107] update missing encrypt filter
---
app/config/collections2.php | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/app/config/collections2.php b/app/config/collections2.php
index 4122cd9f06..44aa99bf21 100644
--- a/app/config/collections2.php
+++ b/app/config/collections2.php
@@ -810,12 +810,12 @@ $collections = [
'$id' => 'secret',
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => 256, // var_dump of \bin2hex(\random_bytes(128)) => string(256)
+ 'size' => 256, // var_dump of \bin2hex(\random_bytes(128)) => string(256) // TODO what will happen after encryption filter?
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
- 'filters' => [],
+ 'filters' => ['encrypt'],
],
],
'indexes' => [
@@ -882,12 +882,12 @@ $collections = [
'$id' => 'httpPass',
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => Database::LENGTH_KEY,
+ 'size' => Database::LENGTH_KEY, // TODO will the length suffice after encryption?
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
- 'filters' => [],
+ 'filters' => ['encrypt'],
],
[
'$id' => 'security',
@@ -970,7 +970,7 @@ $collections = [
'required' => false,
'default' => null,
'array' => false,
- 'filters' => [],
+ 'filters' => ['encrypt'],
],
[
'$id' => 'passwordUpdate',
@@ -1150,23 +1150,23 @@ $collections = [
'$id' => 'providerToken',
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => 16384,
+ 'size' => 16384, // TODO is this required size and will it suffice after encryption?
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
- 'filters' => [],
+ 'filters' => ['encrypt'],
],
[
'$id' => 'secret',
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => 64, // https://www.tutorialspoint.com/how-long-is-the-sha256-hash-in-mysql
+ 'size' => 64, // https://www.tutorialspoint.com/how-long-is-the-sha256-hash-in-mysql // TODO what will happen to size after encryption?
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
- 'filters' => [],
+ 'filters' => ['encrypt'],
],
[
'$id' => 'expire',
From b0762f169d45ffeb7cd1aafc6d65bd0878f34898 Mon Sep 17 00:00:00 2001
From: Damodar Lohani
Date: Wed, 1 Dec 2021 13:18:49 +0545
Subject: [PATCH 010/107] encrypt oauth2 providers
---
app/config/collections2.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/config/collections2.php b/app/config/collections2.php
index 44aa99bf21..69700c1ebe 100644
--- a/app/config/collections2.php
+++ b/app/config/collections2.php
@@ -500,7 +500,7 @@ $collections = [
'required' => false,
'default' => null,
'array' => false,
- 'filters' => ['json'],
+ 'filters' => ['json', 'encrypt'],
],
[
'$id' => 'platforms',
From d60dc467f821801c2474f40112c4a79a998c4e3b Mon Sep 17 00:00:00 2001
From: Damodar Lohani
Date: Thu, 2 Dec 2021 13:34:56 +0545
Subject: [PATCH 011/107] incrementing field size for encrypted fields
---
app/config/collections2.php | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/config/collections2.php b/app/config/collections2.php
index 69700c1ebe..49c87e2231 100644
--- a/app/config/collections2.php
+++ b/app/config/collections2.php
@@ -810,7 +810,7 @@ $collections = [
'$id' => 'secret',
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => 256, // var_dump of \bin2hex(\random_bytes(128)) => string(256) // TODO what will happen after encryption filter?
+ 'size' => 512, // var_dump of \bin2hex(\random_bytes(128)) => string(256) doubling for encryption
'signed' => true,
'required' => true,
'default' => null,
@@ -1150,7 +1150,7 @@ $collections = [
'$id' => 'providerToken',
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => 16384, // TODO is this required size and will it suffice after encryption?
+ 'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
@@ -1161,7 +1161,7 @@ $collections = [
'$id' => 'secret',
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => 64, // https://www.tutorialspoint.com/how-long-is-the-sha256-hash-in-mysql // TODO what will happen to size after encryption?
+ 'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
From 32b13f113adbfb02a08392a2f495a6000e728af6 Mon Sep 17 00:00:00 2001
From: Damodar Lohani
Date: Thu, 2 Dec 2021 15:32:40 +0545
Subject: [PATCH 012/107] session secret field size increase
---
app/config/collections.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/config/collections.php b/app/config/collections.php
index 380982889a..9afb8f1d78 100644
--- a/app/config/collections.php
+++ b/app/config/collections.php
@@ -1161,7 +1161,7 @@ $collections = [
'$id' => 'secret',
'type' => Database::VAR_STRING,
'format' => '',
- 'size' => 256, // https://www.tutorialspoint.com/how-long-is-the-sha256-hash-in-mysql (256 for encryption)
+ 'size' => 512, // https://www.tutorialspoint.com/how-long-is-the-sha256-hash-in-mysql (512 for encryption)
'signed' => true,
'required' => false,
'default' => null,
From e6304d71b0c27c9650f94835ac50a76d37f69c63 Mon Sep 17 00:00:00 2001
From: Damodar Lohani
Date: Thu, 2 Dec 2021 15:35:19 +0545
Subject: [PATCH 013/107] remove encrypt filter from password as it's already
hashed
---
app/config/collections.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/config/collections.php b/app/config/collections.php
index 9afb8f1d78..4216c1fa32 100644
--- a/app/config/collections.php
+++ b/app/config/collections.php
@@ -970,7 +970,7 @@ $collections = [
'required' => false,
'default' => null,
'array' => false,
- 'filters' => ['encrypt'],
+ 'filters' => [],
],
[
'$id' => 'passwordUpdate',
From a32351ff7e35ab646a14d5c3af310f0872b2c790 Mon Sep 17 00:00:00 2001
From: Damodar Lohani
Date: Thu, 2 Dec 2021 18:44:28 +0545
Subject: [PATCH 014/107] add the filter back
---
app/config/collections.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/config/collections.php b/app/config/collections.php
index 4216c1fa32..a5face465b 100644
--- a/app/config/collections.php
+++ b/app/config/collections.php
@@ -1497,7 +1497,7 @@ $collections = [
'required' => false,
'default' => null,
'array' => false,
- 'filters' => [],
+ 'filters' => ['encrypt'],
],
],
'indexes' => [
From 244b829c6800c63b05e90ad3e464b0abe80418c6 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Thu, 2 Dec 2021 23:10:14 +0100
Subject: [PATCH 015/107] Update app/controllers/api/teams.php
Co-authored-by: kodumbeats
---
app/controllers/api/teams.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index bb5a966774..7b06c13d7f 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -114,7 +114,7 @@ App::get('/v1/teams')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM_LIST)
->param('search', '', new Text(256), 'Enter any text to search. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Limit how many results will be returned. Returns 25 results by default. Maximum of 100 results allowed per request.', true)
+ ->param('limit', 25, new Range(0, 100), 'Limit how many results will be returned. Returns up to 25 results by default. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 2000), 'Use this value to manage pagination. The default value is 0.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Use ASC to order results in ascending and DESC to order results in descending order.', true)
->inject('response')
From 18dfe9b4a0a57385ccbdd9924b26a1270e8a009a Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Thu, 2 Dec 2021 23:10:36 +0100
Subject: [PATCH 016/107] Update docs/references/teams/get-team-members.md
Co-authored-by: kodumbeats
---
docs/references/teams/get-team-members.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/references/teams/get-team-members.md b/docs/references/teams/get-team-members.md
index f8a5c7839f..d18b2b358e 100644
--- a/docs/references/teams/get-team-members.md
+++ b/docs/references/teams/get-team-members.md
@@ -1 +1 @@
-Lists a teams members using the team's unique ID. All team members have read access to this endpoint.
\ No newline at end of file
+Lists a team's members using the team's unique ID. All team members have read access to this endpoint.
\ No newline at end of file
From c3f06f9a580a8bbfbbd0e72c78594aa59c572c82 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Thu, 2 Dec 2021 23:19:07 +0100
Subject: [PATCH 017/107] Update app/controllers/api/teams.php
Co-authored-by: kodumbeats
---
app/controllers/api/teams.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 7b06c13d7f..1b2e998737 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -262,7 +262,7 @@ App::post('/v1/teams/:teamId/memberships')
->label('abuse-limit', 10)
->param('teamId', '', new UID(), 'The unique team ID.')
->param('email', '', new Email(), 'The email address of the new team member.')
- ->param('roles', [], new ArrayList(new Key()), 'An Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
+ ->param('roles', [], new ArrayList(new Key()), 'An array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page
->param('name', '', new Text(128), 'The name of the new team member. Max length: 128 chars.', true)
->inject('response')
From 95e2cb6290c4d35fc61f454af56a123b2094b825 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Thu, 2 Dec 2021 23:20:57 +0100
Subject: [PATCH 018/107] Update app/controllers/api/teams.php
Co-authored-by: kodumbeats
---
app/controllers/api/teams.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 1b2e998737..f822a81fed 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -116,7 +116,7 @@ App::get('/v1/teams')
->param('search', '', new Text(256), 'Enter any text to search. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Limit how many results will be returned. Returns up to 25 results by default. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 2000), 'Use this value to manage pagination. The default value is 0.', true)
- ->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Use ASC to order results in ascending and DESC to order results in descending order.', true)
+ ->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('projectDB')
->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) {
From ea8985d5916e643161d7d1d985690e8b3361a7b8 Mon Sep 17 00:00:00 2001
From: Damodar Lohani
Date: Fri, 3 Dec 2021 13:23:21 +0545
Subject: [PATCH 019/107] skip decryption for null
---
app/init.php | 3 +++
1 file changed, 3 insertions(+)
diff --git a/app/init.php b/app/init.php
index 6992728987..1947856c15 100644
--- a/app/init.php
+++ b/app/init.php
@@ -332,6 +332,9 @@ Database::addFilter('encrypt',
]);
},
function($value) {
+ if(is_null($value)) {
+ return null;
+ }
$value = json_decode($value, true);
$key = App::getEnv('_APP_OPENSSL_KEY_V'.$value['version']);
From e944da2f3e08e2f6b864a0538a9cf2a980d8c02b Mon Sep 17 00:00:00 2001
From: Torsten Dittmann
Date: Mon, 6 Dec 2021 13:03:12 +0100
Subject: [PATCH 020/107] feat(realtime): add console channel
---
app/controllers/shared/api.php | 8 +-
app/init.php | 40 -
app/workers/database.php | 72 +-
public/dist/scripts/app-all.js | 3 +-
public/dist/scripts/app.js | 3 +-
public/scripts/init.js | 23 +-
src/Appwrite/Messaging/Adapter/Realtime.php | 18 +-
tests/e2e/Services/Realtime/RealtimeBase.php | 1041 +----------------
.../Realtime/RealtimeConsoleClientTest.php | 276 +++++
.../Realtime/RealtimeCustomClientTest.php | 1037 ++++++++++++++++
10 files changed, 1434 insertions(+), 1087 deletions(-)
create mode 100644 tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php
diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php
index 210cb46912..3bd1aba579 100644
--- a/app/controllers/shared/api.php
+++ b/app/controllers/shared/api.php
@@ -166,7 +166,7 @@ App::init(function ($utopia, $request, $project) {
throw new Exception('JWT authentication is disabled for this project', 501);
}
break;
-
+
default:
throw new Exception('Unsupported authentication route');
break;
@@ -187,7 +187,7 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
/** @var bool $mode */
if (!empty($events->getParam('event'))) {
- if(empty($events->getParam('eventData'))) {
+ if (empty($events->getParam('eventData'))) {
$events->setParam('eventData', $response->getPayload());
}
@@ -206,10 +206,10 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits
if ($project->getId() !== 'console') {
$payload = new Document($response->getPayload());
- $target = Realtime::fromPayload($events->getParam('event'), $payload);
+ $target = Realtime::fromPayload($events->getParam('event'), $payload, $project);
Realtime::send(
- $project->getId(),
+ $target['projectId'] ?? $project->getId(),
$response->getPayload(),
$events->getParam('event'),
$target['channels'],
diff --git a/app/init.php b/app/init.php
index 6992728987..0c7c9e55fc 100644
--- a/app/init.php
+++ b/app/init.php
@@ -21,9 +21,6 @@ use Appwrite\Extend\PDO;
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
-use Appwrite\Database\Database as DatabaseOld;
-use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
-use Appwrite\Database\Adapter\Redis as RedisAdapter;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
@@ -156,43 +153,6 @@ if(!empty($user) || !empty($pass)) {
Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '').':'.App::getEnv('_APP_REDIS_PORT', ''));
}
-/**
- * Old DB Filters
- */
-DatabaseOld::addFilter('json',
- function($value) {
- if(!is_array($value)) {
- return $value;
- }
- return json_encode($value);
- },
- function($value) {
- return json_decode($value, true);
- }
-);
-
-DatabaseOld::addFilter('encrypt',
- function($value) {
- $key = App::getEnv('_APP_OPENSSL_KEY_V1');
- $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
- $tag = null;
-
- return json_encode([
- 'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
- 'method' => OpenSSL::CIPHER_AES_128_GCM,
- 'iv' => bin2hex($iv),
- 'tag' => bin2hex($tag),
- 'version' => '1',
- ]);
- },
- function($value) {
- $value = json_decode($value, true);
- $key = App::getEnv('_APP_OPENSSL_KEY_V'.$value['version']);
-
- return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag']));
- }
-);
-
/**
* New DB Filters
*/
diff --git a/app/workers/database.php b/app/workers/database.php
index 3b9f36afbd..f58502704a 100644
--- a/app/workers/database.php
+++ b/app/workers/database.php
@@ -1,5 +1,6 @@
isEmpty()) {
throw new Exception('Missing document');
}
-
+
switch (strval($type)) {
case DATABASE_TYPE_CREATE_ATTRIBUTE:
$this->createAttribute($collection, $document, $projectId);
@@ -67,9 +68,11 @@ class DatabaseV1 extends Worker
*/
protected function createAttribute(Document $collection, Document $attribute, string $projectId): void
{
+ $dbForConsole = $this->getConsoleDB();
$dbForInternal = $this->getInternalDB($projectId);
$dbForExternal = $this->getExternalDB($projectId);
+ $event = 'database.attributes.update';
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$type = $attribute->getAttribute('type', '');
@@ -81,6 +84,7 @@ class DatabaseV1 extends Worker
$format = $attribute->getAttribute('format', '');
$formatOptions = $attribute->getAttribute('formatOptions', []);
$filters = $attribute->getAttribute('filters', []);
+ $project = $dbForConsole->getDocument('projects', $projectId);
try {
if(!$dbForExternal->createAttribute($collectionId, $key, $type, $size, $required, $default, $signed, $array, $format, $formatOptions, $filters)) {
@@ -90,6 +94,20 @@ class DatabaseV1 extends Worker
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'failed'));
+ } finally {
+ $target = Realtime::fromPayload($event, $attribute, $project);
+
+ Realtime::send(
+ projectId: 'console',
+ payload: $attribute->getArrayCopy(),
+ event: $event,
+ channels: $target['channels'],
+ roles: $target['roles'],
+ options: [
+ 'projectId' => $projectId,
+ 'collectionId' => $collection->getId()
+ ]
+ );
}
$dbForInternal->deleteCachedDocument('collections', $collectionId);
@@ -102,11 +120,15 @@ class DatabaseV1 extends Worker
*/
protected function deleteAttribute(Document $collection, Document $attribute, string $projectId): void
{
+ $dbForConsole = $this->getConsoleDB();
$dbForInternal = $this->getInternalDB($projectId);
$dbForExternal = $this->getExternalDB($projectId);
+
+ $event = 'database.attributes.delete';
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$status = $attribute->getAttribute('status', '');
+ $project = $dbForConsole->getDocument('projects', $projectId);
// possible states at this point:
// - available: should not land in queue; controller flips these to 'deleting'
@@ -122,6 +144,20 @@ class DatabaseV1 extends Worker
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForInternal->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'stuck'));
+ } finally {
+ $target = Realtime::fromPayload($event, $attribute, $project);
+
+ Realtime::send(
+ projectId: 'console',
+ payload: $attribute->getArrayCopy(),
+ event: $event,
+ channels: $target['channels'],
+ roles: $target['roles'],
+ options: [
+ 'projectId' => $projectId,
+ 'collectionId' => $collection->getId()
+ ]
+ );
}
// The underlying database removes/rebuilds indexes when attribute is removed
@@ -185,15 +221,18 @@ class DatabaseV1 extends Worker
*/
protected function createIndex(Document $collection, Document $index, string $projectId): void
{
+ $dbForConsole = $this->getConsoleDB();
$dbForInternal = $this->getInternalDB($projectId);
$dbForExternal = $this->getExternalDB($projectId);
+ $event = 'database.indexes.update';
$collectionId = $collection->getId();
$key = $index->getAttribute('key', '');
$type = $index->getAttribute('type', '');
$attributes = $index->getAttribute('attributes', []);
$lengths = $index->getAttribute('lengths', []);
$orders = $index->getAttribute('orders', []);
+ $project = $dbForConsole->getDocument('projects', $projectId);
try {
if(!$dbForExternal->createIndex($collectionId, $key, $type, $attributes, $lengths, $orders)) {
@@ -203,6 +242,20 @@ class DatabaseV1 extends Worker
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'failed'));
+ } finally {
+ $target = Realtime::fromPayload($event, $index, $project);
+
+ Realtime::send(
+ projectId: 'console',
+ payload: $index->getArrayCopy(),
+ event: $event,
+ channels: $target['channels'],
+ roles: $target['roles'],
+ options: [
+ 'projectId' => $projectId,
+ 'collectionId' => $collection->getId()
+ ]
+ );
}
$dbForInternal->deleteCachedDocument('collections', $collectionId);
@@ -215,12 +268,15 @@ class DatabaseV1 extends Worker
*/
protected function deleteIndex(Document $collection, Document $index, string $projectId): void
{
+ $dbForConsole = $this->getConsoleDB();
$dbForInternal = $this->getInternalDB($projectId);
$dbForExternal = $this->getExternalDB($projectId);
$collectionId = $collection->getId();
$key = $index->getAttribute('key');
$status = $index->getAttribute('status', '');
+ $event = 'database.indexes.delete';
+ $project = $dbForConsole->getDocument('projects', $projectId);
try {
if($status !== 'failed' && !$dbForExternal->deleteIndex($collectionId, $key)) {
@@ -230,6 +286,20 @@ class DatabaseV1 extends Worker
} catch (\Throwable $th) {
Console::error($th->getMessage());
$dbForInternal->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'stuck'));
+ } finally {
+ $target = Realtime::fromPayload($event, $index, $project);
+
+ Realtime::send(
+ projectId: 'console',
+ payload: $index->getArrayCopy(),
+ event: $event,
+ channels: $target['channels'],
+ roles: $target['roles'],
+ options: [
+ 'projectId' => $projectId,
+ 'collectionId' => $collection->getId()
+ ]
+ );
}
$dbForInternal->deleteCachedDocument('collections', $collectionId);
diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js
index cbddd2d614..b9632bf90b 100644
--- a/public/dist/scripts/app-all.js
+++ b/public/dist/scripts/app-all.js
@@ -3427,7 +3427,8 @@ function handler2(){}
handler2.inline=(el,{expression},{cleanup:cleanup2})=>{let root=closestRoot(el);if(!root._x_refs)
root._x_refs={};root._x_refs[expression]=el;cleanup2(()=>delete root._x_refs[expression]);};directive("ref",handler2);directive("if",(el,{expression},{effect:effect3,cleanup:cleanup2})=>{let evaluate2=evaluateLater(el,expression);let show=()=>{if(el._x_currentIfEl)
return el._x_currentIfEl;let clone2=el.content.cloneNode(true).firstElementChild;addScopeToNode(clone2,{},el);mutateDom(()=>{el.after(clone2);initTree(clone2);});el._x_currentIfEl=clone2;el._x_undoIf=()=>{clone2.remove();delete el._x_currentIfEl;};return clone2;};let hide=()=>{if(!el._x_undoIf)
-return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe('project',event=>{for(let project in event.payload){current[project]=event.payload[project]??0;}});while(true){let newHistory={};let createdHistory=false;for(const project in current){let history=realtime?.history??{};if(!(project in history)){history[project]=new Array(bars).fill({percentage:0,value:0});}
+return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe(['project','console'],response=>{switch(response.event){case'stats.connections':for(let project in response.payload){current[project]=response.payload[project]??0;}
+break;case'database.attributes.create':case'database.attributes.update':case'database.attributes.delete':document.dispatchEvent(new CustomEvent('database.createAttribute'));break;case'database.indexes.create':case'database.indexes.update':case'database.indexes.delete':document.dispatchEvent(new CustomEvent('database.createIndex'));break;}});while(true){let newHistory={};let createdHistory=false;for(const project in current){let history=realtime?.history??{};if(!(project in history)){history[project]=new Array(bars).fill({percentage:0,value:0});}
history=history[project];history.push({percentage:0,value:current[project]});if(history.length>=bars){history.shift();}
const highest=history.reduce((prev,curr)=>{return(curr.value>prev)?curr.value:prev;},0);history=history.map(({percentage,value})=>{createdHistory=true;percentage=value===0?0:((Math.round((value/highest)*10)/10)*100);if(percentage>100)percentage=100;else if(percentage==0&&value!=0)percentage=5;return{percentage:percentage,value:value};})
newHistory[project]=history;}
diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js
index c7eb1c9eee..f2f5a08f79 100644
--- a/public/dist/scripts/app.js
+++ b/public/dist/scripts/app.js
@@ -493,7 +493,8 @@ function handler2(){}
handler2.inline=(el,{expression},{cleanup:cleanup2})=>{let root=closestRoot(el);if(!root._x_refs)
root._x_refs={};root._x_refs[expression]=el;cleanup2(()=>delete root._x_refs[expression]);};directive("ref",handler2);directive("if",(el,{expression},{effect:effect3,cleanup:cleanup2})=>{let evaluate2=evaluateLater(el,expression);let show=()=>{if(el._x_currentIfEl)
return el._x_currentIfEl;let clone2=el.content.cloneNode(true).firstElementChild;addScopeToNode(clone2,{},el);mutateDom(()=>{el.after(clone2);initTree(clone2);});el._x_currentIfEl=clone2;el._x_undoIf=()=>{clone2.remove();delete el._x_currentIfEl;};return clone2;};let hide=()=>{if(!el._x_undoIf)
-return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe('project',event=>{for(let project in event.payload){current[project]=event.payload[project]??0;}});while(true){let newHistory={};let createdHistory=false;for(const project in current){let history=realtime?.history??{};if(!(project in history)){history[project]=new Array(bars).fill({percentage:0,value:0});}
+return;el._x_undoIf();delete el._x_undoIf;};effect3(()=>evaluate2((value)=>{value?show():hide();}));cleanup2(()=>el._x_undoIf&&el._x_undoIf());});mapAttributes(startingWith("@",into(prefix("on:"))));directive("on",skipDuringClone((el,{value,modifiers,expression},{cleanup:cleanup2})=>{let evaluate2=expression?evaluateLater(el,expression):()=>{};let removeListener=on(el,value,modifiers,(e)=>{evaluate2(()=>{},{scope:{$event:e},params:[e]});});cleanup2(()=>removeListener());}));alpine_default.setEvaluator(normalEvaluator);alpine_default.setReactivityEngine({reactive:reactive2,effect:effect2,release:stop,raw:toRaw});var src_default=alpine_default;window.Alpine=src_default;queueMicrotask(()=>{src_default.start();});})();window.ls.error=function(){return function(error){window.console.error(error);if(window.location.pathname!=='/console'){window.location='/console';}};};window.addEventListener("error",function(event){console.error("ERROR-EVENT:",event.error.message,event.error.stack);});document.addEventListener("account.deleteSession",function(){window.location="/auth/signin";});document.addEventListener("account.create",function(){let container=window.ls.container;let form=container.get('serviceForm');let sdk=container.get('console');let promise=sdk.account.createSession(form.email,form.password);container.set("serviceForm",{},true,true);promise.then(function(){var subscribe=document.getElementById('newsletter').checked;if(subscribe){let alerts=container.get('alerts');let loaderId=alerts.add({text:'Loading...',class:""},0);fetch('https://appwrite.io/v1/newsletter/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:form.name,email:form.email,}),}).finally(function(){alerts.remove(loaderId);window.location='/console';});}else{window.location='/console';}},function(error){window.location='/auth/signup?failure=1';});});window.addEventListener("load",async()=>{const bars=12;const realtime=window.ls.container.get('realtime');const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));let current={};window.ls.container.get('console').subscribe(['project','console'],response=>{switch(response.event){case'stats.connections':for(let project in response.payload){current[project]=response.payload[project]??0;}
+break;case'database.attributes.create':case'database.attributes.update':case'database.attributes.delete':document.dispatchEvent(new CustomEvent('database.createAttribute'));break;case'database.indexes.create':case'database.indexes.update':case'database.indexes.delete':document.dispatchEvent(new CustomEvent('database.createIndex'));break;}});while(true){let newHistory={};let createdHistory=false;for(const project in current){let history=realtime?.history??{};if(!(project in history)){history[project]=new Array(bars).fill({percentage:0,value:0});}
history=history[project];history.push({percentage:0,value:current[project]});if(history.length>=bars){history.shift();}
const highest=history.reduce((prev,curr)=>{return(curr.value>prev)?curr.value:prev;},0);history=history.map(({percentage,value})=>{createdHistory=true;percentage=value===0?0:((Math.round((value/highest)*10)/10)*100);if(percentage>100)percentage=100;else if(percentage==0&&value!=0)percentage=5;return{percentage:percentage,value:value};})
newHistory[project]=history;}
diff --git a/public/scripts/init.js b/public/scripts/init.js
index 711c678ac5..fb0e4b5383 100644
--- a/public/scripts/init.js
+++ b/public/scripts/init.js
@@ -57,10 +57,27 @@ window.addEventListener("load", async () => {
const realtime = window.ls.container.get('realtime');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
let current = {};
- window.ls.container.get('console').subscribe('project', event => {
- for (let project in event.payload) {
- current[project] = event.payload[project] ?? 0;
+ window.ls.container.get('console').subscribe(['project', 'console'], response => {
+ switch (response.event) {
+ case 'stats.connections':
+ for (let project in response.payload) {
+ current[project] = response.payload[project] ?? 0;
+ }
+ break;
+ case 'database.attributes.create':
+ case 'database.attributes.update':
+ case 'database.attributes.delete':
+ document.dispatchEvent(new CustomEvent('database.createAttribute'));
+
+ break;
+ case 'database.indexes.create':
+ case 'database.indexes.update':
+ case 'database.indexes.delete':
+ document.dispatchEvent(new CustomEvent('database.createIndex'));
+
+ break;
}
+
});
while (true) {
diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php
index 4269fa04dc..71738e42ba 100644
--- a/src/Appwrite/Messaging/Adapter/Realtime.php
+++ b/src/Appwrite/Messaging/Adapter/Realtime.php
@@ -233,17 +233,19 @@ class Realtime extends Adapter
}
/**
- * Create channels array based on the event name and payload.
- *
+ * Creae channels array based on the event name and payload.
+ *
* @param string $event
* @param Document $payload
+ * @param Document|null $project
* @return array
*/
- public static function fromPayload(string $event, Document $payload): array
+ public static function fromPayload(string $event, Document $payload, Document $project = null): array
{
$channels = [];
$roles = [];
$permissionsChanged = false;
+ $projectId = null;
switch (true) {
case strpos($event, 'account.recovery.') === 0:
@@ -279,6 +281,13 @@ class Realtime extends Adapter
$channels[] = 'collections.' . $payload->getId();
$roles = $payload->getRead();
+ break;
+ case strpos($event, 'database.attributes.') === 0:
+ case strpos($event, 'database.indexes.') === 0:
+ $channels[] = 'console';
+ $projectId = 'console';
+ $roles = ['team:' . $project->getAttribute('teamId')];
+
break;
case strpos($event, 'database.documents.') === 0:
$channels[] = 'documents';
@@ -306,7 +315,8 @@ class Realtime extends Adapter
return [
'channels' => $channels,
'roles' => $roles,
- 'permissionsChanged' => $permissionsChanged
+ 'permissionsChanged' => $permissionsChanged,
+ 'projectId' => $projectId
];
}
}
diff --git a/tests/e2e/Services/Realtime/RealtimeBase.php b/tests/e2e/Services/Realtime/RealtimeBase.php
index 68a121df93..1f734333d8 100644
--- a/tests/e2e/Services/Realtime/RealtimeBase.php
+++ b/tests/e2e/Services/Realtime/RealtimeBase.php
@@ -2,24 +2,26 @@
namespace Tests\E2E\Services\Realtime;
-use CURLFile;
-use Tests\E2E\Client;
-use WebSocket\Client as WebSocketClient;
use WebSocket\ConnectionException;
+use WebSocket\Client as WebSocketClient;
trait RealtimeBase
{
-
- private function getWebsocket($channels = [], $headers = [])
+ private function getWebsocket($channels = [], $headers = [], $projectId = null)
{
+ if (is_null($projectId)) {
+ $projectId = $this->getProject()['$id'];
+ }
+
$headers = array_merge([
'Origin' => 'appwrite.test'
], $headers);
$query = [
- 'project' => $this->getProject()['$id'],
+ 'project' => $projectId,
'channels' => $channels
];
+
return new WebSocketClient('ws://appwrite-traefik/v1/realtime?' . http_build_query($query), [
'headers' => $headers,
'timeout' => 30,
@@ -38,17 +40,6 @@ trait RealtimeBase
/**
* Test for FAILURE
*/
- $client = $this->getWebsocket(['documents'], ['origin' => 'http://appwrite.unknown']);
- $payload = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $payload);
- $this->assertArrayHasKey('data', $payload);
- $this->assertEquals('error', $payload['type']);
- $this->assertEquals(1008, $payload['data']['code']);
- $this->assertEquals('Invalid Origin. Register your new client (appwrite.unknown) as a new Web platform on your project console dashboard', $payload['data']['message']);
- $this->expectException(ConnectionException::class); // Check if server disconnnected client
- $client->close();
-
$client = $this->getWebsocket();
$payload = json_decode($client->receive(), true);
@@ -90,1020 +81,4 @@ trait RealtimeBase
$this->expectException(ConnectionException::class); // Check if server disconnnected client
$client->close();
}
-
- public function testChannelParsing()
- {
- $user = $this->getUser();
- $userId = $user['$id'] ?? '';
- $session = $user['session'] ?? '';
- $headers = [
- 'origin' => 'http://localhost',
- 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session
- ];
-
- $client = $this->getWebsocket(['documents'], $headers);
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertCount(1, $response['data']['channels']);
- $this->assertContains('documents', $response['data']['channels']);
- $this->assertEquals($userId, $response['data']['user']['$id']);
-
- $client->close();
-
- $client = $this->getWebsocket(['account'], $headers);
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals($userId, $response['data']['user']['$id']);
-
- $client->close();
-
- $client = $this->getWebsocket(['account', 'documents', 'account.123'], $headers);
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertCount(3, $response['data']['channels']);
- $this->assertContains('documents', $response['data']['channels']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals($userId, $response['data']['user']['$id']);
-
- $client->close();
-
- $client = $this->getWebsocket([
- 'account',
- 'files',
- 'files.1',
- 'collections',
- 'collections.1',
- 'collections.1.documents',
- 'collections.2',
- 'collections.2.documents',
- 'documents',
- 'documents.1',
- 'documents.2',
- ], $headers);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertCount(12, $response['data']['channels']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertContains('files', $response['data']['channels']);
- $this->assertContains('files.1', $response['data']['channels']);
- $this->assertContains('collections', $response['data']['channels']);
- $this->assertContains('collections.1', $response['data']['channels']);
- $this->assertContains('collections.1.documents', $response['data']['channels']);
- $this->assertContains('collections.2', $response['data']['channels']);
- $this->assertContains('collections.2.documents', $response['data']['channels']);
- $this->assertContains('documents', $response['data']['channels']);
- $this->assertContains('documents.1', $response['data']['channels']);
- $this->assertContains('documents.2', $response['data']['channels']);
- $this->assertEquals($userId, $response['data']['user']['$id']);
-
- $client->close();
- }
-
- public function testManualAuthentication()
- {
- $user = $this->getUser();
- $userId = $user['$id'] ?? '';
- $session = $user['session'] ?? '';
-
- /**
- * Test for SUCCESS
- */
- $client = $this->getWebsocket(['account'], [
- 'origin' => 'http://localhost'
- ]);
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(1, $response['data']['channels']);
- $this->assertContains('account', $response['data']['channels']);
-
- $client->send(\json_encode([
- 'type' => 'authentication',
- 'data' => [
- 'session' => $session
- ]
- ]));
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('response', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertEquals('authentication', $response['data']['to']);
- $this->assertTrue($response['data']['success']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertEquals($userId, $response['data']['user']['$id']);
-
- /**
- * Test for FAILURE
- */
- $client->send(\json_encode([
- 'type' => 'authentication',
- 'data' => [
- 'session' => 'invalid_session'
- ]
- ]));
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('error', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertEquals(1003, $response['data']['code']);
- $this->assertEquals('Session is not valid.', $response['data']['message']);
-
- $client->send(\json_encode([
- 'type' => 'authentication',
- 'data' => []
- ]));
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('error', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertEquals(1003, $response['data']['code']);
- $this->assertEquals('Payload is not valid.', $response['data']['message']);
-
- $client->send(\json_encode([
- 'type' => 'unknown',
- 'data' => [
- 'session' => 'invalid_session'
- ]
- ]));
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('error', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertEquals(1003, $response['data']['code']);
- $this->assertEquals('Message type is not valid.', $response['data']['message']);
-
- $client->send(\json_encode([
- 'test' => '123',
- ]));
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('error', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertEquals(1003, $response['data']['code']);
- $this->assertEquals('Message format is not valid.', $response['data']['message']);
-
-
- $client->close();
- }
-
- public function testChannelAccount()
- {
- $user = $this->getUser();
- $userId = $user['$id'] ?? '';
- $session = $user['session'] ?? '';
- $projectId = $this->getProject()['$id'];
-
- $client = $this->getWebsocket(['account'], [
- 'origin' => 'http://localhost',
- 'cookie' => 'a_session_'.$projectId.'=' . $session
- ]);
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertEquals($userId, $response['data']['user']['$id']);
-
- /**
- * Test Account Name Event
- */
- $name = "Torsten Dittmann";
-
- $this->client->call(Client::METHOD_PATCH, '/account/name', array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- 'cookie' => 'a_session_' . $projectId . '=' . $session,
- ]), [
- 'name' => $name
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.update.name', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $this->assertEquals($name, $response['data']['payload']['name']);
-
-
- /**
- * Test Account Password Event
- */
- $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- 'cookie' => 'a_session_'.$projectId.'=' . $session,
- ]), [
- 'password' => 'new-password',
- 'oldPassword' => 'password',
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.update.password', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $this->assertEquals($name, $response['data']['payload']['name']);
-
- /**
- * Test Account Email Update
- */
- $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- 'cookie' => 'a_session_'.$projectId.'=' . $session,
- ]), [
- 'email' => 'torsten@appwrite.io',
- 'password' => 'new-password',
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.update.email', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $this->assertEquals('torsten@appwrite.io', $response['data']['payload']['email']);
-
- /**
- * Test Account Verification Create
- */
- $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- 'cookie' => 'a_session_'.$projectId.'=' . $session,
- ]), [
- 'url' => 'http://localhost/verification',
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.verification.create', $response['data']['event']);
-
- $lastEmail = $this->getLastEmail();
- $verification = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
-
- /**
- * Test Account Verification Complete
- */
- $response = $this->client->call(Client::METHOD_PUT, '/account/verification', array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- 'cookie' => 'a_session_'.$projectId.'=' . $session,
- ]), [
- 'userId' => $userId,
- 'secret' => $verification,
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.verification.update', $response['data']['event']);
-
- /**
- * Test Acoount Prefs Update
- */
- $this->client->call(Client::METHOD_PATCH, '/account/prefs', array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- 'cookie' => 'a_session_'.$projectId.'=' . $session,
- ]), [
- 'prefs' => [
- 'prefKey1' => 'prefValue1',
- 'prefKey2' => 'prefValue2',
- ]
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.update.prefs', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- /**
- * Test Account Session Create
- */
- $response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- ]), [
- 'email' => 'torsten@appwrite.io',
- 'password' => 'new-password',
- ]);
-
- $sessionNew = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$projectId];
- $sessionNewId = $response['body']['$id'];
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.sessions.create', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- /**
- * Test Account Session Delete
- */
- $this->client->call(Client::METHOD_DELETE, '/account/sessions/'.$sessionNewId, array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- 'cookie' => 'a_session_'.$projectId.'=' . $sessionNew,
- ]));
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.sessions.delete', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- /**
- * Test Account Create Recovery
- */
- $this->client->call(Client::METHOD_POST, '/account/recovery', array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- ]), [
- 'email' => 'torsten@appwrite.io',
- 'url' => 'http://localhost/recovery',
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $lastEmail = $this->getLastEmail();
- $recovery = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.recovery.create', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $response = $this->client->call(Client::METHOD_PUT, '/account/recovery', array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- ]), [
- 'userId' => $userId,
- 'secret' => $recovery,
- 'password' => 'test-recovery',
- 'passwordAgain' => 'test-recovery',
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertContains('account', $response['data']['channels']);
- $this->assertContains('account.' . $userId, $response['data']['channels']);
- $this->assertEquals('account.recovery.update', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $client->close();
- }
-
- public function testChannelDatabase()
- {
- $user = $this->getUser();
- $session = $user['session'] ?? '';
- $projectId = $this->getProject()['$id'];
-
- $client = $this->getWebsocket(['documents', 'collections'], [
- 'origin' => 'http://localhost',
- 'cookie' => 'a_session_'.$projectId.'=' . $session
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('documents', $response['data']['channels']);
- $this->assertContains('collections', $response['data']['channels']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertEquals($user['$id'], $response['data']['user']['$id']);
-
- /**
- * Test Collection Create
- */
- $actors = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'collectionId' => 'unique()',
- 'name' => 'Actors',
- 'read' => ['role:all'],
- 'write' => ['role:all'],
- 'permission' => 'collection'
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('collections', $response['data']['channels']);
- $this->assertContains('collections.' . $actors['body']['$id'], $response['data']['channels']);
- $this->assertEquals('database.collections.create', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $data = ['actorsId' => $actors['body']['$id']];
-
- $name = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'attributeId' => 'name',
- 'size' => 256,
- 'required' => true,
- ]);
-
- $this->assertEquals($name['headers']['status-code'], 201);
- $this->assertEquals($name['body']['key'], 'name');
- $this->assertEquals($name['body']['type'], 'string');
- $this->assertEquals($name['body']['size'], 256);
- $this->assertEquals($name['body']['required'], true);
-
- sleep(2);
-
- /**
- * Test Document Create
- */
- $document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'documentId' => 'unique()',
- 'data' => [
- 'name' => 'Chris Evans'
- ],
- 'read' => ['role:all'],
- 'write' => ['role:all'],
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(3, $response['data']['channels']);
- $this->assertContains('documents', $response['data']['channels']);
- $this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']);
- $this->assertContains('collections.' . $actors['body']['$id'] . '.documents', $response['data']['channels']);
- $this->assertEquals('database.documents.create', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
- $this->assertEquals($response['data']['payload']['name'], 'Chris Evans');
-
- $data['documentId'] = $document['body']['$id'];
-
- /**
- * Test Document Update
- */
- $document = $this->client->call(Client::METHOD_PATCH, '/database/collections/' . $data['actorsId'] . '/documents/' . $data['documentId'], array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'documentId' => 'unique()',
- 'data' => [
- 'name' => 'Chris Evans 2'
- ],
- 'read' => ['role:all'],
- 'write' => ['role:all'],
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(3, $response['data']['channels']);
- $this->assertContains('documents', $response['data']['channels']);
- $this->assertContains('documents.' . $data['documentId'], $response['data']['channels']);
- $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
- $this->assertEquals('database.documents.update', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $this->assertEquals($response['data']['payload']['name'], 'Chris Evans 2');
-
-
- /**
- * Test Document Delete
- */
- $document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'documentId' => 'unique()',
- 'data' => [
- 'name' => 'Bradley Cooper'
- ],
- 'read' => ['role:all'],
- 'write' => ['role:all'],
- ]);
-
- $client->receive();
-
- $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/documents/' . $document['body']['$id'], array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()));
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(3, $response['data']['channels']);
- $this->assertContains('documents', $response['data']['channels']);
- $this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']);
- $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
- $this->assertEquals('database.documents.delete', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
- $this->assertEquals($response['data']['payload']['name'], 'Bradley Cooper');
-
- $client->close();
- }
-
- public function testChannelFiles()
- {
- $user = $this->getUser();
- $session = $user['session'] ?? '';
- $projectId = $this->getProject()['$id'];
-
- $client = $this->getWebsocket(['files'], [
- 'origin' => 'http://localhost',
- 'cookie' => 'a_session_'.$projectId.'=' . $session
- ]);
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(1, $response['data']['channels']);
- $this->assertContains('files', $response['data']['channels']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertEquals($user['$id'], $response['data']['user']['$id']);
-
- /**
- * Test File Create
- */
- $file = $this->client->call(Client::METHOD_POST, '/storage/files', array_merge([
- 'content-type' => 'multipart/form-data',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'fileId' => 'unique()',
- 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
- 'read' => ['role:all'],
- 'write' => ['role:all'],
- 'folderId' => 'xyz',
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('files', $response['data']['channels']);
- $this->assertContains('files.' . $file['body']['$id'], $response['data']['channels']);
- $this->assertEquals('storage.files.create', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $data = ['fileId' => $file['body']['$id']];
-
- /**
- * Test File Update
- */
- $this->client->call(Client::METHOD_PUT, '/storage/files/' . $data['fileId'], array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'read' => ['role:all'],
- 'write' => ['role:all'],
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('files', $response['data']['channels']);
- $this->assertContains('files.' . $file['body']['$id'], $response['data']['channels']);
- $this->assertEquals('storage.files.update', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- /**
- * Test File Delete
- */
- $this->client->call(Client::METHOD_DELETE, '/storage/files/' . $data['fileId'], array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()));
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('files', $response['data']['channels']);
- $this->assertContains('files.' . $file['body']['$id'], $response['data']['channels']);
- $this->assertEquals('storage.files.delete', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $client->close();
- }
-
- public function testChannelExecutions()
- {
- $user = $this->getUser();
- $session = $user['session'] ?? '';
- $projectId = $this->getProject()['$id'];
-
- $client = $this->getWebsocket(['executions'], [
- 'origin' => 'http://localhost',
- 'cookie' => 'a_session_'.$projectId.'=' . $session
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(1, $response['data']['channels']);
- $this->assertContains('executions', $response['data']['channels']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertEquals($user['$id'], $response['data']['user']['$id']);
-
- /**
- * Test Functions Create
- */
- $function = $this->client->call(Client::METHOD_POST, '/functions', [
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ], [
- 'functionId' => 'unique()',
- 'name' => 'Test',
- 'execute' => ['role:member'],
- 'runtime' => 'php-8.0',
- 'timeout' => 10,
- ]);
-
- $functionId = $function['body']['$id'] ?? '';
-
- $this->assertEquals($function['headers']['status-code'], 201);
- $this->assertNotEmpty($function['body']['$id']);
-
- $tag = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/tags', array_merge([
- 'content-type' => 'multipart/form-data',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'command' => 'php index.php',
- 'code' => new CURLFile(realpath(__DIR__ . '/../../../resources/functions/timeout.tar.gz'), 'application/x-gzip', 'php-fx.tar.gz'),
- ]);
-
- $tagId = $tag['body']['$id'] ?? '';
-
- $this->assertEquals($tag['headers']['status-code'], 201);
- $this->assertNotEmpty($tag['body']['$id']);
-
- $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/tag', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey']
- ]), [
- 'tag' => $tagId,
- ]);
-
- $this->assertEquals($response['headers']['status-code'], 200);
- $this->assertNotEmpty($response['body']['$id']);
-
- $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id']
- ], $this->getHeaders()), []);
-
- $this->assertEquals($execution['headers']['status-code'], 201);
- $this->assertNotEmpty($execution['body']['$id']);
-
- $response = json_decode($client->receive(), true);
- $responseUpdate = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(3, $response['data']['channels']);
- $this->assertContains('executions', $response['data']['channels']);
- $this->assertContains('executions.' . $execution['body']['$id'], $response['data']['channels']);
- $this->assertContains('functions.' . $execution['body']['functionId'], $response['data']['channels']);
- $this->assertEquals('functions.executions.create', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $this->assertArrayHasKey('type', $responseUpdate);
- $this->assertArrayHasKey('data', $responseUpdate);
- $this->assertEquals('event', $responseUpdate['type']);
- $this->assertNotEmpty($responseUpdate['data']);
- $this->assertArrayHasKey('timestamp', $responseUpdate['data']);
- $this->assertCount(3, $responseUpdate['data']['channels']);
- $this->assertContains('executions', $responseUpdate['data']['channels']);
- $this->assertContains('executions.' . $execution['body']['$id'], $responseUpdate['data']['channels']);
- $this->assertContains('functions.' . $execution['body']['functionId'], $responseUpdate['data']['channels']);
- $this->assertEquals('functions.executions.update', $responseUpdate['data']['event']);
- $this->assertNotEmpty($responseUpdate['data']['payload']);
-
- $client->close();
- }
-
- public function testChannelTeams(): array
- {
- $user = $this->getUser();
- $session = $user['session'] ?? '';
- $projectId = $this->getProject()['$id'];
-
- $client = $this->getWebsocket(['teams'], [
- 'origin' => 'http://localhost',
- 'cookie' => 'a_session_'.$projectId.'=' . $session
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(1, $response['data']['channels']);
- $this->assertContains('teams', $response['data']['channels']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertEquals($user['$id'], $response['data']['user']['$id']);
-
- /**
- * Test Team Create
- */
- $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- ], $this->getHeaders()), [
- 'teamId' => 'unique()',
- 'name' => 'Arsenal'
- ]);
-
- $teamId = $team['body']['$id'] ?? '';
-
- $this->assertEquals(201, $team['headers']['status-code']);
- $this->assertNotEmpty($team['body']['$id']);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('teams', $response['data']['channels']);
- $this->assertContains('teams.' . $teamId, $response['data']['channels']);
- $this->assertEquals('teams.create', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- /**
- * Test Team Update
- */
- $team = $this->client->call(Client::METHOD_PUT, '/teams/'.$teamId, array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $projectId,
- ], $this->getHeaders()), [
- 'name' => 'Manchester'
- ]);
-
- $this->assertEquals($team['headers']['status-code'], 200);
- $this->assertNotEmpty($team['body']['$id']);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('teams', $response['data']['channels']);
- $this->assertContains('teams.' . $teamId, $response['data']['channels']);
- $this->assertEquals('teams.update', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $client->close();
-
- return ['teamId' => $teamId];
- }
-
- /**
- * @depends testChannelTeams
- */
- public function testChannelMemberships(array $data)
- {
- $teamId = $data['teamId'] ?? '';
-
- $user = $this->getUser();
- $session = $user['session'] ?? '';
- $projectId = $this->getProject()['$id'];
-
- $client = $this->getWebsocket(['memberships'], [
- 'origin' => 'http://localhost',
- 'cookie' => 'a_session_'.$projectId.'='.$session
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('connected', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertCount(1, $response['data']['channels']);
- $this->assertContains('memberships', $response['data']['channels']);
- $this->assertNotEmpty($response['data']['user']);
- $this->assertEquals($user['$id'], $response['data']['user']['$id']);
-
- $response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamId.'/memberships', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()));
-
- $membershipId = $response['body']['memberships'][0]['$id'];
-
- /**
- * Test Update Membership
- */
- $roles = ['admin', 'editor', 'uncle'];
- $this->client->call(Client::METHOD_PATCH, '/teams/'.$teamId.'/memberships/'.$membershipId, array_merge([
- 'origin' => 'http://localhost',
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'roles' => $roles
- ]);
-
- $response = json_decode($client->receive(), true);
-
- $this->assertArrayHasKey('type', $response);
- $this->assertArrayHasKey('data', $response);
- $this->assertEquals('event', $response['type']);
- $this->assertNotEmpty($response['data']);
- $this->assertArrayHasKey('timestamp', $response['data']);
- $this->assertCount(2, $response['data']['channels']);
- $this->assertContains('memberships', $response['data']['channels']);
- $this->assertContains('memberships.' . $membershipId, $response['data']['channels']);
- $this->assertEquals('teams.memberships.update', $response['data']['event']);
- $this->assertNotEmpty($response['data']['payload']);
-
- $client->close();
- }
}
diff --git a/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php
new file mode 100644
index 0000000000..00c39c5d05
--- /dev/null
+++ b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php
@@ -0,0 +1,276 @@
+getUser();
+ $session = $user['session'] ?? '';
+ $projectId = 'console';
+
+ $client = $this->getWebsocket(['console'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_console='. $this->getRoot()['session'],
+ ], $projectId);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+
+ /**
+ * Test Attributes
+ */
+ $actors = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'collectionId' => 'unique()',
+ 'name' => 'Actors',
+ 'read' => ['role:all'],
+ 'write' => ['role:all'],
+ 'permission' => 'collection'
+ ]);
+
+ $data = ['actorsId' => $actors['body']['$id']];
+
+ $name = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'attributeId' => 'name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+
+ $this->assertEquals($name['headers']['status-code'], 201);
+ $this->assertEquals($name['body']['key'], 'name');
+ $this->assertEquals($name['body']['type'], 'string');
+ $this->assertEquals($name['body']['size'], 256);
+ $this->assertEquals($name['body']['required'], true);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertEquals('database.attributes.create', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals('processing', $response['data']['payload']['status']);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertEquals('database.attributes.update', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals('available', $response['data']['payload']['status']);
+
+ $client->close();
+
+ return $data;
+ }
+
+ /**
+ * @depends testAttributes
+ */
+ public function testIndexes(array $data)
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = 'console';
+
+ $client = $this->getWebsocket(['console'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_console='. $this->getRoot()['session'],
+ ], $projectId);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+
+ /**
+ * Test Indexes
+ */
+ $index = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/indexes', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'indexId' => 'key_name',
+ 'type' => 'key',
+ 'attributes' => [
+ 'name',
+ ],
+ ]);
+
+ $this->assertEquals($index['headers']['status-code'], 201);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertEquals('database.indexes.create', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals('processing', $response['data']['payload']['status']);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertEquals('database.indexes.update', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals('available', $response['data']['payload']['status']);
+
+ $client->close();
+
+ return $data;
+ }
+
+ /**
+ * @depends testIndexes
+ */
+ public function testDeleteIndex(array $data)
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = 'console';
+
+ $client = $this->getWebsocket(['console'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_console='. $this->getRoot()['session'],
+ ], $projectId);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+
+ /**
+ * Test Delete Index
+ */
+ $attribute = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/indexes/key_name', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals($attribute['headers']['status-code'], 204);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertEquals('database.indexes.delete', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $client->close();
+
+ return $data;
+ }
+
+ /**
+ * @depends testDeleteIndex
+ */
+ public function testDeleteAttribute(array $data)
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = 'console';
+
+ $client = $this->getWebsocket(['console'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_console='. $this->getRoot()['session'],
+ ], $projectId);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+
+ /**
+ * Test Delete Attribute
+ */
+ $attribute = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/attributes/name', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals($attribute['headers']['status-code'], 204);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('console', $response['data']['channels']);
+ $this->assertEquals('database.attributes.delete', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $client->close();
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php
index 8822c493b2..ed10aefa59 100644
--- a/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php
+++ b/tests/e2e/Services/Realtime/RealtimeCustomClientTest.php
@@ -2,9 +2,12 @@
namespace Tests\E2E\Services\Realtime;
+use CURLFile;
+use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
+use WebSocket\ConnectionException;
class RealtimeCustomClientTest extends Scope
@@ -12,4 +15,1038 @@ class RealtimeCustomClientTest extends Scope
use RealtimeBase;
use ProjectCustom;
use SideClient;
+
+ public function testChannelParsing()
+ {
+ $user = $this->getUser();
+ $userId = $user['$id'] ?? '';
+ $session = $user['session'] ?? '';
+
+ $headers = [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session
+ ];
+
+ $client = $this->getWebsocket(['documents'], $headers);
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertEquals($userId, $response['data']['user']['$id']);
+
+ $client->close();
+
+ $client = $this->getWebsocket(['account'], $headers);
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals($userId, $response['data']['user']['$id']);
+
+ $client->close();
+
+ $client = $this->getWebsocket(['account', 'documents', 'account.123'], $headers);
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertCount(3, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals($userId, $response['data']['user']['$id']);
+
+ $client->close();
+
+ $client = $this->getWebsocket([
+ 'account',
+ 'files',
+ 'files.1',
+ 'collections',
+ 'collections.1',
+ 'collections.1.documents',
+ 'collections.2',
+ 'collections.2.documents',
+ 'documents',
+ 'documents.1',
+ 'documents.2',
+ ], $headers);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertCount(12, $response['data']['channels']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertContains('files', $response['data']['channels']);
+ $this->assertContains('files.1', $response['data']['channels']);
+ $this->assertContains('collections', $response['data']['channels']);
+ $this->assertContains('collections.1', $response['data']['channels']);
+ $this->assertContains('collections.1.documents', $response['data']['channels']);
+ $this->assertContains('collections.2', $response['data']['channels']);
+ $this->assertContains('collections.2.documents', $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('documents.1', $response['data']['channels']);
+ $this->assertContains('documents.2', $response['data']['channels']);
+ $this->assertEquals($userId, $response['data']['user']['$id']);
+
+ $client->close();
+ }
+
+ public function testManualAuthentication()
+ {
+ $user = $this->getUser();
+ $userId = $user['$id'] ?? '';
+ $session = $user['session'] ?? '';
+
+ /**
+ * Test for SUCCESS
+ */
+ $client = $this->getWebsocket(['account'], [
+ 'origin' => 'http://localhost'
+ ]);
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('account', $response['data']['channels']);
+
+ $client->send(\json_encode([
+ 'type' => 'authentication',
+ 'data' => [
+ 'session' => $session
+ ]
+ ]));
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('response', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertEquals('authentication', $response['data']['to']);
+ $this->assertTrue($response['data']['success']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($userId, $response['data']['user']['$id']);
+
+ /**
+ * Test for FAILURE
+ */
+ $client->send(\json_encode([
+ 'type' => 'authentication',
+ 'data' => [
+ 'session' => 'invalid_session'
+ ]
+ ]));
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('error', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertEquals(1003, $response['data']['code']);
+ $this->assertEquals('Session is not valid.', $response['data']['message']);
+
+ $client->send(\json_encode([
+ 'type' => 'authentication',
+ 'data' => []
+ ]));
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('error', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertEquals(1003, $response['data']['code']);
+ $this->assertEquals('Payload is not valid.', $response['data']['message']);
+
+ $client->send(\json_encode([
+ 'type' => 'unknown',
+ 'data' => [
+ 'session' => 'invalid_session'
+ ]
+ ]));
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('error', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertEquals(1003, $response['data']['code']);
+ $this->assertEquals('Message type is not valid.', $response['data']['message']);
+
+ $client->send(\json_encode([
+ 'test' => '123',
+ ]));
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('error', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertEquals(1003, $response['data']['code']);
+ $this->assertEquals('Message format is not valid.', $response['data']['message']);
+
+
+ $client->close();
+ }
+
+ public function testConnectionPlatform()
+ {
+ /**
+ * Test for FAILURE
+ */
+ $client = $this->getWebsocket(['documents'], ['origin' => 'http://appwrite.unknown']);
+ $payload = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $payload);
+ $this->assertArrayHasKey('data', $payload);
+ $this->assertEquals('error', $payload['type']);
+ $this->assertEquals(1008, $payload['data']['code']);
+ $this->assertEquals('Invalid Origin. Register your new client (appwrite.unknown) as a new Web platform on your project console dashboard', $payload['data']['message']);
+ $this->expectException(ConnectionException::class); // Check if server disconnnected client
+ $client->close();
+ }
+
+ public function testChannelAccount()
+ {
+ $user = $this->getUser();
+ $userId = $user['$id'] ?? '';
+ $session = $user['session'] ?? '';
+ $projectId = $this->getProject()['$id'];
+
+ $client = $this->getWebsocket(['account'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_'.$projectId.'=' . $session
+ ]);
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($userId, $response['data']['user']['$id']);
+
+ /**
+ * Test Account Name Event
+ */
+ $name = "Torsten Dittmann";
+
+ $this->client->call(Client::METHOD_PATCH, '/account/name', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'cookie' => 'a_session_' . $projectId . '=' . $session,
+ ]), [
+ 'name' => $name
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.update.name', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $this->assertEquals($name, $response['data']['payload']['name']);
+
+
+ /**
+ * Test Account Password Event
+ */
+ $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'cookie' => 'a_session_'.$projectId.'=' . $session,
+ ]), [
+ 'password' => 'new-password',
+ 'oldPassword' => 'password',
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.update.password', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $this->assertEquals($name, $response['data']['payload']['name']);
+
+ /**
+ * Test Account Email Update
+ */
+ $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'cookie' => 'a_session_'.$projectId.'=' . $session,
+ ]), [
+ 'email' => 'torsten@appwrite.io',
+ 'password' => 'new-password',
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.update.email', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $this->assertEquals('torsten@appwrite.io', $response['data']['payload']['email']);
+
+ /**
+ * Test Account Verification Create
+ */
+ $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'cookie' => 'a_session_'.$projectId.'=' . $session,
+ ]), [
+ 'url' => 'http://localhost/verification',
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.verification.create', $response['data']['event']);
+
+ $lastEmail = $this->getLastEmail();
+ $verification = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
+
+ /**
+ * Test Account Verification Complete
+ */
+ $response = $this->client->call(Client::METHOD_PUT, '/account/verification', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'cookie' => 'a_session_'.$projectId.'=' . $session,
+ ]), [
+ 'userId' => $userId,
+ 'secret' => $verification,
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.verification.update', $response['data']['event']);
+
+ /**
+ * Test Acoount Prefs Update
+ */
+ $this->client->call(Client::METHOD_PATCH, '/account/prefs', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'cookie' => 'a_session_'.$projectId.'=' . $session,
+ ]), [
+ 'prefs' => [
+ 'prefKey1' => 'prefValue1',
+ 'prefKey2' => 'prefValue2',
+ ]
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.update.prefs', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ /**
+ * Test Account Session Create
+ */
+ $response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ]), [
+ 'email' => 'torsten@appwrite.io',
+ 'password' => 'new-password',
+ ]);
+
+ $sessionNew = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$projectId];
+ $sessionNewId = $response['body']['$id'];
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.sessions.create', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ /**
+ * Test Account Session Delete
+ */
+ $this->client->call(Client::METHOD_DELETE, '/account/sessions/'.$sessionNewId, array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'cookie' => 'a_session_'.$projectId.'=' . $sessionNew,
+ ]));
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.sessions.delete', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ /**
+ * Test Account Create Recovery
+ */
+ $this->client->call(Client::METHOD_POST, '/account/recovery', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ]), [
+ 'email' => 'torsten@appwrite.io',
+ 'url' => 'http://localhost/recovery',
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $lastEmail = $this->getLastEmail();
+ $recovery = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.recovery.create', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $response = $this->client->call(Client::METHOD_PUT, '/account/recovery', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ]), [
+ 'userId' => $userId,
+ 'secret' => $recovery,
+ 'password' => 'test-recovery',
+ 'passwordAgain' => 'test-recovery',
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertContains('account', $response['data']['channels']);
+ $this->assertContains('account.' . $userId, $response['data']['channels']);
+ $this->assertEquals('account.recovery.update', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $client->close();
+ }
+
+ public function testChannelDatabase()
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = $this->getProject()['$id'];
+
+ $client = $this->getWebsocket(['documents', 'collections'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_'.$projectId.'=' . $session
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('collections', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($user['$id'], $response['data']['user']['$id']);
+
+ /**
+ * Test Collection Create
+ */
+ $actors = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'collectionId' => 'unique()',
+ 'name' => 'Actors',
+ 'read' => ['role:all'],
+ 'write' => ['role:all'],
+ 'permission' => 'collection'
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('collections', $response['data']['channels']);
+ $this->assertContains('collections.' . $actors['body']['$id'], $response['data']['channels']);
+ $this->assertEquals('database.collections.create', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $data = ['actorsId' => $actors['body']['$id']];
+
+ $name = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'attributeId' => 'name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+
+ $this->assertEquals($name['headers']['status-code'], 201);
+ $this->assertEquals($name['body']['key'], 'name');
+ $this->assertEquals($name['body']['type'], 'string');
+ $this->assertEquals($name['body']['size'], 256);
+ $this->assertEquals($name['body']['required'], true);
+
+ sleep(2);
+
+ /**
+ * Test Document Create
+ */
+ $document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => 'unique()',
+ 'data' => [
+ 'name' => 'Chris Evans'
+ ],
+ 'read' => ['role:all'],
+ 'write' => ['role:all'],
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(3, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']);
+ $this->assertContains('collections.' . $actors['body']['$id'] . '.documents', $response['data']['channels']);
+ $this->assertEquals('database.documents.create', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals($response['data']['payload']['name'], 'Chris Evans');
+
+ $data['documentId'] = $document['body']['$id'];
+
+ /**
+ * Test Document Update
+ */
+ $document = $this->client->call(Client::METHOD_PATCH, '/database/collections/' . $data['actorsId'] . '/documents/' . $data['documentId'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => 'unique()',
+ 'data' => [
+ 'name' => 'Chris Evans 2'
+ ],
+ 'read' => ['role:all'],
+ 'write' => ['role:all'],
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(3, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('documents.' . $data['documentId'], $response['data']['channels']);
+ $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
+ $this->assertEquals('database.documents.update', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $this->assertEquals($response['data']['payload']['name'], 'Chris Evans 2');
+
+
+ /**
+ * Test Document Delete
+ */
+ $document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'documentId' => 'unique()',
+ 'data' => [
+ 'name' => 'Bradley Cooper'
+ ],
+ 'read' => ['role:all'],
+ 'write' => ['role:all'],
+ ]);
+
+ $client->receive();
+
+ $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/documents/' . $document['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(3, $response['data']['channels']);
+ $this->assertContains('documents', $response['data']['channels']);
+ $this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']);
+ $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
+ $this->assertEquals('database.documents.delete', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+ $this->assertEquals($response['data']['payload']['name'], 'Bradley Cooper');
+
+ $client->close();
+ }
+
+ public function testChannelFiles()
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = $this->getProject()['$id'];
+
+ $client = $this->getWebsocket(['files'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_'.$projectId.'=' . $session
+ ]);
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('files', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($user['$id'], $response['data']['user']['$id']);
+
+ /**
+ * Test File Create
+ */
+ $file = $this->client->call(Client::METHOD_POST, '/storage/files', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'fileId' => 'unique()',
+ 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
+ 'read' => ['role:all'],
+ 'write' => ['role:all'],
+ 'folderId' => 'xyz',
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('files', $response['data']['channels']);
+ $this->assertContains('files.' . $file['body']['$id'], $response['data']['channels']);
+ $this->assertEquals('storage.files.create', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $data = ['fileId' => $file['body']['$id']];
+
+ /**
+ * Test File Update
+ */
+ $this->client->call(Client::METHOD_PUT, '/storage/files/' . $data['fileId'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'read' => ['role:all'],
+ 'write' => ['role:all'],
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('files', $response['data']['channels']);
+ $this->assertContains('files.' . $file['body']['$id'], $response['data']['channels']);
+ $this->assertEquals('storage.files.update', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ /**
+ * Test File Delete
+ */
+ $this->client->call(Client::METHOD_DELETE, '/storage/files/' . $data['fileId'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('files', $response['data']['channels']);
+ $this->assertContains('files.' . $file['body']['$id'], $response['data']['channels']);
+ $this->assertEquals('storage.files.delete', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $client->close();
+ }
+
+ public function testChannelExecutions()
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = $this->getProject()['$id'];
+
+ $client = $this->getWebsocket(['executions'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_'.$projectId.'=' . $session
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('executions', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($user['$id'], $response['data']['user']['$id']);
+
+ /**
+ * Test Functions Create
+ */
+ $function = $this->client->call(Client::METHOD_POST, '/functions', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'functionId' => 'unique()',
+ 'name' => 'Test',
+ 'execute' => ['role:member'],
+ 'runtime' => 'php-8.0',
+ 'timeout' => 10,
+ ]);
+
+ $functionId = $function['body']['$id'] ?? '';
+
+ $this->assertEquals($function['headers']['status-code'], 201);
+ $this->assertNotEmpty($function['body']['$id']);
+
+ $tag = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/tags', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'command' => 'php index.php',
+ 'code' => new CURLFile(realpath(__DIR__ . '/../../../resources/functions/timeout.tar.gz'), 'application/x-gzip', 'php-fx.tar.gz'),
+ ]);
+
+ $tagId = $tag['body']['$id'] ?? '';
+
+ $this->assertEquals($tag['headers']['status-code'], 201);
+ $this->assertNotEmpty($tag['body']['$id']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/tag', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ]), [
+ 'tag' => $tagId,
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 200);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id']
+ ], $this->getHeaders()), []);
+
+ $this->assertEquals($execution['headers']['status-code'], 201);
+ $this->assertNotEmpty($execution['body']['$id']);
+
+ $response = json_decode($client->receive(), true);
+ $responseUpdate = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(3, $response['data']['channels']);
+ $this->assertContains('executions', $response['data']['channels']);
+ $this->assertContains('executions.' . $execution['body']['$id'], $response['data']['channels']);
+ $this->assertContains('functions.' . $execution['body']['functionId'], $response['data']['channels']);
+ $this->assertEquals('functions.executions.create', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $this->assertArrayHasKey('type', $responseUpdate);
+ $this->assertArrayHasKey('data', $responseUpdate);
+ $this->assertEquals('event', $responseUpdate['type']);
+ $this->assertNotEmpty($responseUpdate['data']);
+ $this->assertArrayHasKey('timestamp', $responseUpdate['data']);
+ $this->assertCount(3, $responseUpdate['data']['channels']);
+ $this->assertContains('executions', $responseUpdate['data']['channels']);
+ $this->assertContains('executions.' . $execution['body']['$id'], $responseUpdate['data']['channels']);
+ $this->assertContains('functions.' . $execution['body']['functionId'], $responseUpdate['data']['channels']);
+ $this->assertEquals('functions.executions.update', $responseUpdate['data']['event']);
+ $this->assertNotEmpty($responseUpdate['data']['payload']);
+
+ $client->close();
+ }
+
+ public function testChannelTeams(): array
+ {
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = $this->getProject()['$id'];
+
+ $client = $this->getWebsocket(['teams'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_'.$projectId.'=' . $session
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('teams', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($user['$id'], $response['data']['user']['$id']);
+
+ /**
+ * Test Team Create
+ */
+ $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), [
+ 'teamId' => 'unique()',
+ 'name' => 'Arsenal'
+ ]);
+
+ $teamId = $team['body']['$id'] ?? '';
+
+ $this->assertEquals(201, $team['headers']['status-code']);
+ $this->assertNotEmpty($team['body']['$id']);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('teams', $response['data']['channels']);
+ $this->assertContains('teams.' . $teamId, $response['data']['channels']);
+ $this->assertEquals('teams.create', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ /**
+ * Test Team Update
+ */
+ $team = $this->client->call(Client::METHOD_PUT, '/teams/'.$teamId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), [
+ 'name' => 'Manchester'
+ ]);
+
+ $this->assertEquals($team['headers']['status-code'], 200);
+ $this->assertNotEmpty($team['body']['$id']);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('teams', $response['data']['channels']);
+ $this->assertContains('teams.' . $teamId, $response['data']['channels']);
+ $this->assertEquals('teams.update', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $client->close();
+
+ return ['teamId' => $teamId];
+ }
+
+ /**
+ * @depends testChannelTeams
+ */
+ public function testChannelMemberships(array $data)
+ {
+ $teamId = $data['teamId'] ?? '';
+
+ $user = $this->getUser();
+ $session = $user['session'] ?? '';
+ $projectId = $this->getProject()['$id'];
+
+ $client = $this->getWebsocket(['memberships'], [
+ 'origin' => 'http://localhost',
+ 'cookie' => 'a_session_'.$projectId.'='.$session
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('connected', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertCount(1, $response['data']['channels']);
+ $this->assertContains('memberships', $response['data']['channels']);
+ $this->assertNotEmpty($response['data']['user']);
+ $this->assertEquals($user['$id'], $response['data']['user']['$id']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamId.'/memberships', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $membershipId = $response['body']['memberships'][0]['$id'];
+
+ /**
+ * Test Update Membership
+ */
+ $roles = ['admin', 'editor', 'uncle'];
+ $this->client->call(Client::METHOD_PATCH, '/teams/'.$teamId.'/memberships/'.$membershipId, array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'roles' => $roles
+ ]);
+
+ $response = json_decode($client->receive(), true);
+
+ $this->assertArrayHasKey('type', $response);
+ $this->assertArrayHasKey('data', $response);
+ $this->assertEquals('event', $response['type']);
+ $this->assertNotEmpty($response['data']);
+ $this->assertArrayHasKey('timestamp', $response['data']);
+ $this->assertCount(2, $response['data']['channels']);
+ $this->assertContains('memberships', $response['data']['channels']);
+ $this->assertContains('memberships.' . $membershipId, $response['data']['channels']);
+ $this->assertEquals('teams.memberships.update', $response['data']['event']);
+ $this->assertNotEmpty($response['data']['payload']);
+
+ $client->close();
+ }
}
\ No newline at end of file
From 277d397a41ee33b3d3e2d677313b39bb689f8a6e Mon Sep 17 00:00:00 2001
From: Matej Baco
Date: Mon, 6 Dec 2021 16:03:11 +0100
Subject: [PATCH 021/107] Attempt to fix tests
---
tests/e2e/Services/Locale/LocaleBase.php | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/tests/e2e/Services/Locale/LocaleBase.php b/tests/e2e/Services/Locale/LocaleBase.php
index 5624571c41..98df97a706 100644
--- a/tests/e2e/Services/Locale/LocaleBase.php
+++ b/tests/e2e/Services/Locale/LocaleBase.php
@@ -205,15 +205,15 @@ trait LocaleBase
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertIsArray($response['body']);
- $this->assertEquals(185, $response['body']['sum']);
+ $this->assertEquals(184, $response['body']['sum']);
$this->assertEquals($response['body']['languages'][0]['code'], 'aa');
$this->assertEquals($response['body']['languages'][0]['name'], 'Afar');
$this->assertEquals($response['body']['languages'][0]['nativeName'], 'Afar');
- $this->assertEquals($response['body']['languages'][184]['code'], 'zu');
- $this->assertEquals($response['body']['languages'][184]['name'], 'Zulu');
- $this->assertEquals($response['body']['languages'][184]['nativeName'], 'isiZulu');
+ $this->assertEquals($response['body']['languages'][183]['code'], 'zu');
+ $this->assertEquals($response['body']['languages'][183]['name'], 'Zulu');
+ $this->assertEquals($response['body']['languages'][183]['nativeName'], 'isiZulu');
/**
* Test for FAILURE
From 27b7b73b929d174d41601c78f88b40494e8969ab Mon Sep 17 00:00:00 2001
From: "Eldad A. Fux"
Date: Tue, 7 Dec 2021 18:27:56 +0200
Subject: [PATCH 022/107] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index e71cbf57e9..5d2652cf19 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
[](https://hacktoberfest.appwrite.io)
-[](https://appwrite.io/discord)
+[](https://appwrite.io/discord?r=Github)
[](https://hub.docker.com/r/appwrite/appwrite)
[](https://travis-ci.com/appwrite/appwrite)
[](https://twitter.com/appwrite_io)
From 34f6aea63bd1508ccac697a48e7b38fccb4e2b47 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Wed, 8 Dec 2021 23:16:45 +0100
Subject: [PATCH 023/107] Update app/controllers/api/teams.php
Co-authored-by: kodumbeats
---
app/controllers/api/teams.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index f822a81fed..bcf081eedc 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -113,7 +113,7 @@ App::get('/v1/teams')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM_LIST)
- ->param('search', '', new Text(256), 'Enter any text to search. Max length: 256 chars.', true)
+ ->param('search', '', new Text(256), 'Search term to filter results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Limit how many results will be returned. Returns up to 25 results by default. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 2000), 'Use this value to manage pagination. The default value is 0.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
From 9fb460396c29c5d979f2896b559ad8917535e2e3 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Wed, 8 Dec 2021 23:22:52 +0100
Subject: [PATCH 024/107] Fixed inconsistency
---
docs/references/teams/delete-team.md | 2 +-
docs/references/teams/get-team-members.md | 2 +-
docs/references/teams/get-team.md | 2 +-
docs/references/teams/list-teams.md | 2 +-
docs/references/teams/update-team.md | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/references/teams/delete-team.md b/docs/references/teams/delete-team.md
index 6b73736e9a..19dfe40969 100644
--- a/docs/references/teams/delete-team.md
+++ b/docs/references/teams/delete-team.md
@@ -1 +1 @@
-Delete a team using its unique ID. Only members with the owner role can delete the team.
\ No newline at end of file
+Use this endpoint to delete a team using its unique ID. Only team members with the owner role can delete the team.
\ No newline at end of file
diff --git a/docs/references/teams/get-team-members.md b/docs/references/teams/get-team-members.md
index d18b2b358e..e4de1c9788 100644
--- a/docs/references/teams/get-team-members.md
+++ b/docs/references/teams/get-team-members.md
@@ -1 +1 @@
-Lists a team's members using the team's unique ID. All team members have read access to this endpoint.
\ No newline at end of file
+Use this endpoint to list a team's members using the team's unique ID. All team members have read access to this endpoint.
\ No newline at end of file
diff --git a/docs/references/teams/get-team.md b/docs/references/teams/get-team.md
index 800612ab15..5449bf2153 100644
--- a/docs/references/teams/get-team.md
+++ b/docs/references/teams/get-team.md
@@ -1 +1 @@
-Get a team by its unique ID. All team members have read access for this resource.
\ No newline at end of file
+Use this endpoint to get a team by its unique ID. All team members have read access for this resource.
\ No newline at end of file
diff --git a/docs/references/teams/list-teams.md b/docs/references/teams/list-teams.md
index feab2d45a4..68c3621cbd 100644
--- a/docs/references/teams/list-teams.md
+++ b/docs/references/teams/list-teams.md
@@ -1,3 +1,3 @@
-Get a list of all the teams in which the current user is a member. You can use the parameters to filter your results.
+Use this endpoint to get a list of all the teams in which the current user is a member. You can use the parameters to filter your results.
On admin mode, this endpoint returns a list of all the teams in the current project. [Learn more about different API modes](/docs/admin).
\ No newline at end of file
diff --git a/docs/references/teams/update-team.md b/docs/references/teams/update-team.md
index 0acb528e1c..aa88d0e242 100644
--- a/docs/references/teams/update-team.md
+++ b/docs/references/teams/update-team.md
@@ -1 +1 @@
-Update a team using its unique ID. Only members with the owner role can update the team.
\ No newline at end of file
+Use this endpoint to update a team using its unique ID. Only members with the owner role can update the team.
\ No newline at end of file
From 43ef3c060b5c3804c018fdca858a2e4dcd10c0e3 Mon Sep 17 00:00:00 2001
From: Christy Jacob
Date: Thu, 9 Dec 2021 17:02:12 +0400
Subject: [PATCH 025/107] feat: add new endpoint
---
app/controllers/api/functions.php | 29 ++++
composer.lock | 155 +++++++++++++-----
docs/references/functions/list-runtimes.md | 1 +
src/Appwrite/Utopia/Response.php | 5 +
.../Utopia/Response/Model/Runtime.php | 78 +++++++++
.../Functions/FunctionsCustomServerTest.php | 23 +++
6 files changed, 250 insertions(+), 41 deletions(-)
create mode 100644 docs/references/functions/list-runtimes.md
create mode 100644 src/Appwrite/Utopia/Response/Model/Runtime.php
diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php
index 480b1223d9..7d7d3537be 100644
--- a/app/controllers/api/functions.php
+++ b/app/controllers/api/functions.php
@@ -119,6 +119,35 @@ App::get('/v1/functions')
]), Response::MODEL_FUNCTION_LIST);
});
+App::get('/v1/functions/runtimes')
+ ->groups(['api', 'functions'])
+ ->desc('List the currently active function runtimes.')
+ ->label('scope', 'functions.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'functions')
+ ->label('sdk.method', 'listRuntimes')
+ ->label('sdk.description', '/docs/references/functions/list-runtimes.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_RUNTIME_LIST)
+ ->inject('response')
+ ->action(function ($response) {
+ /** @var Appwrite\Utopia\Response $response */
+ /** @var Utopia\Database\Database $dbForInternal */
+
+ $runtimes = Config::getParam('runtimes');
+
+ $runtimes = array_map(function ($key) use ($runtimes) {
+ $runtimes[$key]['$id'] = $key;
+ return $runtimes[$key];
+ }, array_keys($runtimes));
+
+ $response->dynamic(new Document([
+ 'sum' => count($runtimes),
+ 'runtimes' => $runtimes
+ ]), Response::MODEL_RUNTIME_LIST);
+ });
+
App::get('/v1/functions/:functionId')
->groups(['api', 'functions'])
->desc('Get Function')
diff --git a/composer.lock b/composer.lock
index 1b8f47e179..b541ac91de 100644
--- a/composer.lock
+++ b/composer.lock
@@ -489,16 +489,16 @@
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.4.0",
+ "version": "7.4.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "868b3571a039f0ebc11ac8f344f4080babe2cb94"
+ "reference": "ee0a041b1760e6a53d2a39c8c34115adc2af2c79"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/868b3571a039f0ebc11ac8f344f4080babe2cb94",
- "reference": "868b3571a039f0ebc11ac8f344f4080babe2cb94",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee0a041b1760e6a53d2a39c8c34115adc2af2c79",
+ "reference": "ee0a041b1760e6a53d2a39c8c34115adc2af2c79",
"shasum": ""
},
"require": {
@@ -507,7 +507,7 @@
"guzzlehttp/psr7": "^1.8.3 || ^2.1",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
- "symfony/deprecation-contracts": "^2.2"
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
@@ -593,7 +593,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.4.0"
+ "source": "https://github.com/guzzle/guzzle/tree/7.4.1"
},
"funding": [
{
@@ -609,7 +609,7 @@
"type": "tidelift"
}
],
- "time": "2021-10-18T09:52:00+00:00"
+ "time": "2021-12-06T18:43:05+00:00"
},
{
"name": "guzzlehttp/promises",
@@ -1591,25 +1591,25 @@
},
{
"name": "symfony/deprecation-contracts",
- "version": "v2.5.0",
+ "version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8"
+ "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8",
- "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/c726b64c1ccfe2896cb7df2e1331c357ad1c8ced",
+ "reference": "c726b64c1ccfe2896cb7df2e1331c357ad1c8ced",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=8.0.2"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "2.5-dev"
+ "dev-main": "3.0-dev"
},
"thanks": {
"name": "symfony/contracts",
@@ -1638,7 +1638,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.0.0"
},
"funding": [
{
@@ -1654,7 +1654,7 @@
"type": "tidelift"
}
],
- "time": "2021-07-12T14:48:14+00:00"
+ "time": "2021-11-01T23:48:49+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -3061,6 +3061,77 @@
},
"time": "2021-11-12T11:09:38+00:00"
},
+ {
+ "name": "composer/pcre",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/3d322d715c43a1ac36c7fe215fa59336265500f2",
+ "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "symfony/phpunit-bridge": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/1.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-12-06T15:17:27+00:00"
+ },
{
"name": "composer/semver",
"version": "3.2.6",
@@ -3144,25 +3215,27 @@
},
{
"name": "composer/xdebug-handler",
- "version": "2.0.2",
+ "version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/composer/xdebug-handler.git",
- "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339"
+ "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/84674dd3a7575ba617f5a76d7e9e29a7d3891339",
- "reference": "84674dd3a7575ba617f5a76d7e9e29a7d3891339",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6555461e76962fd0379c444c46fd558a0fcfb65e",
+ "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e",
"shasum": ""
},
"require": {
+ "composer/pcre": "^1",
"php": "^5.3.2 || ^7.0 || ^8.0",
"psr/log": "^1 || ^2 || ^3"
},
"require-dev": {
- "phpstan/phpstan": "^0.12.55",
- "symfony/phpunit-bridge": "^4.2 || ^5"
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0"
},
"type": "library",
"autoload": {
@@ -3188,7 +3261,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/xdebug-handler/issues",
- "source": "https://github.com/composer/xdebug-handler/tree/2.0.2"
+ "source": "https://github.com/composer/xdebug-handler/tree/2.0.3"
},
"funding": [
{
@@ -3204,7 +3277,7 @@
"type": "tidelift"
}
],
- "time": "2021-07-31T17:03:58+00:00"
+ "time": "2021-12-08T13:07:32+00:00"
},
{
"name": "dnoegel/php-xdg-base-dir",
@@ -4035,16 +4108,16 @@
},
{
"name": "phpspec/prophecy",
- "version": "1.14.0",
+ "version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e"
+ "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
- "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
+ "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
"shasum": ""
},
"require": {
@@ -4096,22 +4169,22 @@
],
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
- "source": "https://github.com/phpspec/prophecy/tree/1.14.0"
+ "source": "https://github.com/phpspec/prophecy/tree/v1.15.0"
},
- "time": "2021-09-10T09:02:12+00:00"
+ "time": "2021-12-08T12:19:24+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "9.2.9",
+ "version": "9.2.10",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "f301eb1453c9e7a1bc912ee8b0ea9db22c60223b"
+ "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f301eb1453c9e7a1bc912ee8b0ea9db22c60223b",
- "reference": "f301eb1453c9e7a1bc912ee8b0ea9db22c60223b",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d5850aaf931743067f4bfc1ae4cbd06468400687",
+ "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687",
"shasum": ""
},
"require": {
@@ -4167,7 +4240,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.9"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.10"
},
"funding": [
{
@@ -4175,20 +4248,20 @@
"type": "github"
}
],
- "time": "2021-11-19T15:21:02+00:00"
+ "time": "2021-12-05T09:12:13+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "3.0.5",
+ "version": "3.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8"
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8",
- "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
"shasum": ""
},
"require": {
@@ -4227,7 +4300,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
- "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5"
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
},
"funding": [
{
@@ -4235,7 +4308,7 @@
"type": "github"
}
],
- "time": "2020-09-28T05:57:25+00:00"
+ "time": "2021-12-02T12:48:52+00:00"
},
{
"name": "phpunit/php-invoker",
diff --git a/docs/references/functions/list-runtimes.md b/docs/references/functions/list-runtimes.md
new file mode 100644
index 0000000000..3ffb657911
--- /dev/null
+++ b/docs/references/functions/list-runtimes.md
@@ -0,0 +1 @@
+Get a list of all runtimes that are currently active in your project.
\ No newline at end of file
diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php
index badddf21a6..025e7d8e06 100644
--- a/src/Appwrite/Utopia/Response.php
+++ b/src/Appwrite/Utopia/Response.php
@@ -54,6 +54,7 @@ use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Webhook;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Mock; // Keep last
+use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
@@ -141,6 +142,8 @@ class Response extends SwooleResponse
// Functions
const MODEL_FUNCTION = 'function';
const MODEL_FUNCTION_LIST = 'functionList';
+ const MODEL_RUNTIME = 'runtime';
+ const MODEL_RUNTIME_LIST = 'runtimeList';
const MODEL_TAG = 'tag';
const MODEL_TAG_LIST = 'tagList';
const MODEL_EXECUTION = 'execution';
@@ -201,6 +204,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Teams List', self::MODEL_TEAM_LIST, 'teams', self::MODEL_TEAM))
->setModel(new BaseList('Memberships List', self::MODEL_MEMBERSHIP_LIST, 'memberships', self::MODEL_MEMBERSHIP))
->setModel(new BaseList('Functions List', self::MODEL_FUNCTION_LIST, 'functions', self::MODEL_FUNCTION))
+ ->setModel(new BaseList('Runtimes List', self::MODEL_RUNTIME_LIST, 'runtimes', self::MODEL_RUNTIME))
->setModel(new BaseList('Tags List', self::MODEL_TAG_LIST, 'tags', self::MODEL_TAG))
->setModel(new BaseList('Executions List', self::MODEL_EXECUTION_LIST, 'executions', self::MODEL_EXECUTION))
->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false))
@@ -239,6 +243,7 @@ class Response extends SwooleResponse
->setModel(new Team())
->setModel(new Membership())
->setModel(new Func())
+ ->setModel(new Runtime())
->setModel(new FuncPermissions())
->setModel(new Tag())
->setModel(new Execution())
diff --git a/src/Appwrite/Utopia/Response/Model/Runtime.php b/src/Appwrite/Utopia/Response/Model/Runtime.php
new file mode 100644
index 0000000000..2aae2f95ef
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Runtime.php
@@ -0,0 +1,78 @@
+addRule('$id', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Runtime ID.',
+ 'default' => '',
+ 'example' => 'python-3.8',
+ ])
+ ->addRule('name', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Runtime Name.',
+ 'default' => '',
+ 'example' => 'Python'
+ ])
+ ->addRule('version', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Runtime version.',
+ 'default' => '',
+ 'example' => '3.8',
+ ])
+ ->addRule('base', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Base Docker image used to build the runtime.',
+ 'default' => '',
+ 'example' => 'python:3.8-alpine',
+ ])
+ ->addRule('image', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Image name of Docker Hub.',
+ 'default' => '',
+ 'example' => 'appwrite\/runtime-for-python:3.8',
+ ])
+ ->addRule('logo', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Name of the logo image.',
+ 'default' => '',
+ 'example' => 'python.png',
+ ])
+ ->addRule('supports', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'List of supported architectures.',
+ 'default' => '',
+ 'example' => 'amd64',
+ 'array' => true,
+ ])
+ ;
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName():string
+ {
+ return 'Runtime';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType():string
+ {
+ return Response::MODEL_RUNTIME;
+ }
+}
diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
index 0291de1725..e8541a2c54 100644
--- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
+++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
@@ -795,4 +795,27 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($executions['body']['executions'][0]['trigger'], 'http');
$this->assertStringContainsString('foobar', $executions['body']['executions'][0]['stdout']);
}
+
+ public function testGetRuntimes()
+ {
+
+ $runtimes = $this->client->call(Client::METHOD_GET, '/functions/runtimes', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $runtimes['headers']['status-code']);
+ $this->assertEquals(12, $runtimes['body']['sum']);
+
+ $runtime = $runtimes['body']['runtimes'][0];
+
+ $this->assertArrayHasKey('$id', $runtime);
+ $this->assertArrayHasKey('name', $runtime);
+ $this->assertArrayHasKey('version', $runtime);
+ $this->assertArrayHasKey('logo', $runtime);
+ $this->assertArrayHasKey('image', $runtime);
+ $this->assertArrayHasKey('base', $runtime);
+ $this->assertArrayHasKey('supports', $runtime);
+
+ }
}
From 653dfb292b1eec606e8a8091248f0020d04ffe36 Mon Sep 17 00:00:00 2001
From: Christy Jacob
Date: Thu, 9 Dec 2021 19:53:37 +0400
Subject: [PATCH 026/107] feat: address review comments
---
app/controllers/api/functions.php | 1 -
tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php
index 7d7d3537be..76c9ef47ba 100644
--- a/app/controllers/api/functions.php
+++ b/app/controllers/api/functions.php
@@ -133,7 +133,6 @@ App::get('/v1/functions/runtimes')
->inject('response')
->action(function ($response) {
/** @var Appwrite\Utopia\Response $response */
- /** @var Utopia\Database\Database $dbForInternal */
$runtimes = Config::getParam('runtimes');
diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
index e8541a2c54..c2452c08bd 100644
--- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
+++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
@@ -805,7 +805,7 @@ class FunctionsCustomServerTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $runtimes['headers']['status-code']);
- $this->assertEquals(12, $runtimes['body']['sum']);
+ $this->assertGreaterThan(0, $runtimes['body']['sum']);
$runtime = $runtimes['body']['runtimes'][0];
From 0d60e826667f53a0033449df6b78b28b7a150ad9 Mon Sep 17 00:00:00 2001
From: Torsten Dittmann
Date: Fri, 10 Dec 2021 11:56:11 +0100
Subject: [PATCH 027/107] fix(database): permissions using an admin user
---
app/controllers/api/database.php | 38 ++++++++++++++++++--------------
1 file changed, 22 insertions(+), 16 deletions(-)
diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php
index d60d055912..8c4f2d20be 100644
--- a/app/controllers/api/database.php
+++ b/app/controllers/api/database.php
@@ -1640,16 +1640,19 @@ App::post('/v1/database/collections/:collectionId/documents')
$data['$read'] = (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? []; // By default set read permissions for user
$data['$write'] = (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? []; // By default set write permissions for user
- // Users can only add their roles to documents, API keys can add any
+ // Users can only add their roles to documents, API keys and Admin users can add any
$roles = \array_fill_keys(Authorization::getRoles(), true); // Auth::isAppUser expects roles to be keys, not values of assoc array
- foreach ($data['$read'] as $read) {
- if (!Auth::isAppUser($roles) && !Authorization::isRole($read)) {
- throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400);
+
+ if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
+ foreach ($data['$read'] as $read) {
+ if (!Authorization::isRole($read)) {
+ throw new Exception('Read permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ }
}
- }
- foreach ($data['$write'] as $write) {
- if (!Auth::isAppUser($roles) && !Authorization::isRole($write)) {
- throw new Exception('Write permissions must be one of: ('.\implode(', ', $roles).')', 400);
+ foreach ($data['$write'] as $write) {
+ if (!Authorization::isRole($write)) {
+ throw new Exception('Write permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ }
}
}
@@ -1998,16 +2001,19 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
$data['$read'] = (is_null($read)) ? ($document->getRead() ?? []) : $read; // By default inherit read permissions
$data['$write'] = (is_null($write)) ? ($document->getWrite() ?? []) : $write; // By default inherit write permissions
- // Users can only add their roles to documents, API keys can add any
+ // Users can only add their roles to documents, API keys and Admin users can add any
$roles = \array_fill_keys(Authorization::getRoles(), true); // Auth::isAppUser expects roles to be keys, not values of assoc array
- foreach ($data['$read'] as $read) {
- if (!Auth::isAppUser($roles) && !Authorization::isRole($read)) {
- throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400);
+
+ if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
+ foreach ($data['$read'] as $read) {
+ if (!Authorization::isRole($read)) {
+ throw new Exception('Read permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ }
}
- }
- foreach ($data['$write'] as $write) {
- if (!Auth::isAppUser($roles) && !Authorization::isRole($write)) {
- throw new Exception('Write permissions must be one of: ('.\implode(', ', $roles).')', 400);
+ foreach ($data['$write'] as $write) {
+ if (!Authorization::isRole($write)) {
+ throw new Exception('Write permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ }
}
}
From adffae7cf74f239db1d7f442a6ffcc65205eaf83 Mon Sep 17 00:00:00 2001
From: Torsten Dittmann
Date: Fri, 10 Dec 2021 13:27:11 +0100
Subject: [PATCH 028/107] docs(api): reviewed in-line docs of endpoints
---
app/controllers/api/account.php | 24 ++++----
app/controllers/api/database.php | 92 +++++++++++++++----------------
app/controllers/api/functions.php | 56 +++++++++----------
app/controllers/api/storage.php | 28 +++++-----
app/controllers/api/teams.php | 38 ++++++-------
app/controllers/api/users.php | 40 +++++++-------
6 files changed, 139 insertions(+), 139 deletions(-)
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index 5c86755813..e7f6eb508f 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -317,7 +317,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('scope', 'public')
->label('docs', false)
- ->param('projectId', '', new Text(1024), 'Project unique ID.')
+ ->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(1024), 'OAuth2 code.')
->param('state', '', new Text(2048), 'Login state params.', true)
@@ -344,7 +344,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('scope', 'public')
->label('origin', '*')
->label('docs', false)
- ->param('projectId', '', new Text(1024), 'Project unique ID.')
+ ->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(1024), 'OAuth2 code.')
->param('state', '', new Text(2048), 'Login state params.', true)
@@ -771,7 +771,7 @@ App::put('/v1/account/sessions/magic-url')
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{param-userId}')
- ->param('userId', '', new CustomId(), 'User unique ID.')
+ ->param('userId', '', new CustomId(), 'User ID.')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('request')
->inject('response')
@@ -1185,7 +1185,7 @@ App::get('/v1/account/logs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
- ->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->inject('response')
->inject('user')
@@ -1271,7 +1271,7 @@ App::get('/v1/account/sessions/:sessionId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
- ->param('sessionId', null, new UID(), 'Session unique ID. Use the string \'current\' to get the current device session.')
+ ->param('sessionId', null, new UID(), 'Session ID. Use the string \'current\' to get the current device session.')
->inject('response')
->inject('user')
->inject('locale')
@@ -1367,8 +1367,8 @@ App::patch('/v1/account/password')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
- ->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
- ->param('oldPassword', '', new Password(), 'Old user password. Must be at least 8 chars.', true)
+ ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
+ ->param('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true)
->inject('response')
->inject('user')
->inject('dbForInternal')
@@ -1585,7 +1585,7 @@ App::delete('/v1/account/sessions/:sessionId')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->label('abuse-limit', 100)
- ->param('sessionId', null, new UID(), 'Session unique ID. Use the string \'current\' to delete the current device session.')
+ ->param('sessionId', null, new UID(), 'Session ID. Use the string \'current\' to delete the current device session.')
->inject('request')
->inject('response')
->inject('user')
@@ -1867,10 +1867,10 @@ App::put('/v1/account/recovery')
->label('sdk.response.model', Response::MODEL_TOKEN)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{param-userId}')
- ->param('userId', '', new UID(), 'User account UID address.')
+ ->param('userId', '', new UID(), 'User ID.')
->param('secret', '', new Text(256), 'Valid reset token.')
- ->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
- ->param('passwordAgain', '', new Password(), 'New password again. Must be at least 8 chars.')
+ ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
+ ->param('passwordAgain', '', new Password(), 'Repeat new user password. Must be at least 8 chars.')
->inject('response')
->inject('dbForInternal')
->inject('audits')
@@ -2049,7 +2049,7 @@ App::put('/v1/account/verification')
->label('sdk.response.model', Response::MODEL_TOKEN)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{param-userId}')
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('response')
->inject('user')
diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php
index d60d055912..7bff2d9392 100644
--- a/app/controllers/api/database.php
+++ b/app/controllers/api/database.php
@@ -149,9 +149,9 @@ App::post('/v1/database/collections')
->label('sdk.response.model', Response::MODEL_COLLECTION)
->param('collectionId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
- ->param('permission', null, new WhiteList(['document', 'collection']), 'Permissions type model to use for reading documents in this collection. You can use collection-level permission set once on the collection using the `read` and `write` params, or you can set document-level permission where each document read and write params will decide who has access to read and write to each document individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
- ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
- ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
+ ->param('permission', null, new WhiteList(['document', 'collection']), 'Permissions type model to use for reading documents in this collection. You can use collection-level permission set once on the collection using the `read` and `write` params, or you can set document-level permission where each document read and write params will decide who has access to read and write to each document individually. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
+ ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
+ ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
@@ -206,8 +206,8 @@ App::get('/v1/database/collections')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_COLLECTION_LIST)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of collection to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the collection used as the starting point for the query, excluding the collection itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
@@ -251,7 +251,7 @@ App::get('/v1/database/collections/:collectionId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_COLLECTION)
- ->param('collectionId', '', new UID(), 'Collection unique ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -391,7 +391,7 @@ App::get('/v1/database/:collectionId/usage')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_COLLECTION)
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
- ->param('collectionId', '', new UID(), 'Collection unique ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
@@ -498,8 +498,8 @@ App::get('/v1/database/collections/:collectionId/logs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
- ->param('collectionId', '', new UID(), 'Collection unique ID.')
- ->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->inject('response')
->inject('dbForInternal')
@@ -600,11 +600,11 @@ App::put('/v1/database/collections/:collectionId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_COLLECTION)
- ->param('collectionId', '', new UID(), 'Collection unique ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
- ->param('permission', null, new WhiteList(['document', 'collection']), 'Permissions type model to use for reading documents in this collection. You can use collection-level permission set once on the collection using the `read` and `write` params, or you can set document-level permission where each document read and write params will decide who has access to read and write to each document individually. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
- ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
- ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
+ ->param('permission', null, new WhiteList(['document', 'collection']), 'Permissions type model to use for reading documents in this collection. You can use collection-level permission set once on the collection using the `read` and `write` params, or you can set document-level permission where each document read and write params will decide who has access to read and write to each document individually. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
+ ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
+ ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->inject('response')
->inject('dbForInternal')
->inject('audits')
@@ -663,7 +663,7 @@ App::delete('/v1/database/collections/:collectionId')
->label('sdk.description', '/docs/references/database/delete-collection.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('collectionId', '', new UID(), 'Collection unique ID.')
+ ->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
@@ -723,7 +723,7 @@ App::post('/v1/database/collections/:collectionId/attributes/string')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_STRING)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Range::TYPE_INTEGER), 'Attribute size for text attributes, in number of characters.')
->param('required', null, new Boolean(), 'Is attribute required?')
@@ -773,7 +773,7 @@ App::post('/v1/database/collections/:collectionId/attributes/email')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_EMAIL)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https:///docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Email(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
@@ -817,7 +817,7 @@ App::post('/v1/database/collections/:collectionId/attributes/enum')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_ENUM)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('elements', [], new ArrayList(new Text(0)), 'Array of elements in enumerated type. Uses length of longest element to determine size.')
->param('required', null, new Boolean(), 'Is attribute required?')
@@ -874,7 +874,7 @@ App::post('/v1/database/collections/:collectionId/attributes/ip')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_IP)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new IP(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
@@ -918,7 +918,7 @@ App::post('/v1/database/collections/:collectionId/attributes/url')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_URL)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new URL(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
@@ -961,7 +961,7 @@ App::post('/v1/database/collections/:collectionId/attributes/integer')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_INTEGER)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new Integer(), 'Minimum value to enforce on new documents', true)
@@ -1032,7 +1032,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_FLOAT)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents', true)
@@ -1103,7 +1103,7 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_BOOLEAN)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Boolean(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
@@ -1144,7 +1144,7 @@ App::get('/v1/database/collections/:collectionId/attributes')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_LIST)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -1187,7 +1187,7 @@ App::get('/v1/database/collections/:collectionId/attributes/:attributeId')
Response::MODEL_ATTRIBUTE_URL,
Response::MODEL_ATTRIBUTE_IP,
Response::MODEL_ATTRIBUTE_STRING,])// needs to be last, since its condition would dominate any other string attribute
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->inject('response')
->inject('dbForInternal')
@@ -1242,7 +1242,7 @@ App::delete('/v1/database/collections/:collectionId/attributes/:attributeId')
->label('sdk.description', '/docs/references/database/delete-attribute.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('attributeId', '', new Key(), 'Attribute ID.')
->inject('response')
->inject('dbForInternal')
@@ -1331,7 +1331,7 @@ App::post('/v1/database/collections/:collectionId/indexes')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_INDEX)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('indexId', null, new Key(), 'Index ID.')
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL, Database::INDEX_ARRAY]), 'Index type.')
->param('attributes', null, new ArrayList(new Key()), 'Array of attributes to index.')
@@ -1439,7 +1439,7 @@ App::get('/v1/database/collections/:collectionId/indexes')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_INDEX_LIST)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -1480,7 +1480,7 @@ App::get('/v1/database/collections/:collectionId/indexes/:indexId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_INDEX)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('indexId', null, new Key(), 'Index ID.')
->inject('response')
->inject('dbForInternal')
@@ -1524,7 +1524,7 @@ App::delete('/v1/database/collections/:collectionId/indexes/:indexId')
->label('sdk.description', '/docs/references/database/delete-index.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('indexId', '', new Key(), 'Index ID.')
->inject('response')
->inject('dbForInternal')
@@ -1592,11 +1592,11 @@ App::post('/v1/database/collections/:collectionId/documents')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOCUMENT)
- ->param('documentId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
- ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('documentId', '', new CustomId(), 'Document ID. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection with validation rules using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('data', [], new JSON(), 'Document data as JSON object.')
- ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
- ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
+ ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
+ ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
@@ -1696,9 +1696,9 @@ App::get('/v1/database/collections/:collectionId/documents')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOCUMENT_LIST)
- ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
+ ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('queries', [], new ArrayList(new Text(128)), 'Array of query strings.', true)
- ->param('limit', 25, new Range(0, 100), 'Maximum number of documents to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of documents to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the document used as the starting point for the query, excluding the document itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
@@ -1779,8 +1779,8 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOCUMENT)
- ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
- ->param('documentId', null, new UID(), 'Document unique ID.')
+ ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
+ ->param('documentId', null, new UID(), 'Document ID.')
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
@@ -1836,9 +1836,9 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId/logs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
- ->param('collectionId', '', new UID(), 'Collection unique ID.')
- ->param('documentId', null, new UID(), 'Document unique ID.')
- ->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('collectionId', '', new UID(), 'Collection ID.')
+ ->param('documentId', null, new UID(), 'Document ID.')
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->inject('response')
->inject('dbForInternal')
@@ -1944,11 +1944,11 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOCUMENT)
- ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).')
- ->param('documentId', null, new UID(), 'Document unique ID.')
+ ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection with validation rules using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
+ ->param('documentId', null, new UID(), 'Document ID.')
->param('data', [], new JSON(), 'Document data as JSON object.')
- ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
- ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
+ ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
+ ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
@@ -2056,8 +2056,8 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId')
->label('sdk.description', '/docs/references/database/delete-document.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).')
- ->param('documentId', null, new UID(), 'Document unique ID.')
+ ->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
+ ->param('documentId', null, new UID(), 'Document ID.')
->inject('response')
->inject('dbForInternal')
->inject('dbForExternal')
diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php
index 480b1223d9..16a2d42592 100644
--- a/app/controllers/api/functions.php
+++ b/app/controllers/api/functions.php
@@ -39,11 +39,11 @@ App::post('/v1/functions')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FUNCTION)
- ->param('functionId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('functionId', '', new CustomId(), 'Function ID. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
- ->param('execute', [], new ArrayList(new Text(64)), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
+ ->param('execute', [], new ArrayList(new Text(64)), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->param('runtime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Execution runtime.')
- ->param('vars', [], new Assoc(), 'Key-value JSON object.', true)
+ ->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, 900), 'Function maximum execution time in seconds.', true)
@@ -88,8 +88,8 @@ App::get('/v1/functions')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FUNCTION_LIST)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of functions to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the function used as the starting point for the query, excluding the function itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
@@ -130,7 +130,7 @@ App::get('/v1/functions/:functionId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FUNCTION)
- ->param('functionId', '', new UID(), 'Function unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($functionId, $response, $dbForInternal) {
@@ -156,7 +156,7 @@ App::get('/v1/functions/:functionId/usage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_FUNCTIONS)
- ->param('functionId', '', new UID(), 'Function unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d']), 'Date range.', true)
->inject('response')
->inject('project')
@@ -262,13 +262,13 @@ App::put('/v1/functions/:functionId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FUNCTION)
- ->param('functionId', '', new UID(), 'Function unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
->param('name', '', new Text(128), 'Function name. Max length: 128 chars.')
- ->param('execute', [], new ArrayList(new Text(64)), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
- ->param('vars', [], new Assoc(), 'Key-value JSON object.', true)
+ ->param('execute', [], new ArrayList(new Text(64)), 'An array of strings with execution permissions. By default no user is granted with any execute permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
+ ->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
- ->param('timeout', 15, new Range(1, 900), 'Function maximum execution time in seconds.', true)
+ ->param('timeout', 15, new Range(1, 900), 'Maximum execution time in seconds.', true)
->inject('response')
->inject('dbForInternal')
->inject('project')
@@ -324,8 +324,8 @@ App::patch('/v1/functions/:functionId/tag')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FUNCTION)
- ->param('functionId', '', new UID(), 'Function unique ID.')
- ->param('tag', '', new UID(), 'Tag unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
+ ->param('tag', '', new UID(), 'Tag ID.')
->inject('response')
->inject('dbForInternal')
->inject('project')
@@ -378,7 +378,7 @@ App::delete('/v1/functions/:functionId')
->label('sdk.description', '/docs/references/functions/delete-function.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('functionId', '', new UID(), 'Function unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
->inject('response')
->inject('dbForInternal')
->inject('deletes')
@@ -419,7 +419,7 @@ App::post('/v1/functions/:functionId/tags')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TAG)
- ->param('functionId', '', new UID(), 'Function unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
->param('command', '', new Text('1028'), 'Code execution command.')
->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', false)
->inject('request')
@@ -505,10 +505,10 @@ App::get('/v1/functions/:functionId/tags')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TAG_LIST)
- ->param('functionId', '', new UID(), 'Function unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of tags to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the tag used as the starting point for the query, excluding the tag itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
@@ -560,8 +560,8 @@ App::get('/v1/functions/:functionId/tags/:tagId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TAG)
- ->param('functionId', '', new UID(), 'Function unique ID.')
- ->param('tagId', '', new UID(), 'Tag unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
+ ->param('tagId', '', new UID(), 'Tag ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($functionId, $tagId, $response, $dbForInternal) {
@@ -598,8 +598,8 @@ App::delete('/v1/functions/:functionId/tags/:tagId')
->label('sdk.description', '/docs/references/functions/delete-tag.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('functionId', '', new UID(), 'Function unique ID.')
- ->param('tagId', '', new UID(), 'Tag unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
+ ->param('tagId', '', new UID(), 'Tag ID.')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -659,7 +659,7 @@ App::post('/v1/functions/:functionId/executions')
->label('sdk.response.model', Response::MODEL_EXECUTION)
->label('abuse-limit', 60)
->label('abuse-time', 60)
- ->param('functionId', '', new UID(), 'Function unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
->param('data', '', new Text(8192), 'String of custom data to send to function.', true)
// ->param('async', 1, new Range(0, 1), 'Execute code asynchronously. Pass 1 for true, 0 for false. Default value is 1.', true)
->inject('response')
@@ -767,9 +767,9 @@ App::get('/v1/functions/:functionId/executions')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_EXECUTION_LIST)
- ->param('functionId', '', new UID(), 'Function unique ID.')
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
+ ->param('functionId', '', new UID(), 'Function ID.')
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of executions to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('cursor', '', new UID(), 'ID of the execution used as the starting point for the query, excluding the execution itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
@@ -823,8 +823,8 @@ App::get('/v1/functions/:functionId/executions/:executionId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_EXECUTION)
- ->param('functionId', '', new UID(), 'Function unique ID.')
- ->param('executionId', '', new UID(), 'Execution unique ID.')
+ ->param('functionId', '', new UID(), 'Function ID.')
+ ->param('executionId', '', new UID(), 'Execution ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($functionId, $executionId, $response, $dbForInternal) {
diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php
index 732e061c3f..c671f8e01e 100644
--- a/app/controllers/api/storage.php
+++ b/app/controllers/api/storage.php
@@ -40,10 +40,10 @@ App::post('/v1/storage/files')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE)
- ->param('fileId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('fileId', '', new CustomId(), 'File ID. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('file', [], new File(), 'Binary file.', false)
- ->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
- ->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
+ ->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
+ ->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true)
->inject('request')
->inject('response')
->inject('dbForInternal')
@@ -175,8 +175,8 @@ App::get('/v1/storage/files')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE_LIST)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of files to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the file used as the starting point for the query, excluding the file itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
@@ -224,7 +224,7 @@ App::get('/v1/storage/files/:fileId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE)
- ->param('fileId', '', new UID(), 'File unique ID.')
+ ->param('fileId', '', new UID(), 'File ID.')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -256,7 +256,7 @@ App::get('/v1/storage/files/:fileId/preview')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE)
->label('sdk.methodType', 'location')
- ->param('fileId', '', new UID(), 'File unique ID')
+ ->param('fileId', '', new UID(), 'File ID.')
->param('width', 0, new Range(0, 4000), 'Resize preview image width, Pass an integer between 0 to 4000.', true)
->param('height', 0, new Range(0, 4000), 'Resize preview image height, Pass an integer between 0 to 4000.', true)
->param('gravity', Image::GRAVITY_CENTER, new WhiteList(Image::getGravityTypes()), 'Image crop gravity. Can be one of ' . implode(",", Image::getGravityTypes()), true)
@@ -417,7 +417,7 @@ App::get('/v1/storage/files/:fileId/download')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', '*/*')
->label('sdk.methodType', 'location')
- ->param('fileId', '', new UID(), 'File unique ID.')
+ ->param('fileId', '', new UID(), 'File ID.')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -482,7 +482,7 @@ App::get('/v1/storage/files/:fileId/view')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', '*/*')
->label('sdk.methodType', 'location')
- ->param('fileId', '', new UID(), 'File unique ID.')
+ ->param('fileId', '', new UID(), 'File ID.')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -558,9 +558,9 @@ App::put('/v1/storage/files/:fileId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE)
- ->param('fileId', '', new UID(), 'File unique ID.')
- ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
- ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
+ ->param('fileId', '', new UID(), 'File ID.')
+ ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
+ ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->inject('response')
->inject('dbForInternal')
->inject('audits')
@@ -606,7 +606,7 @@ App::delete('/v1/storage/files/:fileId')
->label('sdk.description', '/docs/references/storage/delete-file.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('fileId', '', new UID(), 'File unique ID.')
+ ->param('fileId', '', new UID(), 'File ID.')
->inject('response')
->inject('dbForInternal')
->inject('events')
@@ -752,7 +752,7 @@ App::get('/v1/storage/:bucketId/usage')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_BUCKETS)
- ->param('bucketId', '', new UID(), 'Bucket unique ID.')
+ ->param('bucketId', '', new UID(), 'Bucket ID.')
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->inject('response')
->inject('dbForInternal')
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 5d8e1781b7..02370df76b 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -34,7 +34,7 @@ App::post('/v1/teams')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM)
- ->param('teamId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('teamId', '', new CustomId(), 'Team ID. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', null, new Text(128), 'Team name. Max length: 128 chars.')
->param('roles', ['owner'], new ArrayList(new Key()), 'Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.', true)
->inject('response')
@@ -105,8 +105,8 @@ App::get('/v1/teams')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM_LIST)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of teams to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the team used as the starting point for the query, excluding the team itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
@@ -150,7 +150,7 @@ App::get('/v1/teams/:teamId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'Team ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($teamId, $response, $dbForInternal) {
@@ -178,7 +178,7 @@ App::put('/v1/teams/:teamId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'Team ID.')
->param('name', null, new Text(128), 'Team name. Max length: 128 chars.')
->inject('response')
->inject('dbForInternal')
@@ -211,7 +211,7 @@ App::delete('/v1/teams/:teamId')
->label('sdk.description', '/docs/references/teams/delete-team.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'Team ID.')
->inject('response')
->inject('dbForInternal')
->inject('events')
@@ -269,11 +269,11 @@ App::post('/v1/teams/:teamId/memberships')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
->label('abuse-limit', 10)
- ->param('teamId', '', new UID(), 'Team unique ID.')
- ->param('email', '', new Email(), 'New team member email.')
+ ->param('teamId', '', new UID(), 'Team ID.')
+ ->param('email', '', new Email(), 'Team member email.')
->param('roles', [], new ArrayList(new Key()), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page
- ->param('name', '', new Text(128), 'New team member name. Max length: 128 chars.', true)
+ ->param('name', '', new Text(128), 'Team member name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('user')
@@ -441,10 +441,10 @@ App::get('/v1/teams/:teamId/memberships')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP_LIST)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'Team ID.')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of memberships to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the membership used as the starting point for the query, excluding the membership itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
@@ -499,8 +499,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP_LIST)
- ->param('teamId', '', new UID(), 'Team unique ID.')
- ->param('membershipId', '', new UID(), 'membership unique ID.')
+ ->param('teamId', '', new UID(), 'Team ID.')
+ ->param('membershipId', '', new UID(), 'Membership ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($teamId, $membershipId, $response, $dbForInternal) {
@@ -536,9 +536,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'Team ID.')
->param('membershipId', '', new UID(), 'Membership ID.')
- ->param('roles', [], new ArrayList(new Key()), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
+ ->param('roles', [], new ArrayList(new Key()), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Max length for each role is 32 chars.')
->inject('request')
->inject('response')
->inject('user')
@@ -601,9 +601,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'Team ID.')
->param('membershipId', '', new UID(), 'Membership ID.')
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->param('secret', '', new Text(256), 'Secret key.')
->inject('request')
->inject('response')
@@ -739,7 +739,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.description', '/docs/references/teams/delete-team-membership.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('teamId', '', new UID(), 'Team unique ID.')
+ ->param('teamId', '', new UID(), 'Team ID.')
->param('membershipId', '', new UID(), 'Membership ID.')
->inject('response')
->inject('dbForInternal')
diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php
index a5d412d5b6..400bbd5a16 100644
--- a/app/controllers/api/users.php
+++ b/app/controllers/api/users.php
@@ -34,7 +34,7 @@ App::post('/v1/users')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
- ->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
@@ -93,8 +93,8 @@ App::get('/v1/users')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER_LIST)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
- ->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
+ ->param('limit', 25, new Range(0, 100), 'Maximum number of users to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
+ ->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->param('cursor', '', new UID(), 'ID of the user used as the starting point for the query, excluding the user itself. Should be used for efficient pagination when working with large sets of data.', true)
->param('cursorDirection', Database::CURSOR_AFTER, new WhiteList([Database::CURSOR_AFTER, Database::CURSOR_BEFORE]), 'Direction of the cursor.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
@@ -143,7 +143,7 @@ App::get('/v1/users/:userId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -175,7 +175,7 @@ App::get('/v1/users/:userId/prefs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PREFERENCES)
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -209,7 +209,7 @@ App::get('/v1/users/:userId/sessions')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION_LIST)
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForInternal')
->inject('locale')
@@ -258,7 +258,7 @@ App::get('/v1/users/:userId/logs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_LOG_LIST)
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->param('limit', 25, new Range(0, 100), 'Maximum number of logs to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this param to manage pagination.', true)
->inject('response')
@@ -377,8 +377,8 @@ App::patch('/v1/users/:userId/status')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
- ->param('userId', '', new UID(), 'User unique ID.')
- ->param('status', null, new Boolean(true), 'User Status. To activate the user pass `true` and to block the user pass `false`')
+ ->param('userId', '', new UID(), 'User ID.')
+ ->param('status', null, new Boolean(true), 'User Status. To activate the user pass `true` and to block the user pass `false`.')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -413,8 +413,8 @@ App::patch('/v1/users/:userId/verification')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
- ->param('userId', '', new UID(), 'User unique ID.')
- ->param('emailVerification', false, new Boolean(), 'User Email Verification Status.')
+ ->param('userId', '', new UID(), 'User ID.')
+ ->param('emailVerification', false, new Boolean(), 'User email verification status.')
->inject('response')
->inject('dbForInternal')
->inject('usage')
@@ -449,7 +449,7 @@ App::patch('/v1/users/:userId/name')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.')
->inject('response')
->inject('dbForInternal')
@@ -488,8 +488,8 @@ App::patch('/v1/users/:userId/password')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
- ->param('userId', '', new UID(), 'User unique ID.')
- ->param('password', '', new Password(), 'New user password. Must be between 6 to 32 chars.')
+ ->param('userId', '', new UID(), 'User ID.')
+ ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
->inject('response')
->inject('dbForInternal')
->inject('audits')
@@ -531,7 +531,7 @@ App::patch('/v1/users/:userId/email')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->param('email', '', new Email(), 'User email.')
->inject('response')
->inject('dbForInternal')
@@ -581,7 +581,7 @@ App::patch('/v1/users/:userId/prefs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PREFERENCES)
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.')
->inject('response')
->inject('dbForInternal')
@@ -616,8 +616,8 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
->label('sdk.description', '/docs/references/users/delete-user-session.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('userId', '', new UID(), 'User unique ID.')
- ->param('sessionId', null, new UID(), 'User unique session ID.')
+ ->param('userId', '', new UID(), 'User ID.')
+ ->param('sessionId', null, new UID(), 'Session ID.')
->inject('response')
->inject('dbForInternal')
->inject('events')
@@ -672,7 +672,7 @@ App::delete('/v1/users/:userId/sessions')
->label('sdk.description', '/docs/references/users/delete-user-sessions.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('userId', '', new UID(), 'User unique ID.')
+ ->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForInternal')
->inject('events')
@@ -719,7 +719,7 @@ App::delete('/v1/users/:userId')
->label('sdk.description', '/docs/references/users/delete.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('userId', '', function () {return new UID();}, 'User unique ID.')
+ ->param('userId', '', function () {return new UID();}, 'User ID.')
->inject('response')
->inject('dbForInternal')
->inject('events')
From bfbf0e54b9696de48f5279655f68955619106754 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Fri, 10 Dec 2021 16:44:54 +0100
Subject: [PATCH 029/107] Rolled back addition of articles
---
app/controllers/api/teams.php | 32 ++++++++++++++++----------------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index bcf081eedc..6c9ac4fd42 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -32,7 +32,7 @@ App::post('/v1/teams')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM)
- ->param('name', null, new Text(128), 'The name of the team. Max length: 128 chars.')
+ ->param('name', null, new Text(128), 'Team name. Max length: 128 chars.')
->param('roles', ['owner'], new ArrayList(new Key()), 'Array of strings. Use this param to set the roles in the team for the user who created it. The default role is **owner**. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.', true)
->inject('response')
->inject('user')
@@ -178,8 +178,8 @@ App::put('/v1/teams/:teamId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TEAM)
- ->param('teamId', '', new UID(), 'The unique team ID.')
- ->param('name', null, new Text(128), 'The new team name. Max length: 128 chars.')
+ ->param('teamId', '', new UID(), 'Unique team ID.')
+ ->param('name', null, new Text(128), 'New team name. Max length: 128 chars.')
->inject('response')
->inject('projectDB')
->action(function ($teamId, $name, $response, $projectDB) {
@@ -214,7 +214,7 @@ App::delete('/v1/teams/:teamId')
->label('sdk.description', '/docs/references/teams/delete-team.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('teamId', '', new UID(), 'The unique team ID.')
+ ->param('teamId', '', new UID(), 'Unique team ID.')
->inject('response')
->inject('projectDB')
->inject('events')
@@ -260,11 +260,11 @@ App::post('/v1/teams/:teamId/memberships')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
->label('abuse-limit', 10)
- ->param('teamId', '', new UID(), 'The unique team ID.')
- ->param('email', '', new Email(), 'The email address of the new team member.')
+ ->param('teamId', '', new UID(), 'Unique team ID.')
+ ->param('email', '', new Email(), 'Email address of the new team member.')
->param('roles', [], new ArrayList(new Key()), 'An array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients']) // TODO add our own built-in confirm page
- ->param('name', '', new Text(128), 'The name of the new team member. Max length: 128 chars.', true)
+ ->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true)
->inject('response')
->inject('project')
->inject('user')
@@ -472,8 +472,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
- ->param('teamId', '', new UID(), 'The unique team ID.')
- ->param('membershipId', '', new UID(), 'The membership ID.')
+ ->param('teamId', '', new UID(), 'Unique team ID.')
+ ->param('membershipId', '', new UID(), 'Membership ID.')
->param('roles', [], new ArrayList(new Key()), 'An array of strings. Use this param to set the user\'s roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Max length for each role is 32 chars.')
->inject('request')
->inject('response')
@@ -534,7 +534,7 @@ App::get('/v1/teams/:teamId/memberships')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP_LIST)
- ->param('teamId', '', new UID(), 'The unique team ID.')
+ ->param('teamId', '', new UID(), 'Unique team ID.')
->param('search', '', new Text(256), 'Search term to filter your results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Limit how many results will be returned. By default will return a maximum of 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
@@ -588,10 +588,10 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MEMBERSHIP)
- ->param('teamId', '', new UID(), 'The unique team ID.')
- ->param('membershipId', '', new UID(), 'The membership ID.')
- ->param('userId', '', new UID(), 'The unique user ID.')
- ->param('secret', '', new Text(256), 'The secret key.')
+ ->param('teamId', '', new UID(), 'Unique team ID.')
+ ->param('membershipId', '', new UID(), 'Membership ID.')
+ ->param('userId', '', new UID(), 'Unique user ID.')
+ ->param('secret', '', new Text(256), 'Secret key.')
->inject('request')
->inject('response')
->inject('user')
@@ -734,8 +734,8 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->label('sdk.description', '/docs/references/teams/delete-team-membership.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
- ->param('teamId', '', new UID(), 'The unique team ID.')
- ->param('membershipId', '', new UID(), 'The membership ID.')
+ ->param('teamId', '', new UID(), 'Unique team ID.')
+ ->param('membershipId', '', new UID(), 'Membership ID.')
->inject('response')
->inject('projectDB')
->inject('audits')
From eb9d258be77206a9c20811a2ea9950140df10be9 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Fri, 10 Dec 2021 16:45:21 +0100
Subject: [PATCH 030/107] Changed "On admin mode" to "In admin mode"
---
docs/references/teams/list-teams.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/references/teams/list-teams.md b/docs/references/teams/list-teams.md
index 68c3621cbd..a360974998 100644
--- a/docs/references/teams/list-teams.md
+++ b/docs/references/teams/list-teams.md
@@ -1,3 +1,3 @@
Use this endpoint to get a list of all the teams in which the current user is a member. You can use the parameters to filter your results.
-On admin mode, this endpoint returns a list of all the teams in the current project. [Learn more about different API modes](/docs/admin).
\ No newline at end of file
+In admin mode, this endpoint returns a list of all the teams in the current project. [Learn more about different API modes](/docs/admin).
\ No newline at end of file
From f02c1d01c5626e260e738fbee19b471f8b8db540 Mon Sep 17 00:00:00 2001
From: Radmacher <66096031+rdmchr@users.noreply.github.com>
Date: Fri, 10 Dec 2021 16:48:47 +0100
Subject: [PATCH 031/107] Removed "Use this endpoint" to avoid repetition
---
docs/references/teams/create-team-membership.md | 2 +-
docs/references/teams/create-team.md | 2 +-
docs/references/teams/delete-team.md | 2 +-
docs/references/teams/get-team.md | 2 +-
docs/references/teams/list-teams.md | 2 +-
docs/references/teams/update-team-membership-roles.md | 2 +-
docs/references/teams/update-team.md | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/docs/references/teams/create-team-membership.md b/docs/references/teams/create-team-membership.md
index f0bf9e416c..7668c69b99 100644
--- a/docs/references/teams/create-team-membership.md
+++ b/docs/references/teams/create-team-membership.md
@@ -1,4 +1,4 @@
-Use this endpoint to invite a new member to join your team. If initiated from the client SDK, an email with a link to join the team will be sent to the member's email address and an account will be created for them should they not be signed up already. If initiated from server-side SDKs, the new member will automatically be added to the team.
+Invite a new member to join your team. If initiated from the client SDK, an email with a link to join the team will be sent to the member's email address and an account will be created for them should they not be signed up already. If initiated from server-side SDKs, the new member will automatically be added to the team.
Use the 'url' parameter to redirect the user from the invitation email back to your app. When the user is redirected, use the [Update Team Membership Status](/docs/client/teams#teamsUpdateMembershipStatus) endpoint to allow the user to accept the invitation to the team.
diff --git a/docs/references/teams/create-team.md b/docs/references/teams/create-team.md
index 3ff3a30c8e..eaa8b1d9a1 100644
--- a/docs/references/teams/create-team.md
+++ b/docs/references/teams/create-team.md
@@ -1 +1 @@
-Use this endpoint to create a new team. The user who creates the team will automatically be assigned as the owner of the team. Only the users with the owner role can invite new members, add new owners and delete or update the team.
\ No newline at end of file
+Create a new team. The user who creates the team will automatically be assigned as the owner of the team. Only the users with the owner role can invite new members, add new owners and delete or update the team.
\ No newline at end of file
diff --git a/docs/references/teams/delete-team.md b/docs/references/teams/delete-team.md
index 19dfe40969..82bcc24cdf 100644
--- a/docs/references/teams/delete-team.md
+++ b/docs/references/teams/delete-team.md
@@ -1 +1 @@
-Use this endpoint to delete a team using its unique ID. Only team members with the owner role can delete the team.
\ No newline at end of file
+Delete a team using its unique ID. Only team members with the owner role can delete the team.
\ No newline at end of file
diff --git a/docs/references/teams/get-team.md b/docs/references/teams/get-team.md
index 5449bf2153..800612ab15 100644
--- a/docs/references/teams/get-team.md
+++ b/docs/references/teams/get-team.md
@@ -1 +1 @@
-Use this endpoint to get a team by its unique ID. All team members have read access for this resource.
\ No newline at end of file
+Get a team by its unique ID. All team members have read access for this resource.
\ No newline at end of file
diff --git a/docs/references/teams/list-teams.md b/docs/references/teams/list-teams.md
index a360974998..5b59bcbaae 100644
--- a/docs/references/teams/list-teams.md
+++ b/docs/references/teams/list-teams.md
@@ -1,3 +1,3 @@
-Use this endpoint to get a list of all the teams in which the current user is a member. You can use the parameters to filter your results.
+Get a list of all the teams in which the current user is a member. You can use the parameters to filter your results.
In admin mode, this endpoint returns a list of all the teams in the current project. [Learn more about different API modes](/docs/admin).
\ No newline at end of file
diff --git a/docs/references/teams/update-team-membership-roles.md b/docs/references/teams/update-team-membership-roles.md
index add91638bb..43e8a2a1ee 100644
--- a/docs/references/teams/update-team-membership-roles.md
+++ b/docs/references/teams/update-team-membership-roles.md
@@ -1 +1 @@
-Use this endpoint to modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](/docs/permissions)
\ No newline at end of file
+Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](/docs/permissions)
\ No newline at end of file
diff --git a/docs/references/teams/update-team.md b/docs/references/teams/update-team.md
index aa88d0e242..0acb528e1c 100644
--- a/docs/references/teams/update-team.md
+++ b/docs/references/teams/update-team.md
@@ -1 +1 @@
-Use this endpoint to update a team using its unique ID. Only members with the owner role can update the team.
\ No newline at end of file
+Update a team using its unique ID. Only members with the owner role can update the team.
\ No newline at end of file
From b530fd66cd4f91ef04b8df4df43789077783a6d2 Mon Sep 17 00:00:00 2001
From: Torsten Dittmann
Date: Fri, 10 Dec 2021 17:48:54 +0100
Subject: [PATCH 032/107] fix(storage): add same permissions check to files
uploaded
---
app/controllers/api/storage.php | 43 ++++++-
.../Storage/StorageCustomClientTest.php | 111 +++++++++++++++++-
2 files changed, 152 insertions(+), 2 deletions(-)
diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php
index 732e061c3f..0522826af4 100644
--- a/app/controllers/api/storage.php
+++ b/app/controllers/api/storage.php
@@ -1,5 +1,6 @@
isEmpty()) ? ['user:'.$user->getId()] : $read ?? []; // By default set read permissions for user
+ $write = (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? [];
+
+ // Users can only add their roles to files, API keys and Admin users can add any
+ $roles = \array_fill_keys(Authorization::getRoles(), true); // Auth::isAppUser expects roles to be keys, not values of assoc array
+
+ if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
+ foreach ($read as $role) {
+ if (!Authorization::isRole($role)) {
+ throw new Exception('Read permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ }
+ }
+ foreach ($write as $role) {
+ if (!Authorization::isRole($role)) {
+ throw new Exception('Write permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ }
+ }
+ }
+
$file = $request->getFiles('file');
/*
@@ -563,13 +583,34 @@ App::put('/v1/storage/files/:fileId')
->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
->inject('response')
->inject('dbForInternal')
+ ->inject('user')
->inject('audits')
->inject('usage')
- ->action(function ($fileId, $read, $write, $response, $dbForInternal, $audits, $usage) {
+ ->action(function ($fileId, $read, $write, $response, $dbForInternal, $user, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
+ /** @var Utopia\Database\Document $user */
/** @var Appwrite\Event\Event $audits */
+ $read = (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? []; // By default set read permissions for user
+ $write = (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? [];
+
+ // Users can only add their roles to files, API keys and Admin users can add any
+ $roles = \array_fill_keys(Authorization::getRoles(), true); // Auth::isAppUser expects roles to be keys, not values of assoc array
+
+ if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
+ foreach ($read as $role) {
+ if (!Authorization::isRole($role)) {
+ throw new Exception('Read permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ }
+ }
+ foreach ($write as $role) {
+ if (!Authorization::isRole($role)) {
+ throw new Exception('Write permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ }
+ }
+ }
+
$file = $dbForInternal->getDocument('files', $fileId);
if (empty($file->getId())) {
diff --git a/tests/e2e/Services/Storage/StorageCustomClientTest.php b/tests/e2e/Services/Storage/StorageCustomClientTest.php
index 2a0ead59f6..a0dee63156 100644
--- a/tests/e2e/Services/Storage/StorageCustomClientTest.php
+++ b/tests/e2e/Services/Storage/StorageCustomClientTest.php
@@ -3,6 +3,9 @@
namespace Tests\E2E\Services\Storage;
use CURLFile;
+use Exception;
+use SebastianBergmann\RecursionContext\InvalidArgumentException;
+use PHPUnit\Framework\ExpectationFailedException;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
@@ -14,7 +17,7 @@ class StorageCustomClientTest extends Scope
use ProjectCustom;
use SideClient;
- public function testCreateFileDefaultPermissions():void
+ public function testCreateFileDefaultPermissions(): array
{
/**
* Test for SUCCESS
@@ -36,5 +39,111 @@ class StorageCustomClientTest extends Scope
$this->assertEquals('permissions.png', $file['body']['name']);
$this->assertEquals('image/png', $file['body']['mimeType']);
$this->assertEquals(47218, $file['body']['sizeOriginal']);
+
+ return $file['body'];
+ }
+
+ public function testCreateFileAbusePermissions(): void
+ {
+ /**
+ * Test for FAILURE
+ */
+ $file = $this->client->call(Client::METHOD_POST, '/storage/files', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'fileId' => 'unique()',
+ 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'),
+ 'folderId' => 'xyz',
+ 'read' => ['user:notme']
+ ]);
+
+ $this->assertEquals($file['headers']['status-code'], 400);
+ $this->assertStringStartsWith('Read permissions must be one of:', $file['body']['message']);
+ $this->assertStringContainsString('role:all', $file['body']['message']);
+ $this->assertStringContainsString('role:member', $file['body']['message']);
+ $this->assertStringContainsString('user:'.$this->getUser()['$id'], $file['body']['message']);
+
+ $file = $this->client->call(Client::METHOD_POST, '/storage/files', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'fileId' => 'unique()',
+ 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'),
+ 'folderId' => 'xyz',
+ 'write' => ['user:notme']
+ ]);
+
+ $this->assertEquals($file['headers']['status-code'], 400);
+ $this->assertStringStartsWith('Write permissions must be one of:', $file['body']['message']);
+ $this->assertStringContainsString('role:all', $file['body']['message']);
+ $this->assertStringContainsString('role:member', $file['body']['message']);
+ $this->assertStringContainsString('user:'.$this->getUser()['$id'], $file['body']['message']);
+
+ $file = $this->client->call(Client::METHOD_POST, '/storage/files', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'fileId' => 'unique()',
+ 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'permissions.png'),
+ 'folderId' => 'xyz',
+ 'read' => ['user:notme'],
+ 'write' => ['user:notme']
+ ]);
+
+ $this->assertEquals($file['headers']['status-code'], 400);
+ $this->assertStringStartsWith('Read permissions must be one of:', $file['body']['message']);
+ $this->assertStringContainsString('role:all', $file['body']['message']);
+ $this->assertStringContainsString('role:member', $file['body']['message']);
+ $this->assertStringContainsString('user:'.$this->getUser()['$id'], $file['body']['message']);
+ }
+
+ /**
+ * @depends testCreateFileDefaultPermissions
+ */
+ public function testUpdateFileAbusePermissions(array $data): void
+ {
+ /**
+ * Test for FAILURE
+ */
+ $file = $this->client->call(Client::METHOD_PUT, '/storage/files/' . $data['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'read' => ['user:notme']
+ ]);
+
+ $this->assertEquals($file['headers']['status-code'], 400);
+ $this->assertStringStartsWith('Read permissions must be one of:', $file['body']['message']);
+ $this->assertStringContainsString('role:all', $file['body']['message']);
+ $this->assertStringContainsString('role:member', $file['body']['message']);
+ $this->assertStringContainsString('user:'.$this->getUser()['$id'], $file['body']['message']);
+
+ $file = $this->client->call(Client::METHOD_PUT, '/storage/files/' . $data['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'write' => ['user:notme']
+ ]);
+
+ $this->assertEquals($file['headers']['status-code'], 400);
+ $this->assertStringStartsWith('Write permissions must be one of:', $file['body']['message']);
+ $this->assertStringContainsString('role:all', $file['body']['message']);
+ $this->assertStringContainsString('role:member', $file['body']['message']);
+ $this->assertStringContainsString('user:'.$this->getUser()['$id'], $file['body']['message']);
+
+ $file = $this->client->call(Client::METHOD_PUT, '/storage/files/' . $data['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'read' => ['user:notme'],
+ 'write' => ['user:notme']
+ ]);
+
+ $this->assertEquals($file['headers']['status-code'], 400);
+ $this->assertStringStartsWith('Read permissions must be one of:', $file['body']['message']);
+ $this->assertStringContainsString('role:all', $file['body']['message']);
+ $this->assertStringContainsString('role:member', $file['body']['message']);
+ $this->assertStringContainsString('user:'.$this->getUser()['$id'], $file['body']['message']);
}
}
\ No newline at end of file
From 75a83e9b535c7d21c5d0aec274461f6a9b725668 Mon Sep 17 00:00:00 2001
From: kodumbeats
Date: Fri, 10 Dec 2021 12:22:10 -0500
Subject: [PATCH 033/107] Harmonize offset param descriptions
---
app/controllers/api/teams.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 6c9ac4fd42..fbe7f0b4ba 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -115,7 +115,7 @@ App::get('/v1/teams')
->label('sdk.response.model', Response::MODEL_TEAM_LIST)
->param('search', '', new Text(256), 'Search term to filter results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Limit how many results will be returned. Returns up to 25 results by default. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, 2000), 'Use this value to manage pagination. The default value is 0.', true)
+ ->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this value to manage pagination.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('projectDB')
@@ -537,7 +537,7 @@ App::get('/v1/teams/:teamId/memberships')
->param('teamId', '', new UID(), 'Unique team ID.')
->param('search', '', new Text(256), 'Search term to filter your results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Limit how many results will be returned. By default will return a maximum of 25 results. Maximum of 100 results allowed per request.', true)
- ->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
+ ->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this value to manage pagination.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order results by ASC or DESC order.', true)
->inject('response')
->inject('projectDB')
From ca821f398965d9d90268597d24758f113ef27d8e Mon Sep 17 00:00:00 2001
From: kodumbeats
Date: Fri, 10 Dec 2021 12:23:20 -0500
Subject: [PATCH 034/107] Add missing period to doc
---
docs/references/teams/update-team-membership-roles.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/references/teams/update-team-membership-roles.md b/docs/references/teams/update-team-membership-roles.md
index 43e8a2a1ee..344d2875df 100644
--- a/docs/references/teams/update-team-membership-roles.md
+++ b/docs/references/teams/update-team-membership-roles.md
@@ -1 +1 @@
-Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](/docs/permissions)
\ No newline at end of file
+Modify the roles of a team member. Only team members with the owner role have access to this endpoint. Learn more about [roles and permissions](/docs/permissions).
\ No newline at end of file
From aef6c113700e22d321267bbaf1c135e8237b933f Mon Sep 17 00:00:00 2001
From: Torsten Dittmann
Date: Fri, 10 Dec 2021 18:52:33 +0100
Subject: [PATCH 035/107] fix(auth): use getRoles instead of static property
---
app/controllers/api/account.php | 17 +++++++-----
app/controllers/api/database.php | 12 ++++-----
app/controllers/api/storage.php | 12 ++++-----
app/controllers/api/teams.php | 12 ++++-----
app/controllers/general.php | 2 +-
app/controllers/shared/api.php | 9 ++++---
src/Appwrite/Auth/Auth.php | 10 ++++----
tests/unit/Auth/AuthTest.php | 44 ++++++++++++++++----------------
8 files changed, 61 insertions(+), 57 deletions(-)
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index 5c86755813..d2e68b1144 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -648,8 +648,9 @@ App::post('/v1/account/sessions/magic-url')
throw new Exception('SMTP Disabled', 503);
}
- $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
- $isAppUser = Auth::isAppUser(Authorization::$roles);
+ $roles = Authorization::getRoles();
+ $isPrivilegedUser = Auth::isPrivilegedUser($roles);
+ $isAppUser = Auth::isAppUser($roles);
$user = $dbForInternal->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]);
@@ -1780,8 +1781,9 @@ App::post('/v1/account/recovery')
throw new Exception('SMTP Disabled', 503);
}
- $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
- $isAppUser = Auth::isAppUser(Authorization::$roles);
+ $roles = Authorization::getRoles();
+ $isPrivilegedUser = Auth::isPrivilegedUser($roles);
+ $isAppUser = Auth::isAppUser($roles);
$email = \strtolower($email);
$profile = $dbForInternal->findOne('users', [new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address
@@ -1971,9 +1973,10 @@ App::post('/v1/account/verification')
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503);
}
-
- $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
- $isAppUser = Auth::isAppUser(Authorization::$roles);
+
+ $roles = Authorization::getRoles();
+ $isPrivilegedUser = Auth::isPrivilegedUser($roles);
+ $isAppUser = Auth::isAppUser($roles);
$verificationSecret = Auth::tokenGenerator();
diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php
index 8c4f2d20be..3759c63ad2 100644
--- a/app/controllers/api/database.php
+++ b/app/controllers/api/database.php
@@ -1641,17 +1641,17 @@ App::post('/v1/database/collections/:collectionId/documents')
$data['$write'] = (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? []; // By default set write permissions for user
// Users can only add their roles to documents, API keys and Admin users can add any
- $roles = \array_fill_keys(Authorization::getRoles(), true); // Auth::isAppUser expects roles to be keys, not values of assoc array
+ $roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach ($data['$read'] as $read) {
if (!Authorization::isRole($read)) {
- throw new Exception('Read permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400);
}
}
foreach ($data['$write'] as $write) {
if (!Authorization::isRole($write)) {
- throw new Exception('Write permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ throw new Exception('Write permissions must be one of: ('.\implode(', ', $roles).')', 400);
}
}
}
@@ -2002,17 +2002,17 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
$data['$write'] = (is_null($write)) ? ($document->getWrite() ?? []) : $write; // By default inherit write permissions
// Users can only add their roles to documents, API keys and Admin users can add any
- $roles = \array_fill_keys(Authorization::getRoles(), true); // Auth::isAppUser expects roles to be keys, not values of assoc array
+ $roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach ($data['$read'] as $read) {
if (!Authorization::isRole($read)) {
- throw new Exception('Read permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400);
}
}
foreach ($data['$write'] as $write) {
if (!Authorization::isRole($write)) {
- throw new Exception('Write permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ throw new Exception('Write permissions must be one of: ('.\implode(', ', $roles).')', 400);
}
}
}
diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php
index 0522826af4..3e095f4d01 100644
--- a/app/controllers/api/storage.php
+++ b/app/controllers/api/storage.php
@@ -63,17 +63,17 @@ App::post('/v1/storage/files')
$write = (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? [];
// Users can only add their roles to files, API keys and Admin users can add any
- $roles = \array_fill_keys(Authorization::getRoles(), true); // Auth::isAppUser expects roles to be keys, not values of assoc array
+ $roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach ($read as $role) {
if (!Authorization::isRole($role)) {
- throw new Exception('Read permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400);
}
}
foreach ($write as $role) {
if (!Authorization::isRole($role)) {
- throw new Exception('Write permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ throw new Exception('Write permissions must be one of: ('.\implode(', ', $roles).')', 400);
}
}
}
@@ -596,17 +596,17 @@ App::put('/v1/storage/files/:fileId')
$write = (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? [];
// Users can only add their roles to files, API keys and Admin users can add any
- $roles = \array_fill_keys(Authorization::getRoles(), true); // Auth::isAppUser expects roles to be keys, not values of assoc array
+ $roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach ($read as $role) {
if (!Authorization::isRole($role)) {
- throw new Exception('Read permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400);
}
}
foreach ($write as $role) {
if (!Authorization::isRole($role)) {
- throw new Exception('Write permissions must be one of: ('.\implode(', ', array_keys($roles)).')', 400);
+ throw new Exception('Write permissions must be one of: ('.\implode(', ', $roles).')', 400);
}
}
}
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 5d8e1781b7..5b4476fb09 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -49,8 +49,8 @@ App::post('/v1/teams')
Authorization::disable();
- $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
- $isAppUser = Auth::isAppUser(Authorization::$roles);
+ $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
+ $isAppUser = Auth::isAppUser(Authorization::getRoles());
$teamId = $teamId == 'unique()' ? $dbForInternal->getId() : $teamId;
$team = $dbForInternal->createDocument('teams', new Document([
@@ -293,8 +293,8 @@ App::post('/v1/teams/:teamId/memberships')
throw new Exception('SMTP Disabled', 503);
}
- $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
- $isAppUser = Auth::isAppUser(Authorization::$roles);
+ $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
+ $isAppUser = Auth::isAppUser(Authorization::getRoles());
$email = \strtolower($email);
$name = (empty($name)) ? $email : $name;
@@ -566,8 +566,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception('User not found', 404);
}
- $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
- $isAppUser = Auth::isAppUser(Authorization::$roles);
+ $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
+ $isAppUser = Auth::isAppUser(Authorization::getRoles());
$isOwner = Authorization::isRole('team:'.$team->getId().'/owner');;
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
diff --git a/app/controllers/general.php b/app/controllers/general.php
index 4d2c6b7135..e75b2f2164 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -254,7 +254,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
if(!empty($service)) {
if(array_key_exists($service, $project->getAttribute('services',[]))
&& !$project->getAttribute('services',[])[$service]
- && !Auth::isPrivilegedUser(Authorization::$roles)) {
+ && !Auth::isPrivilegedUser(Authorization::getRoles())) {
throw new Exception('Service is disabled', 503);
}
}
diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php
index 210cb46912..79f476c133 100644
--- a/app/controllers/shared/api.php
+++ b/app/controllers/shared/api.php
@@ -64,8 +64,9 @@ App::init(function ($utopia, $request, $response, $project, $user, $events, $aud
;
}
- $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
- $isAppUser = Auth::isAppUser(Authorization::$roles);
+ $roles = Authorization::getRoles();
+ $isPrivilegedUser = Auth::isPrivilegedUser($roles);
+ $isAppUser = Auth::isAppUser($roles);
if (($abuse->check() // Route is rate-limited
&& App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') // Abuse is not disabled
@@ -128,8 +129,8 @@ App::init(function ($utopia, $request, $project) {
$route = $utopia->match($request);
- $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
- $isAppUser = Auth::isAppUser(Authorization::$roles);
+ $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
+ $isAppUser = Auth::isAppUser(Authorization::getRoles());
if($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php
index 2895b3db11..8ac5839ee0 100644
--- a/src/Appwrite/Auth/Auth.php
+++ b/src/Appwrite/Auth/Auth.php
@@ -242,9 +242,9 @@ class Auth
public static function isPrivilegedUser(array $roles): bool
{
if (
- array_key_exists('role:'.self::USER_ROLE_OWNER, $roles) ||
- array_key_exists('role:'.self::USER_ROLE_DEVELOPER, $roles) ||
- array_key_exists('role:'.self::USER_ROLE_ADMIN, $roles)
+ in_array('role:'.self::USER_ROLE_OWNER, $roles) ||
+ in_array('role:'.self::USER_ROLE_DEVELOPER, $roles) ||
+ in_array('role:'.self::USER_ROLE_ADMIN, $roles)
) {
return true;
}
@@ -261,7 +261,7 @@ class Auth
*/
public static function isAppUser(array $roles): bool
{
- if (array_key_exists('role:'.self::USER_ROLE_APP, $roles)) {
+ if (in_array('role:'.self::USER_ROLE_APP, $roles)) {
return true;
}
@@ -278,7 +278,7 @@ class Auth
{
$roles = [];
- if (!self::isPrivilegedUser(Authorization::$roles) && !self::isAppUser(Authorization::$roles)) {
+ if (!self::isPrivilegedUser(Authorization::getRoles()) && !self::isAppUser(Authorization::getRoles())) {
if ($user->getId()) {
$roles[] = 'user:'.$user->getId();
$roles[] = 'role:'.Auth::USER_ROLE_MEMBER;
diff --git a/tests/unit/Auth/AuthTest.php b/tests/unit/Auth/AuthTest.php
index c3eb711155..c3b9d00758 100644
--- a/tests/unit/Auth/AuthTest.php
+++ b/tests/unit/Auth/AuthTest.php
@@ -172,35 +172,35 @@ class AuthTest extends TestCase
public function testIsPrivilegedUser()
{
$this->assertEquals(false, Auth::isPrivilegedUser([]));
- $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_GUEST => true]));
- $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_MEMBER => true]));
- $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_ADMIN => true]));
- $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_DEVELOPER => true]));
- $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_OWNER => true]));
- $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_APP => true]));
- $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_SYSTEM => true]));
+ $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_GUEST]));
+ $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_MEMBER]));
+ $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_ADMIN]));
+ $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_DEVELOPER]));
+ $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_OWNER]));
+ $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_APP]));
+ $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_SYSTEM]));
- $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_APP => true, 'role:'.Auth::USER_ROLE_APP => true]));
- $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_APP => true, 'role:'.Auth::USER_ROLE_GUEST => true]));
- $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_OWNER => true, 'role:'.Auth::USER_ROLE_GUEST => true]));
- $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_OWNER => true, 'role:'.Auth::USER_ROLE_ADMIN => true, 'role:'.Auth::USER_ROLE_DEVELOPER => true]));
+ $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_APP, 'role:'.Auth::USER_ROLE_APP]));
+ $this->assertEquals(false, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_APP, 'role:'.Auth::USER_ROLE_GUEST]));
+ $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_OWNER, 'role:'.Auth::USER_ROLE_GUEST]));
+ $this->assertEquals(true, Auth::isPrivilegedUser(['role:'.Auth::USER_ROLE_OWNER, 'role:'.Auth::USER_ROLE_ADMIN, 'role:'.Auth::USER_ROLE_DEVELOPER]));
}
public function testIsAppUser()
{
$this->assertEquals(false, Auth::isAppUser([]));
- $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_GUEST => true]));
- $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_MEMBER => true]));
- $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_ADMIN => true]));
- $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_DEVELOPER => true]));
- $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_OWNER => true]));
- $this->assertEquals(true, Auth::isAppUser(['role:'.Auth::USER_ROLE_APP => true]));
- $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_SYSTEM => true]));
+ $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_GUEST]));
+ $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_MEMBER]));
+ $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_ADMIN]));
+ $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_DEVELOPER]));
+ $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_OWNER]));
+ $this->assertEquals(true, Auth::isAppUser(['role:'.Auth::USER_ROLE_APP]));
+ $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_SYSTEM]));
- $this->assertEquals(true, Auth::isAppUser(['role:'.Auth::USER_ROLE_APP => true, 'role:'.Auth::USER_ROLE_APP => true]));
- $this->assertEquals(true, Auth::isAppUser(['role:'.Auth::USER_ROLE_APP => true, 'role:'.Auth::USER_ROLE_GUEST => true]));
- $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_OWNER => true, 'role:'.Auth::USER_ROLE_GUEST => true]));
- $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_OWNER => true, 'role:'.Auth::USER_ROLE_ADMIN => true, 'role:'.Auth::USER_ROLE_DEVELOPER => true]));
+ $this->assertEquals(true, Auth::isAppUser(['role:'.Auth::USER_ROLE_APP, 'role:'.Auth::USER_ROLE_APP]));
+ $this->assertEquals(true, Auth::isAppUser(['role:'.Auth::USER_ROLE_APP, 'role:'.Auth::USER_ROLE_GUEST]));
+ $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_OWNER, 'role:'.Auth::USER_ROLE_GUEST]));
+ $this->assertEquals(false, Auth::isAppUser(['role:'.Auth::USER_ROLE_OWNER, 'role:'.Auth::USER_ROLE_ADMIN, 'role:'.Auth::USER_ROLE_DEVELOPER]));
}
public function testGuestRoles()
From 76bf24ac9b31236cc53cc171225be6549f0e362b Mon Sep 17 00:00:00 2001
From: Eldad Fux
Date: Sat, 11 Dec 2021 02:17:28 +0200
Subject: [PATCH 036/107] Updated Youtube URL
---
app/init.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/init.php b/app/init.php
index 52cb48093d..84a9f81966 100644
--- a/app/init.php
+++ b/app/init.php
@@ -64,7 +64,7 @@ const APP_SOCIAL_DISCORD = 'https://appwrite.io/discord';
const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244';
const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
-const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite';
+const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
// Deletion Types
const DELETE_TYPE_DOCUMENT = 'document';
From 8a46713d8ec71ec6dbebee68497452f2a9a53e00 Mon Sep 17 00:00:00 2001
From: Eldad Fux
Date: Sat, 11 Dec 2021 22:17:33 +0200
Subject: [PATCH 037/107] Updated our Twitter handle
---
CONTRIBUTING.md | 4 ++--
README.md | 4 ++--
app/init.php | 4 ++--
app/tasks/sdks.php | 2 +-
4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 01aa3d2ede..a1f9b50c77 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,7 +4,7 @@ We would ❤️ for you to contribute to Appwrite and help make it better! We wa
## How to Start?
-If you are worried or don’t know where to start, check out our next section explaining what kind of help we could use and where can you get involved. You can reach out with questions to [Eldad Fux (@eldadfux)](https://twitter.com/eldadfux) or [@appwrite_io](https://twitter.com/appwrite_io) on Twitter, and anyone from the [Appwrite team on Discord](https://discord.gg/GSeTUeA). You can also submit an issue, and a maintainer can guide you!
+If you are worried or don’t know where to start, check out our next section explaining what kind of help we could use and where can you get involved. You can reach out with questions to [Eldad Fux (@eldadfux)](https://twitter.com/eldadfux) or [@appwrite](https://twitter.com/appwrite) on Twitter, and anyone from the [Appwrite team on Discord](https://discord.gg/GSeTUeA). You can also submit an issue, and a maintainer can guide you!
## Code of Conduct
@@ -395,7 +395,7 @@ Pull requests are great, but there are many other areas where you can help Appwr
### Blogging & Speaking
-Blogging, speaking about, or creating tutorials about one of Appwrite’s many features. Mention [@appwrite_io](https://twitter.com/appwrite_io) on Twitter and/or [email team@appwrite.io](mailto:team@appwrite.io) so we can give pointers and tips and help you spread the word by promoting your content on the different Appwrite communication channels. Please add your blog posts and videos of talks to our [Awesome Appwrite](https://github.com/appwrite/awesome-appwrite) repo on GitHub.
+Blogging, speaking about, or creating tutorials about one of Appwrite’s many features. Mention [@appwrite](https://twitter.com/appwrite) on Twitter and/or [email team@appwrite.io](mailto:team@appwrite.io) so we can give pointers and tips and help you spread the word by promoting your content on the different Appwrite communication channels. Please add your blog posts and videos of talks to our [Awesome Appwrite](https://github.com/appwrite/awesome-appwrite) repo on GitHub.
### Presenting at Meetups
diff --git a/README.md b/README.md
index 5d2652cf19..d61a286fb8 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
[](https://appwrite.io/discord?r=Github)
[](https://hub.docker.com/r/appwrite/appwrite)
[](https://travis-ci.com/appwrite/appwrite)
-[](https://twitter.com/appwrite_io)
+[](https://twitter.com/appwrite)
[](docs/tutorials/add-translations.md)
@@ -155,7 +155,7 @@ For security issues, kindly email us at [security@appwrite.io](mailto:security@a
## Follow Us
-Join our growing community around the world! See our official [Blog](https://medium.com/appwrite-io). Follow us on [Twitter](https://twitter.com/appwrite_io), [Facebook Page](https://www.facebook.com/appwrite.io), [Facebook Group](https://www.facebook.com/groups/appwrite.developers/) , [Dev Community](https://dev.to/appwrite) or join our live [Discord server](https://discord.gg/GSeTUeA) for more help, ideas, and discussions.
+Join our growing community around the world! See our official [Blog](https://medium.com/appwrite-io). Follow us on [Twitter](https://twitter.com/appwrite), [Facebook Page](https://www.facebook.com/appwrite.io), [Facebook Group](https://www.facebook.com/groups/appwrite.developers/) , [Dev Community](https://dev.to/appwrite) or join our live [Discord server](https://discord.gg/GSeTUeA) for more help, ideas, and discussions.
## License
diff --git a/app/init.php b/app/init.php
index 7461116bc0..26e291b82c 100644
--- a/app/init.php
+++ b/app/init.php
@@ -54,8 +54,8 @@ const APP_STORAGE_FUNCTIONS = '/storage/functions';
const APP_STORAGE_CACHE = '/storage/cache';
const APP_STORAGE_CERTIFICATES = '/storage/certificates';
const APP_STORAGE_CONFIG = '/storage/config';
-const APP_SOCIAL_TWITTER = 'https://twitter.com/appwrite_io';
-const APP_SOCIAL_TWITTER_HANDLE = 'appwrite_io';
+const APP_SOCIAL_TWITTER = 'https://twitter.com/appwrite';
+const APP_SOCIAL_TWITTER_HANDLE = 'appwrite';
const APP_SOCIAL_FACEBOOK = 'https://www.facebook.com/appwrite.io';
const APP_SOCIAL_LINKEDIN = 'https://www.linkedin.com/company/appwrite';
const APP_SOCIAL_INSTAGRAM = 'https://www.instagram.com/appwrite.io';
diff --git a/app/tasks/sdks.php b/app/tasks/sdks.php
index fa17c25855..2517d526d3 100644
--- a/app/tasks/sdks.php
+++ b/app/tasks/sdks.php
@@ -180,7 +180,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
->setShareText('Appwrite is a backend as a service for building web or mobile apps')
->setShareURL('http://appwrite.io')
->setShareTags('JS,javascript,reactjs,angular,ios,android,serverless')
- ->setShareVia('appwrite_io')
+ ->setShareVia('appwrite')
->setWarning($warning)
->setReadme($readme)
->setGettingStarted($gettingStarted)
From 624c18c0bf076e54f18da7164b6ed7c896f635ac Mon Sep 17 00:00:00 2001
From: Eldad Fux
Date: Sun, 12 Dec 2021 18:05:33 +0200
Subject: [PATCH 038/107] Updated og image
---
app/views/layouts/default.phtml | 2 +-
public/images/ogimage.png | Bin 0 -> 676279 bytes
2 files changed, 1 insertion(+), 1 deletion(-)
create mode 100644 public/images/ogimage.png
diff --git a/app/views/layouts/default.phtml b/app/views/layouts/default.phtml
index c6c0de795e..3b7445e9bc 100644
--- a/app/views/layouts/default.phtml
+++ b/app/views/layouts/default.phtml
@@ -10,7 +10,7 @@ $litespeed = $this->getParam('litespeed', true);
$analytics = $this->getParam('analytics', 'UA-26264668-9');
$mode = $this->getParam('mode', '');
$canonical = $this->getParam('canonical', '');
-$image = $this->getParam('image', '/images/logo.png');
+$image = $this->getParam('image', '/images/ogimage.png');
$locale = $this->getParam('locale', null);
$runtimes = $this->getParam('runtimes', null);
diff --git a/public/images/ogimage.png b/public/images/ogimage.png
new file mode 100644
index 0000000000000000000000000000000000000000..1b356dbbd5df8f12f8d2dacca5b9eb5675214af6
GIT binary patch
literal 676279
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