Compare commits

...
Author SHA1 Message Date
fogelito 4d670bb47d Remove use 2025-07-20 12:06:23 +03:00
fogelito a9e53a46a6 formatting 2025-07-20 12:04:55 +03:00
fogelito e2797ab9d4 Disable index array 2025-07-20 11:57:43 +03:00
Steven NguyenandGitHub 8b90acd8c6 Merge pull request #10164 from appwrite/improve-scheme-error
chore: improve invalid scheme error in origin check
2025-07-19 07:16:20 -07:00
Chirag Aggarwal 7fecfc73ca fix: formatting 2025-07-19 10:39:39 +05:30
Chirag Aggarwal c55f039f13 chore: add unit test 2025-07-18 22:23:33 +05:30
Chirag Aggarwal d613237fb4 chore: improve invalid scheme error in origin check 2025-07-18 22:08:43 +05:30
Steven NguyenandGitHub 5742dad1e4 Merge pull request #10161 from appwrite/update-dart-flutter
chore: update dart 16.1.0, flutter 17.0.2 and cli 8.2.2
2025-07-18 08:49:34 -07:00
Chirag Aggarwal ea0654cf5e improve naming 2025-07-18 17:16:01 +05:30
Chirag Aggarwal 99ea9c5d80 chore: update cli to 8.2.2 2025-07-18 17:10:20 +05:30
Chirag Aggarwal 712425c383 fix: version 2025-07-18 15:59:09 +05:30
Chirag Aggarwal 0cc443b47c fix: branch name 2025-07-18 15:56:12 +05:30
Chirag Aggarwal c729a84d51 chore: update dart to 16.1.0 and flutter to 17.0.2 2025-07-18 15:55:11 +05:30
Jake BarnbyandGitHub 33435890dd Merge pull request #10158 from appwrite/fix-redirect-specs
Handle redirect validator in specs + GraphQL type mapper
2025-07-18 12:19:22 +12:00
Jake Barnby 0a5fbb7bc7 Handle redirect validator in specs + GraphQL type mapper 2025-07-18 11:59:32 +12:00
Matej BačoandGitHub 1e61091834 Merge pull request #10157 from appwrite/fix-build-usage-specification
fix: build usage specification
2025-07-17 15:33:38 +02:00
loks0n 94decb26e6 fix: build usage specification 2025-07-17 14:05:05 +01:00
Matej BačoandGitHub a3f9707938 Merge pull request #10155 from appwrite/update-cli-8.2.1
Update cli 8.2.1
2025-07-17 11:55:14 +02:00
Chirag Aggarwal 0a82b6d30d chore: update CLI to 8.2.1 2025-07-17 11:44:30 +05:30
Damodar LohaniandGitHub 0f38bb689f Merge pull request #10138 from appwrite/update-react-native-0.10.1
chore: update react native to 0.10.1 and dotnet to 0.14.0
2025-07-15 10:39:30 +05:45
Steven NguyenandGitHub 00889426de Merge pull request #10150 from appwrite/fix-apple-oauth
fix: prevent injected $user from being shadowed
2025-07-14 21:29:16 -07:00
Steven Nguyen 477682e062 fix: prevent injected $user from being shadowed 2025-07-14 16:05:58 -07:00
Chirag Aggarwal 054b6a995a update dotnet to 0.14.0 2025-07-14 08:54:37 +05:30
Chirag Aggarwal a2ca08023d chore: update composer 2025-07-12 12:18:11 +05:30
Chirag Aggarwal 8a2e0d4970 chore: update react native to 0.10.1 and changelog 2025-07-12 12:04:51 +05:30
Matej BačoandGitHub 51d07832f7 Merge pull request #10136 from appwrite/update-cli-8.2.0
chore: update cli to 8.2.0
2025-07-11 16:39:13 +02:00
Matej BačoandGitHub 8c57d28f8f Merge pull request #10127 from appwrite/chore-speed-up-tests
Chore: speed up tests
2025-07-11 16:21:40 +02:00
Matej Bačo 0bdb64fc77 Fix migration tests 2025-07-11 15:54:53 +02:00
Chirag Aggarwal 32cb5e0a79 chore: update cli to 8.2.0 2025-07-11 16:31:50 +05:30
Matej BačoandGitHub e24671f6f4 Merge pull request #10135 from appwrite/fix-staging-specs
Fix: specs with new env vars
2025-07-11 12:08:21 +02:00
Matej Bačo 9bab764378 Fix specs with new env vars 2025-07-11 11:15:55 +02:00
Steven NguyenandGitHub fd35043dd7 Merge pull request #10119 from appwrite/fix-mobile-oauth2-redirect
fix: update OAuth2 redirect URLs
2025-07-10 16:46:58 -07:00
Steven Nguyen 100aea337f fix: update OAuth2 redirect URLs
1. handle non-standard ports if the request came in on a non-standard port
2. use the _APP_CONSOLE_DOMAIN env var to handle cases where the Console domain may not be the endpoint domain such as on Appwrite Cloud
3. use the _APP_OPTIONS_FORCE_HTTPS env var to determine the protocol to be consistent with the rest of the codebase
2025-07-10 15:51:13 -07:00
Steven NguyenandGitHub 435a319fb6 Merge pull request #10130 from appwrite/fix-oauth2-redirect-validation
fix: success validation in oauth2 redirect
2025-07-10 09:06:43 -07:00
Matej Bačo 18492ab917 Revert unwanted change 2025-07-10 16:16:06 +02:00
Matej Bačo 89d54f847f Speed up build cancellation 2025-07-10 15:52:29 +02:00
Matej Bačo 6dc6ff6ba0 Fix formatting 2025-07-10 14:44:44 +02:00
Matej Bačo 581235f972 Fix more tests 2025-07-10 14:43:56 +02:00
Matej Bačo f02ba4c976 Fix function tests 2025-07-10 14:25:53 +02:00
Matej Bačo eaf9b8b228 Migrate remaining tests to node 2025-07-10 12:55:45 +02:00
Steven Nguyen 11332ed1f9 chore: fix deprecation warning from passing null into json_decode 2025-07-09 22:10:17 -07:00
Steven Nguyen 990e1b91bb fix: omit : and @ if user and pass are empty
So, unparsing should not end up with https://:@appwrite.io just because user and pass are empty strings.
2025-07-09 22:09:18 -07:00
Steven Nguyen f202685450 fix: handle parsing URL that is only a scheme
In react native, a redirect URL may only be a url scheme such as appwrite-callback-myproject://. Since parse_url() fails on this type of URL, we need to add this workaround.
2025-07-09 22:01:38 -07:00
Steven NguyenandGitHub 9b90d8030e Merge pull request #10129 from appwrite/dat-579
Changed the default permission param of upsert document
2025-07-09 20:44:36 -07:00
Steven Nguyen 53086fe975 fix: success validation in oauth2 redirect
We switched to using the Redirect class for validating redirect URLs to cover additional cases like react native expo scheme, but we missed this validation.
2025-07-09 15:11:30 -07:00
ArnabChatterjee20k 1d9aac5a49 updated upsert document test with api key 2025-07-09 23:57:39 +05:30
ArnabChatterjee20k ca61b528b9 reverted the api signature and started handling it internally 2025-07-09 23:49:56 +05:30
ArnabChatterjee20k 50109b833a removed tests for the upsert without permissions for passing the tests 2025-07-09 21:49:07 +05:30
Matej Bačo 5ace34790d Migrate many tests to JS 2025-07-09 16:58:55 +02:00
Matej Bačo 1a3f8bbd48 quality fixes 2025-07-09 14:29:36 +02:00
Matej BačoandGitHub 41fc925103 Merge branch '1.7.x' into chore-speed-up-tests 2025-07-09 13:47:33 +02:00
Matej BačoandGitHub a756e533e8 Merge pull request #10128 from appwrite/reduce-delete-batch-size
Reduce delete batch size
2025-07-09 13:46:03 +02:00
Matej BačoandGitHub 449b9295fd Update src/Appwrite/Platform/Workers/Deletes.php 2025-07-09 13:42:18 +02:00
fogelito 3a298b7a21 Change to 500 2025-07-09 14:32:47 +03:00
ArnabChatterjee20k 13bd2341c0 empty commit to trigger tests 2025-07-09 17:02:44 +05:30
ArnabChatterjee20k c36720e3b0 updated param of permissiosns in upsert and updated tests for upsert without permissions 2025-07-09 16:37:41 +05:30
fogelito 9cc61d89f0 $deleteBatchSize 2025-07-09 14:00:08 +03:00
Matej BačoandGitHub cc1129a7b9 Merge pull request #9952 from appwrite/fix-build-activation-race-condition
Fix build activation race condition
2025-07-09 11:59:40 +02:00
Matej Bačo e42275e0b7 Minify tests 2025-07-09 11:26:05 +02:00
Matej Bačo 26496e68c4 Attempt to simplify code 2025-07-08 14:51:28 +02:00
Khushboo VermaandGitHub f7cbe5b961 Merge branch '1.7.x' into fix-build-activation-race-condition 2025-07-08 17:53:12 +05:30
Khushboo Verma bd63f0cf3d Remove empty check 2025-07-08 17:46:54 +05:30
Khushboo Verma bc9caeaae0 Use ruleId 2025-07-08 17:45:45 +05:30
Khushboo Verma a5a1860ba6 Use createdAt instead of buildStartTime 2025-07-08 17:42:23 +05:30
Khushboo Verma 9d7e218e99 Save result of updateDocument 2025-07-08 17:12:35 +05:30
Matej BačoandGitHub 6d321f6988 Merge pull request #10120 from appwrite/fix-file-tokens
Fix file tokens not working on file-security
2025-07-08 13:41:10 +02:00
Khushboo Verma 61a27bfe55 Simplify auto-activation logic and just update attributes 2025-07-08 16:30:43 +05:30
Darshan 7ca951ed81 remove: {} 2025-07-08 15:43:51 +05:30
Darshan 020814e672 update: inline guest headers. 2025-07-08 15:43:28 +05:30
Darshan 7ea896301f update: test to include preview, view and download with file-security. 2025-07-08 15:42:38 +05:30
Darshan 97d5f0f030 add: e2e. 2025-07-08 15:34:52 +05:30
Darshan 14e52058c7 fix: file tokens not working on file-security. 2025-07-08 13:46:53 +05:30
Khushboo Verma 067b53482f Fix bug 2025-07-08 01:02:55 +05:30
Khushboo Verma 1a286ad3f4 Revert replica changes 2025-07-08 00:53:10 +05:30
Khushboo Verma 80c4982482 Update only specific attribute 2025-07-08 00:51:35 +05:30
Khushboo Verma a23c99ad7a Merge branch 'fix-build-activation-race-condition' of github.com:appwrite/appwrite into fix-build-activation-race-condition 2025-07-07 12:37:06 +05:30
Khushboo Verma ea238bc5ec Fix merge conflicts 2025-07-07 12:36:42 +05:30
Damodar LohaniandGitHub edd7a4ac42 Merge pull request #10088 from appwrite/update-nodejs-17.1.0
Update nodejs 17.1.0
2025-07-07 11:14:25 +05:45
Chirag AggarwalandGitHub f678add130 Merge branch '1.7.x' into update-nodejs-17.1.0 2025-07-07 10:39:56 +05:30
Steven NguyenandGitHub 543b872f60 Merge pull request #10107 from appwrite/fix-origin-validation-extensions
fix: origin validation for web extensions
2025-07-06 14:53:56 -07:00
Chirag Aggarwal 5fffae39d4 chore: update naming and add tests 2025-07-05 17:59:01 +05:30
Chirag Aggarwal 9dcb80649f fix: extension for safari 2025-07-05 10:18:40 +05:30
Chirag Aggarwal 18d30c48d5 fix: add missing check for hostname 2025-07-05 10:15:18 +05:30
Chirag Aggarwal 0fb55f6312 fix: origin validation for web extensions 2025-07-05 07:59:52 +05:30
Chirag Aggarwal 7bbf58d0dd update cli 2025-07-04 15:31:52 +05:30
Chirag AggarwalandGitHub 231225ee10 Merge branch '1.7.x' into update-nodejs-17.1.0 2025-07-04 09:37:51 +05:30
Chirag Aggarwal 58c2f55d1c fix: revert to main 2025-07-03 18:51:50 +05:30
Chirag Aggarwal 2cf7246c7a fix: changelog 2025-07-03 18:15:50 +05:30
Chirag Aggarwal dd9096fd2b fix: lint 2025-07-03 18:04:28 +05:30
Chirag Aggarwal 60158aec0c chore: update cli to 8.1.1 as well 2025-07-03 18:02:40 +05:30
Chirag Aggarwal 421510f847 chore: update nodejs to 17.1.0 2025-07-02 11:01:25 +05:30
Khushboo VermaandGitHub 8ef24cb2a9 Merge branch '1.7.x' into fix-build-activation-race-condition 2025-07-01 12:22:17 +05:30
Khushboo Verma ddb5c52e7c Resolve merge conflicts 2025-07-01 02:25:38 +05:30
Khushboo Verma 3ad5a2652e Update console version 2025-06-13 19:14:00 +05:30
Khushboo Verma 88d9bdd395 Merge branch 'fix-build-activation-race-condition' of github.com:appwrite/appwrite into fix-build-activation-race-condition 2025-06-13 19:05:12 +05:30
Khushboo Verma 995482ee20 Update console version 2025-06-13 19:04:46 +05:30
Khushboo Verma d2abdea6cf Resolve merge conflicts 2025-06-13 19:04:17 +05:30
Khushboo VermaandGitHub dde7570072 Merge branch '1.7.x' into fix-build-activation-race-condition 2025-06-10 16:21:51 +05:30
Khushboo Verma e44d1b6600 Merge branch '1.7.x' into fix-build-activation-race-condition 2025-06-03 14:23:51 +05:30
Khushboo Verma 2088792d09 Fix build activation race condition 2025-06-03 14:23:17 +05:30
Khushboo Verma 7a465dc8f8 Don't auto-activate deployment which started earlier & ended later than active deployment 2025-06-02 17:53:35 +05:30
85 changed files with 1154 additions and 813 deletions
+3 -2
View File
@@ -85,8 +85,9 @@ _APP_COMPUTE_MAINTENANCE_INTERVAL=600
_APP_COMPUTE_RUNTIMES_NETWORK=runtimes
_APP_EXECUTOR_SECRET=your-secret-key
_APP_EXECUTOR_HOST=http://exc1/v1
_APP_FUNCTIONS_RUNTIMES=php-8.0,node-18.0,python-3.9,ruby-3.1
_APP_SITES_RUNTIMES=static-1,node-22,flutter-3.32
_APP_BROWSER_HOST=http://appwrite-browser:3000/v1
_APP_FUNCTIONS_RUNTIMES=node-22
_APP_SITES_RUNTIMES=static-1,node-22
_APP_MAINTENANCE_INTERVAL=86400
_APP_MAINTENANCE_START_TIME=12:00
_APP_MAINTENANCE_RETENTION_CACHE=2592000
+85 -2
View File
@@ -178,6 +178,7 @@ jobs:
- name: Load and Start Appwrite
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's|^_APP_BROWSER_HOST=.*|_APP_BROWSER_HOST=http://invalid-browser/v1|' .env
docker compose up -d
sleep 30
@@ -198,7 +199,7 @@ jobs:
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
e2e_shared_mode_test:
name: E2E Shared Mode Service Test
@@ -277,7 +278,7 @@ jobs:
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
e2e_dev_keys:
name: E2E Service Test (Dev Keys)
@@ -358,3 +359,85 @@ jobs:
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys
e2e_screenshots_keys:
name: E2E Service Test (Site Screenshots)
runs-on: ubuntu-latest
needs: setup
steps:
- name: checkout
uses: actions/checkout@v4
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
sleep 30
- name: Run Site tests with browser connected in dedicated table mode
run: |
echo "Keeping original value of _APP_BROWSER_HOST"
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
export _APP_DATABASE_SHARED_TABLES_V1=
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots
e2e_screenshots_shared_mode:
name: E2E Shared Mode Service Test (Site Screenshots)
runs-on: ubuntu-latest
needs: [ setup, check_database_changes ]
if: needs.check_database_changes.outputs.database_changed == 'true'
strategy:
fail-fast: false
matrix:
tables-mode: [
'Shared V1',
'Shared V2',
]
steps:
- name: checkout
uses: actions/checkout@v4
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
sleep 30
- name: Run Site tests with browser connected in ${{ matrix.tables-mode }} table mode
run: |
echo "Keeping original value of _APP_BROWSER_HOST"
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
fi
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots
+7 -6
View File
@@ -59,7 +59,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '17.0.1',
'version' => '17.0.2',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@@ -134,7 +134,7 @@ return [
[
'key' => 'react-native',
'name' => 'React Native',
'version' => '0.10.0',
'version' => '0.10.1',
'url' => 'https://github.com/appwrite/sdk-for-react-native',
'package' => 'https://npmjs.com/package/react-native-appwrite',
'enabled' => true,
@@ -217,7 +217,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '8.1.0',
'version' => '8.2.2',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@@ -231,6 +231,7 @@ return [
'gitRepoName' => 'sdk-for-cli',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'master',
'exclude' => [
'services' => [
['name' => 'assistant'],
@@ -250,7 +251,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '17.0.0',
'version' => '17.1.0',
'url' => 'https://github.com/appwrite/sdk-for-node',
'package' => 'https://www.npmjs.com/package/node-appwrite',
'enabled' => true,
@@ -358,7 +359,7 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.13.0',
'version' => '0.14.0',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => true,
@@ -376,7 +377,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '16.0.0',
'version' => '16.1.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
+86 -29
View File
@@ -1189,9 +1189,16 @@ App::get('/v1/account/sessions/oauth2/:provider')
->inject('response')
->inject('project')
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
$protocol = $request->getProtocol();
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
if ($protocol === 'https' && $port !== '443') {
$callbackBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$callbackBase .= ':' . $port;
}
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
if (!$providerEnabled) {
@@ -1216,12 +1223,20 @@ App::get('/v1/account/sessions/oauth2/:provider')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$host = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$redirectBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
$redirectBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$redirectBase .= ':' . $port;
}
if (empty($success)) {
$success = $protocol . '://' . $request->getHostname() . $oauthDefaultSuccess;
$success = $redirectBase . $oauthDefaultSuccess;
}
if (empty($failure)) {
$failure = $protocol . '://' . $request->getHostname() . $oauthDefaultFailure;
$failure = $redirectBase . $oauthDefaultFailure;
}
$oauth2 = new $className($appId, $appSecret, $callback, [
@@ -1251,9 +1266,14 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->inject('request')
->inject('response')
->action(function (string $projectId, string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response) {
$domain = $request->getHostname();
$protocol = $request->getProtocol();
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
if ($protocol === 'https' && $port !== '443') {
$callbackBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$callbackBase .= ':' . $port;
}
$params = $request->getParams();
$params['project'] = $projectId;
@@ -1262,7 +1282,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
->redirect($callbackBase . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
. \http_build_query($params));
});
@@ -1282,8 +1302,14 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->inject('request')
->inject('response')
->action(function (string $projectId, string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response) {
$domain = $request->getHostname();
$protocol = $request->getProtocol();
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
if ($protocol === 'https' && $port !== '443') {
$callbackBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$callbackBase .= ':' . $port;
}
$params = $request->getParams();
$params['project'] = $projectId;
@@ -1292,7 +1318,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($protocol . '://' . $domain . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
->redirect($callbackBase . '/v1/account/sessions/oauth2/' . $provider . '/redirect?'
. \http_build_query($params));
});
@@ -1316,15 +1342,24 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('request')
->inject('response')
->inject('project')
->inject('platforms')
->inject('devKey')
->inject('user')
->inject('dbForProject')
->inject('geodb')
->inject('queueForEvents')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents) use ($oauthDefaultSuccess) {
$protocol = $request->getProtocol();
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, array $platforms, Document $devKey, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
if ($protocol === 'https' && $port !== '443') {
$callbackBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$callbackBase .= ':' . $port;
}
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => ''];
$validateURL = new URL();
$redirect = new Redirect($platforms);
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
@@ -1351,11 +1386,11 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$state = $defaultState;
}
if (!$validateURL->isValid($state['success'])) {
if ($devKey->isEmpty() && !$redirect->isValid($state['success'])) {
throw new Exception(Exception::PROJECT_INVALID_SUCCESS_URL);
}
if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) {
if ($devKey->isEmpty() && !empty($state['failure']) && !$redirect->isValid($state['failure'])) {
throw new Exception(Exception::PROJECT_INVALID_FAILURE_URL);
}
$failure = [];
@@ -1422,13 +1457,13 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$name = '';
$nameOAuth = $oauth2->getUserName($accessToken);
$userParam = \json_decode($request->getParam('user'), true);
$userParam = $request->getParam('user');
if (!empty($nameOAuth)) {
$name = $nameOAuth;
} elseif (is_array($userParam)) {
$nameParam = $userParam['name'];
if (is_array($nameParam) && isset($nameParam['firstName']) && isset($nameParam['lastName'])) {
$name = $nameParam['firstName'] . ' ' . $nameParam['lastName'];
} elseif ($userParam !== null) {
$userDecoded = \json_decode($userParam, true);
if (isset($userDecoded['name']['firstName']) && isset($userDecoded['name']['lastName'])) {
$name = $userDecoded['name']['firstName'] . ' ' . $userDecoded['name']['lastName'];
}
}
$email = $oauth2->getUserEmail($accessToken);
@@ -1746,8 +1781,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$state['success']['query'] = URLParser::unparseQuery($query);
$state['success'] = URLParser::unparse($state['success']);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
@@ -1785,9 +1818,16 @@ App::get('/v1/account/tokens/oauth2/:provider')
->inject('response')
->inject('project')
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
$protocol = $request->getProtocol();
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
if ($protocol === 'https' && $port !== '443') {
$callbackBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$callbackBase .= ':' . $port;
}
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
if (!$providerEnabled) {
@@ -1812,12 +1852,20 @@ App::get('/v1/account/tokens/oauth2/:provider')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$host = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$redirectBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
$redirectBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$redirectBase .= ':' . $port;
}
if (empty($success)) {
$success = $protocol . '://' . $request->getHostname() . $oauthDefaultSuccess;
$success = $redirectBase . $oauthDefaultSuccess;
}
if (empty($failure)) {
$failure = $protocol . '://' . $request->getHostname() . $oauthDefaultFailure;
$failure = $redirectBase . $oauthDefaultFailure;
}
$oauth2 = new $className($appId, $appSecret, $callback, [
@@ -1960,7 +2008,16 @@ App::post('/v1/account/tokens/magic-url')
$dbForProject->purgeCachedDocument('users', $user->getId());
if (empty($url)) {
$url = $request->getProtocol() . '://' . $request->getHostname() . '/console/auth/magic-url';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$host = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$port = $request->getPort();
$callbackBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
$callbackBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$callbackBase .= ':' . $port;
}
$url = $callbackBase . '/console/auth/magic-url';
}
$url = Template::parseURL($url);
+2 -1
View File
@@ -2958,6 +2958,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
$collection->getAttribute('attributes'),
$dbForProject->getAdapter()->getMaxIndexLength(),
$dbForProject->getAdapter()->getInternalIndexesKeys(),
$dbForProject->getAdapter()->getSupportForIndexArray(),
);
if (!$validator->isValid($index)) {
@@ -4309,7 +4310,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
}
$data['$id'] = $documentId;
$data['$permissions'] = $permissions;
$data['$permissions'] = $permissions ?? [];
$newDocument = new Document($data);
$operations = 0;
+8 -3
View File
@@ -967,6 +967,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
@@ -987,6 +988,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
/* @type Document $file */
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
}
@@ -1157,7 +1159,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->inject('resourceToken')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, ?string $token, Request $request, Response $response, Database $dbForProject, string $mode, Document $resourceToken, Device $deviceForFiles) {
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
@@ -1175,9 +1177,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($fileSecurity && !$valid) {
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
/* @type Document $file */
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
}
@@ -1317,6 +1320,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->inject('resourceToken')
->inject('deviceForFiles')
->action(function (string $bucketId, string $fileId, ?string $token, Response $response, Request $request, Database $dbForProject, string $mode, Document $resourceToken, Device $deviceForFiles) {
/* @type Document $bucket */
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
@@ -1334,9 +1338,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($fileSecurity && !$valid) {
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
/* @type Document $file */
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
}
-26
View File
@@ -129,32 +129,6 @@ App::get('/v1/mock/tests/general/oauth2/failure')
]);
});
App::patch('/v1/mock/functions-v2')
->desc('Update Function Version to V2 (outdated code syntax)')
->groups(['mock', 'api', 'functions'])
->label('scope', 'functions.write')
->label('docs', false)
->param('functionId', '', new UID(), 'Function ID.')
->inject('response')
->inject('dbForProject')
->action(function (string $functionId, Response $response, Database $dbForProject) {
$isDevelopment = System::getEnv('_APP_ENV', 'development') === 'development';
if (!$isDevelopment) {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED);
}
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('version', 'v2'));
$response->noContent();
});
App::post('/v1/mock/api-key-unprefixed')
->desc('Create API Key (without standard prefix)')
->groups(['mock', 'api', 'projects'])
-1
View File
@@ -576,7 +576,6 @@ App::init()
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
+2 -2
View File
@@ -52,7 +52,7 @@
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.71.*",
"utopia-php/database": "dev-validate-create-index-array as 0.71.10",
"utopia-php/detector": "0.1.*",
"utopia-php/domains": "0.8.*",
"utopia-php/dsn": "0.2.1",
@@ -62,7 +62,7 @@
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.18.*",
"utopia-php/migration": "0.10.*",
"utopia-php/migration": "0.11.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*",
Generated
+55 -43
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "55bc52686a08d64930e6af7411ac0654",
"content-hash": "b7f645a2dfd3313b7202189c133ea954",
"packages": [
{
"name": "adhocore/jwt",
@@ -69,16 +69,16 @@
},
{
"name": "appwrite/appwrite",
"version": "11.1.0",
"version": "15.0.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3"
"reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/1d043f543acdb17b9fdb440b1b2dd208e400bad3",
"reference": "1d043f543acdb17b9fdb440b1b2dd208e400bad3",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/deb97b62e0abed8a4fd5c5d48e77365cf89867cf",
"reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf",
"shasum": ""
},
"require": {
@@ -104,10 +104,10 @@
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/11.1.0",
"source": "https://github.com/appwrite/sdk-for-php/tree/15.0.0",
"url": "https://appwrite.io/support"
},
"time": "2024-06-26T07:03:23+00:00"
"time": "2025-05-18T09:47:10+00:00"
},
{
"name": "appwrite/php-clamav",
@@ -3493,16 +3493,16 @@
},
{
"name": "utopia-php/database",
"version": "0.71.9",
"version": "dev-validate-create-index-array",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "eb2f759020bba617e99dd67973a9bd949b47f54e"
"reference": "9e2895b92cb094436973d5f8f30d75014bf03ae6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/eb2f759020bba617e99dd67973a9bd949b47f54e",
"reference": "eb2f759020bba617e99dd67973a9bd949b47f54e",
"url": "https://api.github.com/repos/utopia-php/database/zipball/9e2895b92cb094436973d5f8f30d75014bf03ae6",
"reference": "9e2895b92cb094436973d5f8f30d75014bf03ae6",
"shasum": ""
},
"require": {
@@ -3543,9 +3543,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.71.9"
"source": "https://github.com/utopia-php/database/tree/validate-create-index-array"
},
"time": "2025-07-02T16:37:41+00:00"
"time": "2025-07-20T08:28:54+00:00"
},
{
"name": "utopia-php/detector",
@@ -3993,20 +3993,20 @@
},
{
"name": "utopia-php/migration",
"version": "0.10.4",
"version": "0.11.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "0c85917482db172b3ccdc0704e42af3c1cc89361"
"reference": "d528a454d5c1ed6564b2843a39ff13297bcdb1af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/0c85917482db172b3ccdc0704e42af3c1cc89361",
"reference": "0c85917482db172b3ccdc0704e42af3c1cc89361",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/d528a454d5c1ed6564b2843a39ff13297bcdb1af",
"reference": "d528a454d5c1ed6564b2843a39ff13297bcdb1af",
"shasum": ""
},
"require": {
"appwrite/appwrite": "11.*",
"appwrite/appwrite": "15.*",
"ext-curl": "*",
"ext-openssl": "*",
"php": ">=8.1",
@@ -4043,9 +4043,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/0.10.4"
"source": "https://github.com/utopia-php/migration/tree/0.11.1"
},
"time": "2025-07-02T18:31:09+00:00"
"time": "2025-07-11T13:46:37+00:00"
},
{
"name": "utopia-php/orchestration",
@@ -4810,16 +4810,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.41.9",
"version": "0.41.17",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "61037c1ed9262308cab49c1d760f3278036ab694"
"reference": "ef5e9d952032deecffee7f825a941c40ed4c84b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/61037c1ed9262308cab49c1d760f3278036ab694",
"reference": "61037c1ed9262308cab49c1d760f3278036ab694",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/ef5e9d952032deecffee7f825a941c40ed4c84b8",
"reference": "ef5e9d952032deecffee7f825a941c40ed4c84b8",
"shasum": ""
},
"require": {
@@ -4855,9 +4855,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.9"
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.17"
},
"time": "2025-06-27T10:16:17+00:00"
"time": "2025-07-18T08:18:48+00:00"
},
{
"name": "doctrine/annotations",
@@ -5084,16 +5084,16 @@
},
{
"name": "laravel/pint",
"version": "v1.22.1",
"version": "v1.24.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "941d1927c5ca420c22710e98420287169c7bcaf7"
"reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7",
"reference": "941d1927c5ca420c22710e98420287169c7bcaf7",
"url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a",
"reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a",
"shasum": ""
},
"require": {
@@ -5104,10 +5104,10 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.75.0",
"illuminate/view": "^11.44.7",
"larastan/larastan": "^3.4.0",
"laravel-zero/framework": "^11.36.1",
"friendsofphp/php-cs-fixer": "^3.82.2",
"illuminate/view": "^11.45.1",
"larastan/larastan": "^3.5.0",
"laravel-zero/framework": "^11.45.0",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.1",
"pestphp/pest": "^2.36.0"
@@ -5117,6 +5117,9 @@
],
"type": "project",
"autoload": {
"files": [
"overrides/Runner/Parallel/ProcessFactory.php"
],
"psr-4": {
"App\\": "app/",
"Database\\Seeders\\": "database/seeders/",
@@ -5146,7 +5149,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2025-05-08T08:38:12+00:00"
"time": "2025-07-10T18:09:32+00:00"
},
{
"name": "matthiasmullie/minify",
@@ -5273,16 +5276,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.13.1",
"version": "1.13.3",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c"
"reference": "faed855a7b5f4d4637717c2b3863e277116beb36"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c",
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36",
"reference": "faed855a7b5f4d4637717c2b3863e277116beb36",
"shasum": ""
},
"require": {
@@ -5321,7 +5324,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.1"
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.3"
},
"funding": [
{
@@ -5329,7 +5332,7 @@
"type": "tidelift"
}
],
"time": "2025-04-29T12:36:36+00:00"
"time": "2025-07-05T12:25:42+00:00"
},
{
"name": "nikic/php-parser",
@@ -8234,9 +8237,18 @@
"time": "2024-03-07T20:33:40+00:00"
}
],
"aliases": [],
"aliases": [
{
"package": "utopia-php/database",
"version": "dev-validate-create-index-array",
"alias": "0.71.10",
"alias_normalized": "0.71.10.0"
}
],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": {
"utopia-php/database": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
+3 -2
View File
@@ -214,7 +214,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:6.1.2
image: appwrite/console:6.1.12
restart: unless-stopped
networks:
- appwrite
@@ -456,6 +456,7 @@ services:
- redis
- mariadb
environment:
- _APP_BROWSER_HOST
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
@@ -977,7 +978,7 @@ services:
- OPR_EXECUTOR_ENV=$_APP_ENV
- OPR_EXECUTOR_RUNTIMES=$_APP_FUNCTIONS_RUNTIMES,$_APP_SITES_RUNTIMES
- OPR_EXECUTOR_SECRET=$_APP_EXECUTOR_SECRET
- OPR_EXECUTOR_RUNTIME_VERSIONS=v2,v5
- OPR_EXECUTOR_RUNTIME_VERSIONS=v5
- OPR_EXECUTOR_LOGGING_CONFIG=$_APP_LOGGING_CONFIG
- OPR_EXECUTOR_STORAGE_DEVICE=$_APP_STORAGE_DEVICE
- OPR_EXECUTOR_STORAGE_S3_ACCESS_KEY=$_APP_STORAGE_S3_ACCESS_KEY
@@ -0,0 +1,16 @@
import 'package:dart_appwrite/dart_appwrite.dart';
Client client = Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setSession(''); // The user session to authenticate with
Databases databases = Databases(client);
Document result = await databases.upsertDocument(
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
documentId: '<DOCUMENT_ID>',
data: {},
permissions: ["read("any")"], // (optional)
);
@@ -10,5 +10,5 @@ Databases databases = Databases(client);
DocumentList result = await databases.upsertDocuments(
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
documents: [], // (optional)
documents: [],
);
@@ -0,0 +1,18 @@
using Appwrite;
using Appwrite.Models;
using Appwrite.Services;
Client client = new Client()
.SetEndPoint("https://<REGION>.cloud.appwrite.io/v1") // Your API Endpoint
.SetProject("<YOUR_PROJECT_ID>") // Your project ID
.SetSession(""); // The user session to authenticate with
Databases databases = new Databases(client);
Document result = await databases.UpsertDocument(
databaseId: "<DATABASE_ID>",
collectionId: "<COLLECTION_ID>",
documentId: "<DOCUMENT_ID>",
data: [object],
permissions: ["read("any")"] // optional
);
@@ -12,5 +12,5 @@ Databases databases = new Databases(client);
DocumentList result = await databases.UpsertDocuments(
databaseId: "<DATABASE_ID>",
collectionId: "<COLLECTION_ID>",
documents: new List<object>() // optional
documents: new List<object>()
);
@@ -0,0 +1,16 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>') // Your project ID
.setSession(''); // The user session to authenticate with
const databases = new sdk.Databases(client);
const result = await databases.upsertDocument(
'<DATABASE_ID>', // databaseId
'<COLLECTION_ID>', // collectionId
'<DOCUMENT_ID>', // documentId
{}, // data
["read("any")"] // permissions (optional)
);
@@ -10,5 +10,5 @@ const databases = new sdk.Databases(client);
const result = await databases.upsertDocuments(
'<DATABASE_ID>', // databaseId
'<COLLECTION_ID>', // collectionId
[] // documents (optional)
[] // documents
);
+43
View File
@@ -1,5 +1,48 @@
# Change Log
## 8.2.2
* Fix object comparison logic when pushing settings
* Type generation fixes:
* Dart: Fixed import casing to snake_case, removed `extends Document` and hardcoded attributes, removed unnecessary imports
* Java: Fixed indentation to 4 spaces, updated imports to `java.util.Objects`, fixed enum casing in strict mode as per [Oracle official docs](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html)
* Javascript: Updated optional values formatting from `|null` to `| null`
* Kotlin: Fixed indentation to 4 spaces per [Kotlinlang official docs](https://kotlinlang.org/docs/coding-conventions.html#indentation)
* PHP: Fixed indentation to 4 spaces per [PHP Fig official docs](https://www.php-fig.org/psr/psr-2/)
* Swift: Fixed indentation to 4 spaces, improved `decodeIfPresent` usage for optionals, added missing `public` to `init` method
* Typescript: Fixed indentation to 4 spaces per [Typescript coding guidelines](https://github.com/microsoft/TypeScript/wiki/Coding-guidelines)
## 8.2.1
* Added `--with-variables` option to the Sites command for adding/updating environment variables
* Fixed Functions environment variables not being pushed with `--with-variables`
* Removed `awaitPools` when wiping old variables
> **Note:** Storing environment variables in the `vars` attribute of `appwrite.json` is now deprecated due to security risks. Variables are now synced directly from the `.env` file in the root directory of the functions or sites folder.
## 8.2.0
* Add `encrypt` attribute support
* Add improved warnings on attribute recreation and deletion
* Fix `null` parsing error when using create attribute command
* Type generation fixes and improvements:
* Add `--strict` / `-s` flag to `appwrite types` command to generate types in strict mode. This automatically converts the casing of attributes to match the language's naming conventions
* Add automatic package import to `dart` language which uses package detection to import the correct package
* Add `Document` class extension to generated types in `dart` and `js` language to support internal attributes like `$id` and `$collectionId` etc.
* Add proper enum support to `js` language
* Fix indentation in `java`, `kotlin` and `swift` to use 2 spaces instead of 4 for consistency across all languages
* Fix doc comments to use correct syntax in various languages (for eg. `///` instead of `/*`)
* Update enums in `dart` to use lowerCamelCase in `strict` mode as per [constant_identifier_names](https://dart.dev/tools/diagnostics/constant_identifier_names?utm_source=dartdev&utm_medium=redir&utm_id=diagcode&utm_content=constant_identifier_names)
## 8.1.1
* Fix circular dependency issue due to usage of `success` method in `utils.js` file from `parser.js` file
* Type generation fixes:
* Add ability to generate types directly to a specific file by passing a file path to `appwrite types output_path`, instead of just a directory
* Fix non-required attributes to not be null if default value is provided
* Fix `Models` import error
* Improve formatting and add auto-generated comments
## 8.1.0
* Add multi-region support to `init` command
+8
View File
@@ -1,5 +1,13 @@
# Change Log
## 16.1.0
* Add `gif` support to `ImageFormat` enum
* Add support for `dart38` and `flutter332` runtimes
* Add `upsertDocument()` method to Databases service
* Fix `convertTo()` method in `Document` and `Preferences` models to correctly accept `Map<String, dynamic>`
* Fix `upsertDocuments()` method to have `documents` attribute as required parameter
## 16.0.0
* Add `<REGION>` to doc examples due to the new multi region endpoints
+63 -1
View File
@@ -1 +1,63 @@
# Change Log
# Change Log
## 0.14.0
* Refactor from Newtonsoft.Json to System.Text.Json for serialization/deserialization
* Update package dependencies in `Package.csproj.twig`
* Migrate all serialization/deserialization logic in `Client.cs.twig`, `Query.cs.twig`, and `Extensions.cs.twig`
* Update model attributes from `[JsonProperty]` to `[JsonPropertyName]` in `Model.cs.twig`
* Create new `ObjectToInferredTypesConverter.cs.twig` for proper object type handling
* Replace `JsonConverter` with `JsonConverter<object>` in `ValueClassConverter.cs.twig`
* Update error handling to use `JsonDocument` instead of `JObject`
## 0.13.0
* Add `<REGION>` to doc examples due to the new multi region endpoints
* Add doc examples and methods for bulk api transactions: `createDocuments`, `deleteDocuments` etc.
* Add doc examples, class and methods for new `Sites` service
* Add doc examples, class and methods for new `Tokens` service
* Add enums for `BuildRuntime `, `Adapter`, `Framework`, `DeploymentDownloadType` and `VCSDeploymentType`
* Update enum for `runtimes` with Pythonml312, Dart219, Flutter327 and Flutter329
* Add `token` param to `getFilePreview` and `getFileView` for File tokens usage
* Add `queries` and `search` params to `listMemberships` method
* Remove `search` param from `listExecutions` method
## 0.12.0
* fix: remove content-type from GET requests by @loks0n in https://github.com/appwrite/sdk-for-dotnet/pull/59
* update: min and max are not optional in methods like `UpdateIntegerAttribute` etc.
* chore: regenerate sdk by @ChiragAgg5k in https://github.com/appwrite/sdk-for-dotnet/pull/60
* chore: fix build error by @ChiragAgg5k in https://github.com/appwrite/sdk-for-dotnet/pull/61
## 0.11.0
* Add new push message parameters by @abnegate in https://github.com/appwrite/sdk-for-dotnet/pull/56
## 0.10.0
* fix: chunk upload by @byawitz in https://github.com/appwrite/sdk-for-dotnet/pull/52
## 0.9.0
* Support for Appwrite 1.6
* Added `key` attribute to `Runtime` response model.
* Added `buildSize` attribute to `Deployments` response model.
* Added `scheduledAt` attribute to `Executions` response model.
* Added `scopes` attribute to `Functions` response model.
* Added `specifications` attribute to `Functions` response model.
* Added new response model for `Specifications`.
* Added new response model for `Builds`.
* Added `createJWT()` : Enables creating a JWT using the `userId`.
* Added `listSpecifications()`: Enables listing available runtime specifications.
* Added `deleteExecution()` : Enables deleting executions.
* Added `updateDeploymentBuild()`: Enables cancelling a deployment.
* Added `scheduledAt` parameter to `createExecution()`: Enables creating a delayed execution
#### Breaking changes
You can find the new syntax for breaking changes in the [Appwrite API references](https://appwrite.io/docs/references). Select version `1.6.x`.
* Removed `otp` parameter from `deleteMFAAuthenticator`.
* Added `scopes` parameter for create/update function.
* Renamed `templateBranch` to `templateVersion` in `createFunction()`.
* Renamed `downloadDeployment()` to `getDeploymentDownload()`
> **Please note: This version is compatible with Appwrite 1.6 and later only. If you do not update your Appwrite SDK, old SDKs will not break your app. Appwrite APIs are backwards compatible.**
+12 -1
View File
@@ -1,9 +1,20 @@
# Change Log
## 16.1.1
## 17.0.2
* Add `gif` support to `ImageFormat` enum
* Fix `convertTo()` method in `Document` and `Preferences` models to correctly accept `Map<String, dynamic>`
## 17.0.1
* Fix `devKeys` support by conditionally including credentials during requests
## 17.0.0
* Update `flutter_web_auth_2` dependency to version 4.1.0
* Update `auth.html` example in README.md to align with `flutter_web_auth_2` documentation
* Breaking changes:
* Minimum iOS version supported is now 17.4 due to the updated requirements of `flutter_web_auth_2` version 4.1.0
## 16.1.0
+58 -3
View File
@@ -1,13 +1,68 @@
# Change Log
## 17.1.0
* Add `upsertDocument` method
* Add `dart-3.8` and `flutter-3.32` runtimes
* Add `gif` image format
* Update bulk operation methods to reflect warning message
* Fix file parameter handling in chunked upload method
## 17.0.0
* Add `<REGION>` to doc examples due to the new multi region endpoints
* Add `REGION` to doc examples due to the new multi region endpoints
* Add doc examples and methods for bulk api transactions: `createDocuments`, `deleteDocuments` etc.
* Add doc examples, class and methods for new `Sites` service
* Add doc examples, class and methods for new `Tokens` service
* Add enums for `BuildRuntime `, `Adapter`, `Framework`, `DeploymentDownloadType` and `VCSDeploymentType`
* Add enums for `BuildRuntime`, `Adapter`, `Framework`, `DeploymentDownloadType` and `VCSDeploymentType`
* Updates enum for `runtimes` with Pythonml312, Dart219, Flutter327 and Flutter329
* Add `token` param to `getFilePreview` and `getFileView` for File tokens usage
* Add `queries` and `search` params to `listMemberships` method
* Removes `search` param from `listExecutions` method
* Removes `search` param from `listExecutions` method
## 16.0.0
* Fix: remove content-type from GET requests
* Update (breaking): min and max params are now optional in `updateFloatAttribute` and `updateIntegerAttribute` methods (changes their positioning in method definition)
## 15.0.1
* Remove titles from all function descriptions
* Fix typing for collection "attribute" key
* Remove unnecessary awaits and asyncs
* Ensure `AppwriteException` response is always string
## 15.0.0
* Fix: pong response & chunked upload
## 14.2.0
* Add new push message parameters
## 14.1.0
* Support updating attribute name and size
## 14.0.0
* Support for Appwrite 1.6
* Add `key` attribute to `Runtime` response model.
* Add `buildSize` attribute to `Deployments` response model
* Add `scheduledAt` attribute to `Executions` response model
* Add `scopes` attribute to `Functions` response model
* Add `specifications` attribute to `Functions` response model
* Add new response model for `Specifications`
* Add new response model for `Builds`
* Add `createJWT()` : Enables creating a JWT using the `userId`
* Add `listSpecifications()`: Enables listing available runtime specifications
* Add `deleteExecution()` : Enables deleting executions
* Add `updateDeploymentBuild()`: Enables cancelling a deployment
* Add `scheduledAt` parameter to `createExecution()`: Enables creating a delayed execution
* Breaking changes
* Remove `otp` parameter from `deleteMFAAuthenticator`.
* Add `scopes` parameter for create/update function.
* Rename `templateBranch` to `templateVersion` in `createFunction()`.
* Rename `downloadDeployment()` to `getDeploymentDownload()`
> You can find the new syntax for breaking changes in the [Appwrite API references](https://appwrite.io/docs/references). Select version `1.6.x`.
+5
View File
@@ -1,5 +1,10 @@
# Change log
## 0.10.1
* Fix URL based methods like `getFileViewURL`, `getFilePreviewURL` etc. by adding the missing `projectId` to searchParams
* Add `gif` to ImageFormat enum
## 0.10.0
* Add generate file URL methods like`getFilePreviewURL`, `getFileViewURL` etc.
+20 -17
View File
@@ -267,48 +267,51 @@ class Mapper
}
switch ((!empty($validator)) ? $validator::class : '') {
case 'Appwrite\Network\Validator\CNAME':
case 'Appwrite\Task\Validator\Cron':
case 'Appwrite\Utopia\Database\Validator\CustomId':
case 'Utopia\Validator\Domain':
case 'Appwrite\Network\Validator\Email':
case 'Appwrite\Auth\Validator\Password':
case 'Appwrite\Event\Validator\Event':
case 'Appwrite\Event\Validator\FunctionEvent':
case 'Appwrite\Network\Validator\CNAME':
case 'Appwrite\Network\Validator\Email':
case 'Appwrite\Network\Validator\Redirect':
case 'Appwrite\Network\Validator\DNS':
case 'Appwrite\Network\Validator\Origin':
case 'Appwrite\Task\Validator\Cron':
case 'Appwrite\Utopia\Database\Validator\CustomId':
case 'Utopia\Database\Validator\Key':
case 'Utopia\Database\Validator\UID':
case 'Utopia\Validator\Domain':
case 'Utopia\Validator\HexColor':
case 'Utopia\Validator\Host':
case 'Utopia\Validator\IP':
case 'Utopia\Database\Validator\Key':
case 'Utopia\Validator\Origin':
case 'Appwrite\Auth\Validator\Password':
case 'Utopia\Validator\Text':
case 'Utopia\Database\Validator\UID':
case 'Utopia\Validator\URL':
case 'Utopia\Validator\WhiteList':
default:
$type = Type::string();
break;
case 'Utopia\Database\Validator\Authorization':
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case 'Appwrite\Utopia\Database\Validator\Queries\Buckets':
case 'Appwrite\Utopia\Database\Validator\Queries\Collections':
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Databases':
case 'Appwrite\Utopia\Database\Validator\Queries\Deployments':
case 'Appwrite\Utopia\Database\Validator\Queries\Installations':
case 'Utopia\Database\Validator\Queries\Documents':
case 'Appwrite\Utopia\Database\Validator\Queries\Executions':
case 'Appwrite\Utopia\Database\Validator\Queries\Files':
case 'Appwrite\Utopia\Database\Validator\Queries\Functions':
case 'Appwrite\Utopia\Database\Validator\Queries\Rules':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Installations':
case 'Appwrite\Utopia\Database\Validator\Queries\Memberships':
case 'Utopia\Database\Validator\Permissions':
case 'Appwrite\Utopia\Database\Validator\Queries\Projects':
case 'Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Roles':
case 'Appwrite\Utopia\Database\Validator\Queries\Rules':
case 'Appwrite\Utopia\Database\Validator\Queries\Teams':
case 'Appwrite\Utopia\Database\Validator\Queries\Users':
case 'Appwrite\Utopia\Database\Validator\Queries\Variables':
case 'Utopia\Database\Validator\Authorization':
case 'Utopia\Database\Validator\Permissions':
case 'Utopia\Database\Validator\Queries':
case 'Utopia\Database\Validator\Queries\Documents':
case 'Utopia\Database\Validator\Roles':
$type = Type::listOf(Type::string());
break;
case 'Utopia\Validator\Boolean':
+8
View File
@@ -24,6 +24,10 @@ class Platform
public const SCHEME_HTTP = 'http';
public const SCHEME_HTTPS = 'https';
public const SCHEME_CHROME_EXTENSION = 'chrome-extension';
public const SCHEME_FIREFOX_EXTENSION = 'moz-extension';
public const SCHEME_SAFARI_EXTENSION = 'safari-web-extension';
public const SCHEME_EDGE_EXTENSION = 'ms-browser-extension';
public const SCHEME_IOS = 'appwrite-ios';
public const SCHEME_MACOS = 'appwrite-macos';
public const SCHEME_WATCHOS = 'appwrite-watchos';
@@ -45,6 +49,10 @@ class Platform
self::SCHEME_ANDROID => 'Android',
self::SCHEME_WINDOWS => 'Windows',
self::SCHEME_LINUX => 'Linux',
self::SCHEME_CHROME_EXTENSION => 'Web (Chrome Extension)',
self::SCHEME_FIREFOX_EXTENSION => 'Web (Firefox Extension)',
self::SCHEME_SAFARI_EXTENSION => 'Web (Safari Extension)',
self::SCHEME_EDGE_EXTENSION => 'Web (Edge Extension)',
];
/**
+16 -2
View File
@@ -12,6 +12,7 @@ class Origin extends Validator
protected array $schemes = [];
protected ?string $scheme = null;
protected ?string $host = null;
protected string $origin = '';
/**
* Constructor
@@ -32,6 +33,7 @@ class Origin extends Validator
*/
public function isValid($origin): bool
{
$this->origin = $origin;
$this->scheme = null;
$this->host = null;
@@ -42,7 +44,15 @@ class Origin extends Validator
$this->scheme = $this->parseScheme($origin);
$this->host = strtolower(parse_url($origin, PHP_URL_HOST) ?? '');
if (in_array($this->scheme, [Platform::SCHEME_HTTP, Platform::SCHEME_HTTPS], true)) {
$webPlatforms = [
Platform::SCHEME_HTTP,
Platform::SCHEME_HTTPS,
Platform::SCHEME_CHROME_EXTENSION,
Platform::SCHEME_FIREFOX_EXTENSION,
Platform::SCHEME_SAFARI_EXTENSION,
Platform::SCHEME_EDGE_EXTENSION,
];
if (in_array($this->scheme, $webPlatforms, true)) {
$validator = new Hostname($this->hostnames);
return $validator->isValid($this->host);
}
@@ -60,13 +70,17 @@ class Origin extends Validator
*/
public function getDescription(): string
{
$platform = $this->scheme ? Platform::getNameByScheme($this->scheme) : null;
$platform = $this->scheme ? Platform::getNameByScheme($this->scheme) : '';
$host = $this->host ? '(' . $this->host . ')' : '';
if (empty($this->host) && empty($this->scheme)) {
return 'Invalid Origin.';
}
if (empty($platform)) {
return 'Invalid Scheme. The scheme used (' . $this->scheme . ') in the Origin (' . $this->origin . ') is not supported. If you are using a custom scheme, please change it to `appwrite-callback-<PROJECT_ID>`';
}
return 'Invalid Origin. Register your new client ' . $host . ' as a new '
. $platform . ' platform on your project console dashboard';
}
@@ -63,8 +63,8 @@ class XList extends Base
$spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']);
}
$maxCpus = System::getEnv('_APP_FUNCTIONS_CPUS', 0);
$maxMemory = System::getEnv('_APP_FUNCTIONS_MEMORY', 0);
$maxCpus = System::getEnv('_APP_COMPUTE_CPUS', 0);
$maxMemory = System::getEnv('_APP_COMPUTE_MEMORY', 0);
// Only add specs that are within the limits set by environment variables
// Treat 0 as no limit
@@ -264,7 +264,7 @@ class Builds extends Action
->setParam('deploymentId', $deployment->getId());
if ($deployment->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
$this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime);
return;
}
@@ -275,8 +275,7 @@ class Builds extends Action
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
$resource = $resource->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource);
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
}
$queueForRealtime
@@ -392,7 +391,7 @@ class Builds extends Action
Console::execute('mkdir -p ' . \escapeshellarg('/tmp/builds/' . $deploymentId), '', $stdout, $stderr);
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
$this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime);
return;
}
@@ -525,8 +524,7 @@ class Builds extends Action
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
$resource = $resource->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource);
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
}
$queueForRealtime
@@ -658,7 +656,7 @@ class Builds extends Action
$err = null;
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
$this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime);
return;
}
@@ -809,7 +807,7 @@ class Builds extends Action
]);
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
$this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime);
return;
}
@@ -858,9 +856,7 @@ class Builds extends Action
$adapter = $resource->getAttribute('adapter', '');
if (empty($adapter)) {
$resource->setAttribute('adapter', $detection->getName());
$resource->setAttribute('fallbackFile', $detection->getFallbackFile() ?? '');
$resource = $dbForProject->updateDocument('sites', $resource->getId(), $resource);
$resource = $dbForProject->updateDocument('sites', $resource->getId(), new Document(['adapter' => $detection->getName(), 'fallbackFile' => $detection->getFallbackFile() ?? '']));
$deployment->setAttribute('adapter', $detection->getName());
$deployment->setAttribute('fallbackFile', $detection->getFallbackFile() ?? '');
@@ -959,8 +955,9 @@ class Builds extends Action
$config['sleep'] = $framework['screenshotSleep'];
}
$browserEndpoint = Config::getParam('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1');
$fetchResponse = $client->fetch(
url: 'http://appwrite-browser:3000/v1/screenshots',
url: $browserEndpoint . '/screenshots',
method: 'POST',
body: $config
);
@@ -1062,8 +1059,7 @@ class Builds extends Action
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
$resource = $resource->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource);
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
}
$queueForRealtime
@@ -1077,14 +1073,38 @@ class Builds extends Action
Console::success("Build id: $deploymentId created");
/** Set auto deploy */
$activateBuild = false;
if ($deployment->getAttribute('activate') === true) {
$resource->setAttribute('live', true);
// Check if current active deployment started later than this deployment
$resource = $dbForProject->getDocument($resource->getCollection(), $resource->getId());
$currentActiveDeploymentId = $resource->getAttribute('deploymentId', '');
if (!empty($currentActiveDeploymentId)) {
$currentActiveDeployment = $dbForProject->getDocument('deployments', $currentActiveDeploymentId);
if (!$currentActiveDeployment->isEmpty()) {
$currentActiveStartTime = $currentActiveDeployment->getCreatedAt();
$deploymentStartTime = $deployment->getCreatedAt();
// Skip auto-activation if current active deployment started later than deployment that is being activated
if ($currentActiveStartTime < $deploymentStartTime) {
$activateBuild = true;
} else {
Console::info('Skipping auto-activation as current deployment is more recent');
}
}
} else {
$activateBuild = true;
}
}
if ($activateBuild) {
switch ($resource->getCollection()) {
case 'functions':
$resource->setAttribute('deploymentId', $deployment->getId());
$resource->setAttribute('deploymentInternalId', $deployment->getSequence());
$resource->setAttribute('deploymentCreatedAt', $deployment->getCreatedAt());
$resource = $dbForProject->updateDocument('functions', $resource->getId(), $resource);
$resource = $dbForProject->updateDocument('functions', $resource->getId(), new Document([
'live' => true,
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentCreatedAt' => $deployment->getCreatedAt(),
]));
$queries = [
Query::equal('projectInternalId', [$project->getSequence()]),
@@ -1098,19 +1118,21 @@ class Builds extends Action
$rulesUpdated = false;
$dbForPlatform->forEach('rules', function (Document $rule) use ($dbForPlatform, $deployment, &$rulesUpdated) {
$rulesUpdated = true;
$rule = $rule
->setAttribute('deploymentId', $deployment->getId())
->setAttribute('deploymentInternalId', $deployment->getSequence());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
$rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
]));
}, $queries);
break;
case 'sites':
$resource->setAttribute('deploymentId', $deployment->getId());
$resource->setAttribute('deploymentInternalId', $deployment->getSequence());
$resource->setAttribute('deploymentScreenshotDark', $deployment->getAttribute('screenshotDark', ''));
$resource->setAttribute('deploymentScreenshotLight', $deployment->getAttribute('screenshotLight', ''));
$resource->setAttribute('deploymentCreatedAt', $deployment->getCreatedAt());
$resource = $dbForProject->updateDocument('sites', $resource->getId(), $resource);
$resource = $dbForProject->updateDocument('sites', $resource->getId(), new Document([
'live' => true,
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''),
'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''),
'deploymentCreatedAt' => $deployment->getCreatedAt(),
]));
$queries = [
Query::equal('projectInternalId', [$project->getSequence()]),
Query::equal('type', ['deployment']),
@@ -1121,10 +1143,10 @@ class Builds extends Action
];
$dbForPlatform->forEach('rules', function (Document $rule) use ($dbForPlatform, $deployment) {
$rule = $rule
->setAttribute('deploymentId', $deployment->getId())
->setAttribute('deploymentInternalId', $deployment->getSequence());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
$rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
]));
}, $queries);
break;
@@ -1166,11 +1188,10 @@ class Builds extends Action
'region' => $project->getAttribute('region')
]));
} catch (Duplicate $err) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
$rule = $rule
->setAttribute('deploymentId', $deployment->getId())
->setAttribute('deploymentInternalId', $deployment->getSequence());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
$rule = $dbForPlatform->updateDocument('rules', $ruleId, new Document([
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
]));
}
$queries = [
@@ -1183,10 +1204,10 @@ class Builds extends Action
];
$dbForPlatform->foreach('rules', function (Document $rule) use ($dbForPlatform, $deployment) {
$rule = $rule
->setAttribute('deploymentId', $deployment->getId())
->setAttribute('deploymentInternalId', $deployment->getSequence());
$dbForPlatform->updateDocument('rules', $rule->getId(), $rule);
$rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
]));
}, $queries);
}
}
@@ -1201,7 +1222,7 @@ class Builds extends Action
->trigger();
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
$this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime);
return;
}
@@ -1224,7 +1245,7 @@ class Builds extends Action
Console::error($th->getTraceAsString());
if ($dbForProject->getDocument('deployments', $deploymentId)->getAttribute('status') === 'canceled') {
Console::info('Build has been canceled');
$this->cancelDeployment($deployment->getId(), $dbForProject, $queueForRealtime);
return;
}
@@ -1256,8 +1277,7 @@ class Builds extends Action
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment);
if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) {
$resource = $resource->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource);
$resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')]));
}
$queueForRealtime
@@ -1283,6 +1303,8 @@ class Builds extends Action
protected function sendUsage(Document $resource, Document $deployment, Document $project, StatsUsage $queue): void
{
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
switch ($deployment->getAttribute('status')) {
case 'ready':
$queue
@@ -1511,4 +1533,22 @@ class Builds extends Action
}
}
}
private function cancelDeployment(string $deploymentId, Database $dbForProject, Realtime $queueForRealtime)
{
Console::info('Build has been canceled');
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$logs = $deployment->getAttribute('buildLogs', '');
$date = \date('H:i:s');
$logs .= "[$date] [appwrite] Build has been canceled. \n";
$deployment->setAttribute('buildLogs', $logs);
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
$queueForRealtime
->setPayload($deployment->getArrayCopy())
->trigger();
}
}
@@ -63,8 +63,8 @@ class XList extends Base
$spec['enabled'] = in_array($spec['slug'], $plan['runtimeSpecifications']);
}
$maxCpus = System::getEnv('_APP_FUNCTIONS_CPUS', 0);
$maxMemory = System::getEnv('_APP_FUNCTIONS_MEMORY', 0);
$maxCpus = System::getEnv('_APP_COMPUTE_CPUS', 0);
$maxMemory = System::getEnv('_APP_COMPUTE_MEMORY', 0);
// Only add specs that are within the limits set by environment variables
// Treat 0 as no limit
@@ -37,6 +37,7 @@ class Action extends UtopiaAction
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
return [
'bucket' => $bucket,
'file' => $file,
+3 -2
View File
@@ -272,6 +272,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$gitUrl = 'git@github.com:aw-tests/' . $language['gitRepoName'] . '.git';
}
$repoBranch = $language['repoBranch'] ?? 'main';
if ($git && !empty($gitUrl)) {
\exec('rm -rf ' . $target . ' && \
mkdir -p ' . $target . ' && \
@@ -279,8 +280,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
git init && \
git remote add origin ' . $gitUrl . ' && \
git fetch origin && \
git checkout main || git checkout -b main && \
git pull origin main && \
git checkout ' . $repoBranch . ' || git checkout -b ' . $repoBranch . ' && \
git pull origin ' . $repoBranch . ' && \
git checkout ' . $gitBranch . ' || git checkout -b ' . $gitBranch . ' && \
git fetch origin ' . $gitBranch . ' || git push -u origin ' . $gitBranch . ' && \
git pull origin ' . $gitBranch . ' && \
+4 -1
View File
@@ -1117,6 +1117,9 @@ class Deletes extends Action
): void {
$start = \microtime(true);
$deleteBatchSize = Database::DELETE_BATCH_SIZE;
$deleteBatchSize = 500; // TODO: Set right value in DB library after investigation
/**
* deleteDocuments uses a cursor, we need to add a unique order by field or use default
*/
@@ -1124,7 +1127,7 @@ class Deletes extends Action
$count = $database->deleteDocuments(
$collection,
$queries,
Database::DELETE_BATCH_SIZE,
$deleteBatchSize,
$callback
);
} catch (Throwable $th) {
@@ -360,6 +360,7 @@ class OpenAPI3 extends Format
break;
case 'Utopia\Validator\Host':
case 'Utopia\Validator\URL':
case 'Appwrite\Network\Validator\Redirect':
$node['schema']['type'] = $validator->getType();
$node['schema']['format'] = 'url';
$node['schema']['x-example'] = 'https://example.com';
@@ -385,6 +385,7 @@ class Swagger2 extends Format
break;
case 'Utopia\Validator\Host':
case 'Utopia\Validator\URL':
case 'Appwrite\Network\Validator\Redirect':
$node['type'] = $validator->getType();
$node['format'] = 'url';
$node['x-example'] = 'https://example.com';
+16 -3
View File
@@ -26,7 +26,20 @@ class URL
'fragment' => '',
];
return \array_merge($default, \parse_url($url));
$parsed = \parse_url($url);
if (is_array($parsed)) {
return \array_merge($default, $parsed);
}
// see if $url is just a scheme
if (preg_match('/^([a-z][a-z0-9+.-]*):/i', $url, $matches)) {
$scheme = $matches[1];
return \array_merge($default, [
'scheme' => $scheme
]);
}
throw new \InvalidArgumentException('Invalid URL: ' . $url);
}
/**
@@ -55,9 +68,9 @@ class URL
$parts['user'] = isset($url['user']) ? $url['user'] : '';
$parts['pass'] = isset($url['pass']) ? ':' . $url['pass'] : '';
$parts['pass'] = !empty($url['pass']) ? ':' . $url['pass'] : '';
$parts['pass'] = ($parts['user'] || $parts['pass']) ? $parts['pass'] . '@' : '';
$parts['pass'] = ($parts['user'] || !empty($parts['pass'])) ? $parts['pass'] . '@' : '';
$parts['path'] = isset($url['path']) ? $url['path'] : '';
+3 -3
View File
@@ -675,7 +675,8 @@ class UsageTest extends Scope
[
'functionId' => 'unique()',
'name' => 'Test',
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'vars' => [
'funcKey1' => 'funcValue1',
'funcKey2' => 'funcValue2',
@@ -697,8 +698,7 @@ class UsageTest extends Scope
$this->assertNotEmpty($response['body']['$id']);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true,
]);
$this->assertNotEmpty($deploymentId);
+53 -15
View File
@@ -1343,8 +1343,12 @@ trait DatabasesBase
'type' => 'key',
'attributes' => ['actors'],
]);
$this->assertEquals(202, $actorsArray['headers']['status-code']);
/**
* Disabling array index due to mysql bug
* @link https://github.com/appwrite/appwrite/pull/10169
*/
$this->assertEquals(400, $actorsArray['headers']['status-code']);
$this->assertEquals('Indexing an array attribute is temporarily disabled', $actorsArray['body']['message']);
$twoLevelsArray = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
'content-type' => 'application/json',
@@ -1357,9 +1361,10 @@ trait DatabasesBase
'orders' => ['DESC', 'DESC'],
]);
$this->assertEquals(202, $twoLevelsArray['headers']['status-code']);
$this->assertEquals('DESC', $twoLevelsArray['body']['orders'][0]);
$this->assertEquals(null, $twoLevelsArray['body']['orders'][1]); // Overwrite by API (array)
$this->assertEquals(400, $twoLevelsArray['headers']['status-code']);
$this->assertEquals('Indexing an array attribute is temporarily disabled', $twoLevelsArray['body']['message']);
//$this->assertEquals('DESC', $twoLevelsArray['body']['orders'][0]);
//$this->assertEquals(null, $twoLevelsArray['body']['orders'][1]); // Overwrite by API (array)
$unknown = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
'content-type' => 'application/json',
@@ -1384,7 +1389,8 @@ trait DatabasesBase
'attributes' => ['integers'], // array attribute
'orders' => ['DESC'], // Check order is removed in API
]);
$this->assertEquals(202, $index1['headers']['status-code']);
$this->assertEquals(400, $index1['headers']['status-code']);
$this->assertEquals('Indexing an array attribute is temporarily disabled', $index1['body']['message']);
$index2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
'content-type' => 'application/json',
@@ -1395,7 +1401,8 @@ trait DatabasesBase
'type' => 'key',
'attributes' => ['integers'], // array attribute
]);
$this->assertEquals(202, $index2['headers']['status-code']);
$this->assertEquals(400, $index2['headers']['status-code']);
$this->assertEquals('Indexing an array attribute is temporarily disabled', $index2['body']['message']);
/**
* Create Indexes by worker
@@ -1409,7 +1416,7 @@ trait DatabasesBase
]), []);
$this->assertIsArray($movies['body']['indexes']);
$this->assertCount(8, $movies['body']['indexes']);
$this->assertCount(4, $movies['body']['indexes']);
$this->assertEquals($titleIndex['body']['key'], $movies['body']['indexes'][0]['key']);
$this->assertEquals($releaseYearIndex['body']['key'], $movies['body']['indexes'][1]['key']);
$this->assertEquals($releaseWithDate1['body']['key'], $movies['body']['indexes'][2]['key']);
@@ -1476,14 +1483,15 @@ trait DatabasesBase
'attributes' => ['actors'],
'lengths' => [120]
]);
$this->assertEquals(202, $create['headers']['status-code']);
$this->assertEquals(400, $create['headers']['status-code']);
$this->assertEquals('Indexing an array attribute is temporarily disabled', $create['body']['message']);
$index = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/indexes/lengthOverrideTestIndex", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals([Database::ARRAY_INDEX_LENGTH], $index['body']['lengths']);
// $index = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/indexes/lengthOverrideTestIndex", [
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// 'x-appwrite-key' => $this->getProject()['apiKey']
// ]);
// $this->assertEquals([Database::ARRAY_INDEX_LENGTH], $index['body']['lengths']);
// Test case for count of lengths greater than attributes (should throw 400)
$create = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", [
@@ -1996,6 +2004,36 @@ trait DatabasesBase
],
]);
$this->assertEquals(2, $documents['body']['total']);
// test without passing permissions
$document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
'releaseYear' => 2000
]
]);
$this->assertEquals(200, $document['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $document['body']['title']);
$document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $document['headers']['status-code']);
$deleteResponse = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(204, $deleteResponse['headers']['status-code']);
}
/**
@@ -21,8 +21,8 @@ class FunctionsConsoleClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -39,8 +39,8 @@ class FunctionsConsoleClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test Failure',
'execute' => ['some-random-string'],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
]);
$this->assertEquals(400, $function2['headers']['status-code']);
@@ -453,7 +453,7 @@ class FunctionsConsoleClientTest extends Scope
{
$function = $this->createFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Variable E2E Test',
'entrypoint' => 'index.js',
'logging' => false,
@@ -481,7 +481,7 @@ class FunctionsConsoleClientTest extends Scope
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.js',
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -502,7 +502,7 @@ class FunctionsConsoleClientTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Download Test',
'entrypoint' => 'index.js',
'logging' => false,
@@ -511,7 +511,7 @@ class FunctionsConsoleClientTest extends Scope
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.js',
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -43,8 +43,8 @@ class FunctionsCustomClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -52,8 +52,7 @@ class FunctionsCustomClientTest extends Scope
'timeout' => 10,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -84,13 +83,12 @@ class FunctionsCustomClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-fn'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -107,8 +105,8 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals('Test', $output['APPWRITE_FUNCTION_NAME']);
$this->assertEquals($deploymentId, $output['APPWRITE_FUNCTION_DEPLOYMENT']);
$this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']);
$this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals('Node.js', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('22', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals(APP_VERSION_STABLE, $output['APPWRITE_VERSION']);
$this->assertEquals(System::getEnv('_APP_REGION', 'default'), $output['APPWRITE_REGION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
@@ -142,10 +140,10 @@ class FunctionsCustomClientTest extends Scope
*/
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'name' => 'Test guest execution',
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'vars' => [
'funcKey1' => 'funcValue1',
'funcKey2' => 'funcValue2',
@@ -154,8 +152,7 @@ class FunctionsCustomClientTest extends Scope
'timeout' => 10,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-fn'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -175,8 +172,8 @@ class FunctionsCustomClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
@@ -193,15 +190,14 @@ class FunctionsCustomClientTest extends Scope
*/
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'name' => 'Test synchronous execution',
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-fn'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -214,11 +210,11 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals($functionId, $output['APPWRITE_FUNCTION_ID']);
$this->assertEquals('Test', $output['APPWRITE_FUNCTION_NAME']);
$this->assertEquals('Test synchronous execution', $output['APPWRITE_FUNCTION_NAME']);
$this->assertEquals($deploymentId, $output['APPWRITE_FUNCTION_DEPLOYMENT']);
$this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']);
$this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals('Node.js', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('22', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals(APP_VERSION_STABLE, $output['APPWRITE_VERSION']);
$this->assertEquals(System::getEnv('_APP_REGION', 'default'), $output['APPWRITE_REGION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
@@ -239,12 +235,12 @@ class FunctionsCustomClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::any()->toString()],
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'entrypoint' => 'index.js'
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.js',
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -340,7 +336,7 @@ class FunctionsCustomClientTest extends Scope
'limit' => 5,
'offset' => 2,
'useCases' => ['databases'],
'runtimes' => ['node-16.0']
'runtimes' => ['node-22']
]);
$this->assertEquals(200, $templates['headers']['status-code']);
@@ -352,7 +348,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertContains($template['useCases'][0], ['databases']);
}
$this->assertContains('node-16.0', array_column($templates['body']['templates'][0]['runtimes'], 'name'));
$this->assertContains('node-22', array_column($templates['body']['templates'][0]['runtimes'], 'name'));
/**
* Test for FAILURE
@@ -35,7 +35,7 @@ class FunctionsCustomServerTest extends Scope
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'specification' => $specifications['body']['specifications'][0]['slug']
]);
$this->assertEquals(201, $function['headers']['status-code']);
@@ -50,7 +50,7 @@ class FunctionsCustomServerTest extends Scope
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'specification' => 'cheap-please'
]);
$this->assertEquals(400, $function['headers']['status-code']);
@@ -64,8 +64,8 @@ class FunctionsCustomServerTest extends Scope
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'buckets.*.create',
'buckets.*.delete',
@@ -79,7 +79,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$this->assertEquals('Test', $function['body']['name']);
$this->assertEquals('php-8.0', $function['body']['runtime']);
$this->assertEquals('node-22', $function['body']['runtime']);
$this->assertEquals(true, $dateValidator->isValid($function['body']['$createdAt']));
$this->assertEquals(true, $dateValidator->isValid($function['body']['$updatedAt']));
$this->assertEquals('', $function['body']['deploymentId']);
@@ -180,7 +180,7 @@ class FunctionsCustomServerTest extends Scope
// Test search runtime
$functions = $this->listFunctions([
'search' => 'php-8.0'
'search' => 'node-22'
]);
$this->assertEquals($functions['headers']['status-code'], 200);
@@ -193,8 +193,8 @@ class FunctionsCustomServerTest extends Scope
$this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test 2',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'buckets.*.create',
'buckets.*.delete',
@@ -286,8 +286,8 @@ class FunctionsCustomServerTest extends Scope
],
'schedule' => '0 0 1 1 *',
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
]);
$dateValidator = new DatetimeValidator();
@@ -322,8 +322,8 @@ class FunctionsCustomServerTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -338,8 +338,7 @@ class FunctionsCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey'],
'x-sdk-language' => 'cli',
], [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -363,7 +362,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(200, $starterTemplate['headers']['status-code']);
$phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
return $runtime['name'] === 'php-8.0';
return $runtime['name'] === 'node-22';
}))[0];
// If this fails, the template has variables, and this test needs to be updated
@@ -373,7 +372,7 @@ class FunctionsCustomServerTest extends Scope
[
'functionId' => ID::unique(),
'name' => $starterTemplate['body']['name'],
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'execute' => $starterTemplate['body']['permissions'],
'entrypoint' => $phpRuntime['entrypoint'],
'events' => $starterTemplate['body']['events'],
@@ -514,7 +513,7 @@ class FunctionsCustomServerTest extends Scope
$functionId = $data['functionId'];
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -522,7 +521,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertNotEmpty($deployment['body']['$id']);
$this->assertEquals('waiting', $deployment['body']['status']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt']));
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
$this->assertEquals('index.js', $deployment['body']['entrypoint']);
$deploymentIdActive = $deployment['body']['$id'] ?? '';
@@ -533,7 +532,7 @@ class FunctionsCustomServerTest extends Scope
}, 50000, 500);
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => 'false'
]);
@@ -573,7 +572,7 @@ class FunctionsCustomServerTest extends Scope
$functionId = $data['functionId'];
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => 'false'
]);
@@ -582,7 +581,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt']));
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
$this->assertEquals('index.js', $deployment['body']['entrypoint']);
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
@@ -595,12 +594,12 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('canceled', $deployment['body']['status']);
/**
* Build worker still runs the build.
* 30s sleep gives worker enough time to finish build.
* After build finished, it should still be canceled, not ready.
*/
\sleep(30);
// Ensures worker got eventually aware of cancellation and reacted properly
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertStringContainsString('Build has been canceled.', $deployment['body']['buildLogs']);
});
$deployment = $this->getDeployment($functionId, $deploymentId);
@@ -618,7 +617,7 @@ class FunctionsCustomServerTest extends Scope
*/
$functionId = $data['functionId'];
$folder = 'php-large';
$folder = 'large';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
@@ -633,13 +632,13 @@ class FunctionsCustomServerTest extends Scope
];
$id = '';
while (!feof($handle)) {
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'php-large-fx.tar.gz');
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'large-fx.tar.gz');
$headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size;
if (!empty($id)) {
$headers['x-appwrite-id'] = $id;
}
$largeTag = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge($headers, $this->getHeaders()), [
'entrypoint' => 'index.php',
'entrypoint' => 'index.js',
'code' => $curlFile,
'activate' => true,
'commands' => 'cp blue.mp4 copy.mp4 && ls -al' // +7MB buildSize
@@ -652,7 +651,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(202, $largeTag['headers']['status-code']);
$this->assertNotEmpty($largeTag['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($largeTag['body']['$createdAt']));
$this->assertEquals('index.php', $largeTag['body']['entrypoint']);
$this->assertEquals('index.js', $largeTag['body']['entrypoint']);
$this->assertGreaterThan(1024 * 1024 * 5, $largeTag['body']['sourceSize']); // ~7MB video file
$this->assertLessThan(1024 * 1024 * 10, $largeTag['body']['sourceSize']); // ~7MB video file
@@ -903,8 +902,8 @@ class FunctionsCustomServerTest extends Scope
$this->assertStringContainsString($data['deploymentId'], $execution['body']['responseBody']);
$this->assertStringContainsString('Test1', $execution['body']['responseBody']);
$this->assertStringContainsString('http', $execution['body']['responseBody']);
$this->assertStringContainsString('PHP', $execution['body']['responseBody']);
$this->assertStringContainsString('8.0', $execution['body']['responseBody']);
$this->assertStringContainsString('Node.js', $execution['body']['responseBody']);
$this->assertStringContainsString('22', $execution['body']['responseBody']);
$this->assertStringContainsString('Global Variable Value', $execution['body']['responseBody']);
// $this->assertStringContainsString('êä', $execution['body']['responseBody']); // tests unknown utf-8 chars
$this->assertNotEmpty($execution['body']['errors']);
@@ -1017,8 +1016,8 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertStringContainsString('Test1', $execution['body']['responseBody']);
$this->assertStringContainsString('http', $execution['body']['responseBody']);
$this->assertStringContainsString('PHP', $execution['body']['responseBody']);
$this->assertStringContainsString('8.0', $execution['body']['responseBody']);
$this->assertStringContainsString('Node.js', $execution['body']['responseBody']);
$this->assertStringContainsString('22', $execution['body']['responseBody']);
// $this->assertStringContainsString('êä', $execution['body']['response']); // tests unknown utf-8 chars
$this->assertLessThan(1.500, $execution['body']['duration']);
@@ -1120,8 +1119,8 @@ class FunctionsCustomServerTest extends Scope
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'specification' => Specification::S_1VCPU_1GB,
]);
@@ -1148,8 +1147,8 @@ class FunctionsCustomServerTest extends Scope
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'specification' => Specification::S_1VCPU_512MB,
]);
@@ -1178,8 +1177,8 @@ class FunctionsCustomServerTest extends Scope
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'specification' => 's-2vcpu-512mb', // Invalid specification
]);
@@ -1236,9 +1235,9 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test php-8.0',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test timeout execution',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [],
'schedule' => '',
'timeout' => 5, // Should timeout after 5 seconds
@@ -1280,12 +1279,12 @@ class FunctionsCustomServerTest extends Scope
*/
public function provideCustomExecutions(): array
{
// Most disabled to keep tests fast
return [
['folder' => 'php-fn', 'name' => 'php-8.0', 'entrypoint' => 'index.php', 'runtimeName' => 'PHP', 'runtimeVersion' => '8.0'],
['folder' => 'node', 'name' => 'node-18.0', 'entrypoint' => 'index.js', 'runtimeName' => 'Node.js', 'runtimeVersion' => '18.0'],
['folder' => 'python', 'name' => 'python-3.9', 'entrypoint' => 'main.py', 'runtimeName' => 'Python', 'runtimeVersion' => '3.9'],
['folder' => 'ruby', 'name' => 'ruby-3.1', 'entrypoint' => 'main.rb', 'runtimeName' => 'Ruby', 'runtimeVersion' => '3.1'],
// Swift and Dart disabled on purpose, as it's very slow.
// ['folder' => 'php-fn', 'name' => 'php-8.0', 'entrypoint' => 'index.php', 'runtimeName' => 'PHP', 'runtimeVersion' => '8.0'],
['folder' => 'node', 'name' => 'node-22', 'entrypoint' => 'index.js', 'runtimeName' => 'Node.js', 'runtimeVersion' => '22'],
// ['folder' => 'python', 'name' => 'python-3.9', 'entrypoint' => 'main.py', 'runtimeName' => 'Python', 'runtimeVersion' => '3.9'],
// ['folder' => 'ruby', 'name' => 'ruby-3.1', 'entrypoint' => 'main.rb', 'runtimeName' => 'Ruby', 'runtimeVersion' => '3.1'],
// [ 'folder' => 'dart', 'name' => 'dart-2.15', 'entrypoint' => 'main.dart', 'runtimeName' => 'Dart', 'runtimeVersion' => '2.15' ],
// [ 'folder' => 'swift', 'name' => 'swift-5.5', 'entrypoint' => 'index.swift', 'runtimeName' => 'Swift', 'runtimeVersion' => '5.5' ],
];
@@ -1324,27 +1323,14 @@ class FunctionsCustomServerTest extends Scope
]);
$execution = $this->createExecution($functionId, [
'body' => 'foobar',
'async' => 'false'
]);
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals($functionId, $output['APPWRITE_FUNCTION_ID']);
$this->assertEquals('Test ' . $name, $output['APPWRITE_FUNCTION_NAME']);
$this->assertEquals($deploymentId, $output['APPWRITE_FUNCTION_DEPLOYMENT']);
$this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']);
$this->assertEquals($runtimeName, $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals($runtimeVersion, $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
$this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']);
$this->assertEquals('variable', $output['CUSTOM_VARIABLE']);
$this->assertEmpty($output['APPWRITE_FUNCTION_USER_ID']);
$this->assertEmpty($output['APPWRITE_FUNCTION_JWT']);
$this->assertEquals($this->getProject()['$id'], $output['APPWRITE_FUNCTION_PROJECT_ID']);
$this->assertStringContainsString('Amazing Function Log', $execution['body']['logs']);
$this->assertEquals('OK', $execution['body']['responseBody']);
$this->assertEmpty($execution['body']['logs']);
$this->assertEmpty($execution['body']['errors']);
$executionId = $execution['body']['$id'] ?? '';
@@ -1357,7 +1343,8 @@ class FunctionsCustomServerTest extends Scope
$this->assertCount(1, $executions['body']['executions']);
$this->assertEquals($executions['body']['executions'][0]['$id'], $executionId);
$this->assertEquals($executions['body']['executions'][0]['trigger'], 'http');
$this->assertStringContainsString('Amazing Function Log', $executions['body']['executions'][0]['logs']);
$this->assertEquals(200, $executions['body']['executions'][0]['responseStatusCode']);
$this->assertEmpty($executions['body']['executions'][0]['responseBody']);
$this->cleanupFunction($functionId);
}
@@ -1366,15 +1353,14 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Binary executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-response'),
'code' => $this->packageFunction('binary-response'),
'activate' => true
]);
@@ -1415,15 +1401,14 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Binary executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-request'),
'code' => $this->packageFunction('binary-request'),
'activate' => true
]);
@@ -1461,49 +1446,6 @@ class FunctionsCustomServerTest extends Scope
$this->cleanupFunction($functionId);
}
public function testv2Function()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP V2',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'events' => [],
'timeout' => 15,
]);
$variable = $this->client->call(Client::METHOD_PATCH, '/mock/functions-v2', [
'content-type' => 'application/json',
'origin' => 'http://localhost',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-mode' => 'admin',
], [
'functionId' => $functionId
]);
$this->assertEquals(204, $variable['headers']['status-code']);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-v2'),
'activate' => true
]);
$execution = $this->createExecution($functionId, [
'body' => 'foobar',
'async' => 'false'
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(true, $output['v2Woks']);
$this->cleanupFunction($functionId);
}
public function testGetRuntimes()
{
$runtimes = $this->client->call(Client::METHOD_GET, '/functions/runtimes', array_merge([
@@ -1531,17 +1473,16 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Event executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Event executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
],
'timeout' => 15,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-event'),
'code' => $this->packageFunction('event-handler'),
'activate' => true
]);
@@ -1584,17 +1525,16 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Scopes executions',
'commands' => 'bash setup.sh && composer install',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Scopes executions',
'commands' => 'bash setup.sh && npm install',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'scopes' => ['users.read'],
'timeout' => 15,
]);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-scopes'),
'code' => $this->packageFunction('dynamic-api-key'),
'activate' => true,
]);
@@ -1643,16 +1583,15 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Cookie executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Cookie executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
]);
$this->assertNotEmpty($functionId);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-cookie'),
'code' => $this->packageFunction('cookies'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId);
@@ -1685,9 +1624,9 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Cookie executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Cookie executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1695,8 +1634,7 @@ class FunctionsCustomServerTest extends Scope
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-cookie'),
'code' => $this->packageFunction('cookies'),
'activate' => true
]);
@@ -1743,9 +1681,9 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Binary executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1753,8 +1691,7 @@ class FunctionsCustomServerTest extends Scope
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-response'),
'code' => $this->packageFunction('binary-response'),
'activate' => true
]);
@@ -1778,9 +1715,9 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Binary executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1788,8 +1725,7 @@ class FunctionsCustomServerTest extends Scope
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-request'),
'code' => $this->packageFunction('binary-request'),
'activate' => true
]);
@@ -1816,8 +1752,8 @@ class FunctionsCustomServerTest extends Scope
], $this->getHeaders()), [
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
]);
@@ -1851,8 +1787,8 @@ class FunctionsCustomServerTest extends Scope
$function1Id = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1860,8 +1796,8 @@ class FunctionsCustomServerTest extends Scope
$function2Id = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test2',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1892,7 +1828,7 @@ class FunctionsCustomServerTest extends Scope
{
$function = $this->createFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Logging Test',
'entrypoint' => 'index.js',
'logging' => false,
@@ -1908,7 +1844,7 @@ class FunctionsCustomServerTest extends Scope
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -1985,7 +1921,7 @@ class FunctionsCustomServerTest extends Scope
// Check if the function specifications are correctly set in builds
$function = $this->createFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Specification Test',
'entrypoint' => 'index.js',
'logging' => false,
@@ -2001,7 +1937,7 @@ class FunctionsCustomServerTest extends Scope
$functionId = $functionId = $function['body']['$id'] ?? '';
$deploymentId = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -2027,7 +1963,7 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Duplicate Deployment Test',
'entrypoint' => 'index.js',
'commands' => ''
@@ -2035,7 +1971,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertNotEmpty($functionId);
$deploymentId1 = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId1);
@@ -2045,7 +1981,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertStringContainsString('APPWRITE_FUNCTION_ID', $execution['body']['responseBody']);
$function = $this->updateFunction($functionId, [
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Duplicate Deployment Test',
'entrypoint' => 'index.js',
'commands' => 'rm index.js && mv maintenance.js index.js'
@@ -2089,13 +2025,12 @@ class FunctionsCustomServerTest extends Scope
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'name' => 'Re-activate Test',
'entrypoint' => 'index.php',
'entrypoint' => 'index.js',
]);
$this->assertNotEmpty($functionId);
$function = $this->getFunction($functionId);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertArrayHasKey('latestDeploymentId', $function['body']);
@@ -2106,7 +2041,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEmpty($function['body']['latestDeploymentStatus']);
$deploymentId1 = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php-cookie'),
'code' => $this->packageFunction('cookies'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId1);
@@ -2124,7 +2059,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertStringContainsString('cookieValue', $execution['body']['responseBody']);
$deploymentId2 = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId2);
@@ -2192,8 +2127,8 @@ class FunctionsCustomServerTest extends Scope
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test Error Pages',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'commands' => 'cd non-existing-directory',
'execute' => ['any']
@@ -2203,8 +2138,7 @@ class FunctionsCustomServerTest extends Scope
$proxyClient->setEndpoint('http://' . $domain);
$deployment = $this->createDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -2221,8 +2155,7 @@ class FunctionsCustomServerTest extends Scope
// canceled deployment
$deployment = $this->createDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -2250,8 +2183,8 @@ class FunctionsCustomServerTest extends Scope
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test Error Pages',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'commands' => '',
'execute' => ['users']
@@ -2262,7 +2195,7 @@ class FunctionsCustomServerTest extends Scope
$proxyClient->setEndpoint('http://' . $domain);
$deploymentId = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId);
@@ -2284,8 +2217,8 @@ class FunctionsCustomServerTest extends Scope
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test Error Pages',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'commands' => '',
'execute' => ['any']
@@ -2296,7 +2229,7 @@ class FunctionsCustomServerTest extends Scope
$proxyClient->setEndpoint('http://' . $domain);
$deploymentId = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId);
@@ -25,8 +25,8 @@ class FunctionsScheduleTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -36,8 +36,7 @@ class FunctionsScheduleTest extends Scope
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -77,14 +76,13 @@ class FunctionsScheduleTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
'logging' => true,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -179,15 +177,14 @@ class FunctionsScheduleTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
'logging' => true,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -26,8 +26,8 @@ class FunctionsClientTest extends Scope
'variables' => [
'functionId' => ID::unique(),
'name' => 'Test Function',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'execute' => [Role::any()->toString()],
]
];
@@ -96,7 +96,7 @@ class FunctionsClientTest extends Scope
'map' => \json_encode([
'code' => ["variables.code"]
]),
'code' => $this->packageFunction('php')
'code' => $this->packageFunction('basic')
];
$deployment = $this->client->call(Client::METHOD_POST, '/graphql', [
@@ -26,8 +26,8 @@ class FunctionsServerTest extends Scope
'variables' => [
'functionId' => ID::unique(),
'name' => 'Test Function',
'entrypoint' => 'index.php',
'runtime' => 'php-8.0',
'entrypoint' => 'index.js',
'runtime' => 'node-22',
'execute' => [Role::any()->toString()],
]
];
@@ -95,7 +95,7 @@ class FunctionsServerTest extends Scope
'map' => \json_encode([
'code' => ["variables.code"]
]),
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
];
$deployment = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
@@ -805,13 +805,12 @@ trait MigrationsBase
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php'
'runtime' => 'node-22',
'entrypoint' => 'index.js'
]);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -855,8 +854,8 @@ trait MigrationsBase
$this->assertEquals($functionId, $response['body']['$id']);
$this->assertEquals('Test', $response['body']['name']);
$this->assertEquals('php-8.0', $response['body']['runtime']);
$this->assertEquals('index.php', $response['body']['entrypoint']);
$this->assertEquals('node-22', $response['body']['runtime']);
$this->assertEquals('index.js', $response['body']['entrypoint']);
$this->assertEventually(function () use ($functionId) {
@@ -30,8 +30,6 @@ class ProjectsCustomServerTest extends Scope
'domain' => $testId . '-api.appwrite.test',
]);
\var_dump($response);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [
+2 -2
View File
@@ -227,7 +227,7 @@ trait ProxyBase
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Proxy Function',
'entrypoint' => 'index.js',
'commands' => '',
@@ -244,7 +244,7 @@ trait ProxyBase
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => 'true'
]);
@@ -524,8 +524,8 @@ class RealtimeConsoleClientTest extends Scope
], $this->getHeaders()), [
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -564,8 +564,7 @@ class RealtimeConsoleClientTest extends Scope
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -1285,10 +1285,10 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'functionId' => ID::unique(),
'name' => 'Test',
'name' => 'Test timeout execution',
'execute' => ['users'],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
@@ -1302,7 +1302,6 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('timeout'),
'activate' => true
]);
@@ -14,6 +14,9 @@ class SitesConsoleClientTest extends Scope
use SideConsole;
use SitesBase;
/**
* @group screenshots
*/
public function testSiteScreenshot(): void
{
$siteId = $this->setupSite([
@@ -937,12 +937,12 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('canceled', $deployment['body']['status']);
/**
* Build worker still runs the build.
* 30s sleep gives worker enough time to finish build.
* After build finished, it should still be canceled, not ready.
*/
\sleep(30);
// Ensures worker got eventually aware of cancellation and reacted properly
$this->assertEventually(function () use ($siteId, $deploymentId) {
$deployment = $this->getDeployment($siteId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertStringContainsString('Build has been canceled.', $deployment['body']['buildLogs']);
});
$deployment = $this->getDeployment($siteId, $deploymentId);
+86
View File
@@ -5,6 +5,8 @@ namespace Tests\E2E\Services\Tokens;
use CURLFile;
use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
trait TokensBase
{
@@ -275,4 +277,88 @@ trait TokensBase
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
}
public function testFileAccessWithFileSecurity(): void
{
$bucket = $this->client->call(
Client::METHOD_POST,
'/storage/buckets',
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
],
[
'name' => 'Test Bucket',
'bucketId' => ID::unique(),
'fileSecurity' => true,
'allowedFileExtensions' => ['jpg', 'png', 'jfif'],
]
);
$this->assertEquals(201, $bucket['headers']['status-code']);
$this->assertNotEmpty($bucket['body']['$id']);
$bucketId = $bucket['body']['$id'];
$file = $this->client->call(
Client::METHOD_POST,
'/storage/buckets/' . $bucketId . '/files',
[
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
],
[
'fileId' => ID::unique(),
'permissions' => [ Permission::read(Role::label('devrel')) ],
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
]
);
$fileId = $file['body']['$id'];
$token = $this->client->call(
Client::METHOD_POST,
'/tokens/buckets/' . $bucketId . '/files/' . $fileId,
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]
);
$jwtToken = $token['body']['secret'];
$endpoints = ['preview', 'view', 'download'];
foreach ($endpoints as $endpoint) {
$response = $this->client->call(
Client::METHOD_GET,
"/storage/buckets/$bucketId/files/$fileId/$endpoint",
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
],
[
'token' => $jwtToken
]
);
$this->assertNotEmpty($response['body']);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
if ($endpoint === 'download') {
$image = new \Imagick();
$image->readImageBlob($response['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo.png');
$this->assertEquals($original->getImageWidth(), $image->getImageWidth());
$this->assertEquals($original->getImageHeight(), $image->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
}
}
}
}
@@ -399,8 +399,8 @@ class WebhooksCustomServerTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
@@ -439,8 +439,8 @@ class WebhooksCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'execute' => [Role::any()->toString()],
'vars' => [
'key1' => 'value1',
@@ -496,7 +496,7 @@ class WebhooksCustomServerTest extends Scope
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'entrypoint' => 'index.php',
'entrypoint' => 'index.js',
'code' => new CURLFile($code, 'application/x-gzip', \basename($code)),
'activate' => true
]);
+46
View File
@@ -0,0 +1,46 @@
module.exports = async(context) => {
context.log('log-works');
context.error('error-log-works');
if(context.req.headers['x-appwrite-user-jwt']) {
context.log('jwt-is-valid');
} else {
context.log('jwt-is-invalid');
}
if(context.req.path === '/custom-response') {
const code = +(context.req.query['code'] || '200');
const body = context.req.query['body'] || '';
return context.res.send(body, code);
}
context.log('body-is-' + (context.req.body ?? ''));
context.log('custom-header-is-' + (context.req.headers['x-custom-header'] ?? ''));
context.log('method-is-' + (context.req.method ?? '').toLowerCase());
context.log('path-is-' + (context.req.path ?? ''));
context.log('user-is-' + (context.req.headers['x-appwrite-user-id'] ?? ''));
const statusCode = context.req.query['code'] || '200';
return context.res.json({
'APPWRITE_FUNCTION_ID' : process.env.APPWRITE_FUNCTION_ID ?? '',
'APPWRITE_FUNCTION_NAME' : process.env.APPWRITE_FUNCTION_NAME ?? '',
'APPWRITE_FUNCTION_DEPLOYMENT' : process.env.APPWRITE_FUNCTION_DEPLOYMENT ?? '',
'APPWRITE_FUNCTION_TRIGGER' : context.req.headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' : process.env.APPWRITE_FUNCTION_RUNTIME_NAME,
'APPWRITE_FUNCTION_RUNTIME_VERSION' : process.env.APPWRITE_FUNCTION_RUNTIME_VERSION,
'APPWRITE_VERSION' : process.env.APPWRITE_VERSION ?? '',
'APPWRITE_REGION' : process.env.APPWRITE_REGION ?? '',
'UNICODE_TEST' : "êä",
'GLOBAL_VARIABLE' : process.env.GLOBAL_VARIABLE ?? '',
'APPWRITE_FUNCTION_EVENT' : context.req.headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_USER_ID' : context.req.headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' : context.req.headers['x-appwrite-user-jwt'] ?? '',
'APPWRITE_FUNCTION_PROJECT_ID' : process.env.APPWRITE_FUNCTION_PROJECT_ID,
'APPWRITE_FUNCTION_MEMORY' : process.env.APPWRITE_FUNCTION_MEMORY,
'APPWRITE_FUNCTION_CPUS' : process.env.APPWRITE_FUNCTION_CPUS,
'CUSTOM_VARIABLE' : process.env.CUSTOM_VARIABLE
}, +statusCode);
}
@@ -0,0 +1,6 @@
const crypto = require('crypto')
module.exports = async(context) => {
const hash = crypto.createHash('md5').update(context.req.bodyBinary).digest("hex")
return context.res.send(hash);
};
@@ -0,0 +1,4 @@
module.exports = async(context) => {
const bytes = Buffer.from(Uint8Array.from([0, 10, 255]));
return context.res.binary(bytes);
};
@@ -0,0 +1,3 @@
module.exports = async(context) => {
return context.res.send(context.req.headers['cookie'] ?? '');
};
-21
View File
@@ -1,21 +0,0 @@
import 'dart:io' show Platform;
Future<dynamic> main(final context) async {
context.log('Amazing Function Log');
response.json({
'APPWRITE_FUNCTION_ID' : Platform.environment['APPWRITE_FUNCTION_ID'] ?? '',
'APPWRITE_FUNCTION_NAME' : Platform.environment['APPWRITE_FUNCTION_NAME'] ?? '',
'APPWRITE_FUNCTION_DEPLOYMENT' : Platform.environment['APPWRITE_FUNCTION_DEPLOYMENT'] ?? '',
'APPWRITE_FUNCTION_TRIGGER' : context.req.headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' : Platform.environment['APPWRITE_FUNCTION_RUNTIME_NAME'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' : Platform.environment['APPWRITE_FUNCTION_RUNTIME_VERSION'] ?? '',
'APPWRITE_FUNCTION_EVENT' : context.req.headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_USER_ID' : context.req.headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' : context.req.headers['x-appwrite-user-jwt'] ?? '',
'APPWRITE_FUNCTION_PROJECT_ID' : Platform.environment['APPWRITE_FUNCTION_PROJECT_ID'] ?? '',
'CUSTOM_VARIABLE' : request.variables['CUSTOM_VARIABLE']
});
}
@@ -0,0 +1,15 @@
const sdk = require('node-appwrite');
module.exports = async(context) => {
const client = new sdk.Client();
client.setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT);
client.setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID);
client.setKey(context.req.headers['x-appwrite-key']);
const users = new sdk.Users(client);
const response = await users.list();
context.log(JSON.stringify(response));
return context.res.json(response);
};
@@ -0,0 +1,31 @@
{
"name": "dynamic-api-key",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dynamic-api-key",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"node-appwrite": "^17.0.0"
}
},
"node_modules/node-appwrite": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-17.0.0.tgz",
"integrity": "sha512-5Moi5ENPnoAfU1/6CZP9K2NTuB6Nm3dSyhokno+24RDuP7czjXCdwzfeyjmyHieggbrLkN89AYSOv9W1XkCL9w==",
"license": "BSD-3-Clause",
"dependencies": {
"node-fetch-native-with-agent": "1.7.2"
}
},
"node_modules/node-fetch-native-with-agent": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz",
"integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==",
"license": "MIT"
}
}
}
@@ -0,0 +1,15 @@
{
"name": "dynamic-api-key",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"node-appwrite": "^17.0.0"
}
}
@@ -1,6 +1,7 @@
ENDPOINT="$APPWRITE_FUNCTION_API_ENDPOINT/users"
PROJECT_ID="$APPWRITE_FUNCTION_PROJECT_ID"
API_KEY="$APPWRITE_FUNCTION_API_KEY"
apk add curl
curl -v -X GET $ENDPOINT -H "x-appwrite-project: $PROJECT_ID" -H "x-appwrite-key: $API_KEY"
@@ -0,0 +1,5 @@
module.exports = async(context) => {
context.log(context.req.body.$id);
context.log(context.req.body.name);
return context.res.empty();
};
+3
View File
@@ -0,0 +1,3 @@
module.exports = async(context) => {
return context.res.empty();
};
+1 -19
View File
@@ -1,21 +1,3 @@
module.exports = async(context) => {
context.log('Amazing Function Log');
return context.res.json({
'APPWRITE_FUNCTION_ID' : process.env.APPWRITE_FUNCTION_ID ?? '',
'APPWRITE_FUNCTION_NAME' : process.env.APPWRITE_FUNCTION_NAME ?? '',
'APPWRITE_FUNCTION_DEPLOYMENT' : process.env.APPWRITE_FUNCTION_DEPLOYMENT ?? '',
'APPWRITE_FUNCTION_TRIGGER' : context.req.headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' : process.env.APPWRITE_FUNCTION_RUNTIME_NAME,
'APPWRITE_FUNCTION_RUNTIME_VERSION' : process.env.APPWRITE_FUNCTION_RUNTIME_VERSION,
'APPWRITE_FUNCTION_EVENT' : context.req.headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_USER_ID' : context.req.headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' : context.req.headers['x-appwrite-user-jwt'] ?? '',
'APPWRITE_FUNCTION_PROJECT_ID' : process.env.APPWRITE_FUNCTION_PROJECT_ID,
'APPWRITE_FUNCTION_MEMORY' : process.env.APPWRITE_FUNCTION_MEMORY,
'APPWRITE_FUNCTION_CPUS' : process.env.APPWRITE_FUNCTION_CPUS,
'CUSTOM_VARIABLE' : process.env.CUSTOM_VARIABLE
});
return context.res.send('OK');
}
@@ -1,6 +0,0 @@
<?php
return function ($context) {
$hash = md5($context->req->bodyBinary);
return $context->res->send($hash);
};
@@ -1,6 +0,0 @@
<?php
return function ($context) {
$bytes = pack('C*', ...[0, 10, 255]);
return $context->res->binary($bytes);
};
@@ -1,5 +0,0 @@
<?php
return function ($context) {
return $context->res->send($context->req->headers['cookie'] ?? '');
};
@@ -1,8 +0,0 @@
<?php
return function ($context) {
$context->log($context->req->body['$id']);
$context->log($context->req->body['name']);
return $context->res->empty();
};
@@ -1,23 +0,0 @@
<?php
return function ($context) {
$context->log('Amazing Function Log');
return $context->res->json([
'APPWRITE_FUNCTION_ID' => \getenv('APPWRITE_FUNCTION_ID') ?: '',
'APPWRITE_FUNCTION_NAME' => \getenv('APPWRITE_FUNCTION_NAME') ?: '',
'APPWRITE_FUNCTION_DEPLOYMENT' => \getenv('APPWRITE_FUNCTION_DEPLOYMENT') ?: '',
'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
'APPWRITE_VERSION' => \getenv('APPWRITE_VERSION') ?: '',
'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'APPWRITE_FUNCTION_EVENT' => $context->req->headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' => $context->req->bodyRaw ?? '',
'APPWRITE_FUNCTION_DATA' => $context->req->bodyRaw ?? '',
'APPWRITE_FUNCTION_USER_ID' => $context->req->headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $context->req->headers['x-appwrite-user-jwt'] ?? '',
'APPWRITE_FUNCTION_PROJECT_ID' => \getenv('APPWRITE_FUNCTION_PROJECT_ID') ?: '',
'CUSTOM_VARIABLE' => \getenv('CUSTOM_VARIABLE') ?: '',
]);
};
@@ -1,18 +0,0 @@
{
"name": "appwrite/cloud-function-demo",
"description": "Demo cloud function script",
"type": "library",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Team Appwrite",
"email": "team@appwrite.io"
}
],
"require": {
"php": ">=7.4.0",
"ext-curl": "*",
"ext-json": "*",
"appwrite/appwrite": "1.1.*"
}
}
-64
View File
@@ -1,64 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "afdff6a172e6c44aee11f1562175f81a",
"packages": [
{
"name": "appwrite/appwrite",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "98b327d3fd18a72f4582019916afd735a0e9e0e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/98b327d3fd18a72f4582019916afd735a0e9e0e7",
"reference": "98b327d3fd18a72f4582019916afd735a0e9e0e7",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.1.0"
},
"require-dev": {
"phpunit/phpunit": "3.7.35"
},
"type": "library",
"autoload": {
"psr-4": {
"Appwrite\\": "src/Appwrite"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks",
"support": {
"email": "team@localhost.test",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/1.1.2",
"url": "https://appwrite.io/support"
},
"time": "2020-08-15T18:24:32+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.4.0",
"ext-curl": "*",
"ext-json": "*"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"
}
@@ -1,16 +0,0 @@
<?php
return function ($context) {
return $context->res->json([
'APPWRITE_FUNCTION_ID' => \getenv('APPWRITE_FUNCTION_ID') ?: '',
'APPWRITE_FUNCTION_NAME' => \getenv('APPWRITE_FUNCTION_NAME') ?: '',
'APPWRITE_FUNCTION_DEPLOYMENT' => \getenv('APPWRITE_FUNCTION_DEPLOYMENT') ?: '',
'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'APPWRITE_FUNCTION_CPUS' => \getenv('APPWRITE_FUNCTION_CPUS') ?: '',
'APPWRITE_FUNCTION_MEMORY' => \getenv('APPWRITE_FUNCTION_MEMORY') ?: '',
'UNICODE_TEST' => "êä"
]);
};
@@ -1,18 +0,0 @@
{
"name": "appwrite/php-scopes",
"description": "PHP scopes test script",
"type": "library",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Team Appwrite",
"email": "team@appwrite.io"
}
],
"require": {
"php": ">=7.4.0",
"ext-curl": "*",
"ext-json": "*",
"appwrite/appwrite": "11.0.*"
}
}
@@ -1,18 +0,0 @@
<?php
require 'vendor/autoload.php';
use Appwrite\Client;
use Appwrite\Services\Users;
return function ($context) {
$client = new Client();
$client
->setEndpoint(getenv('APPWRITE_FUNCTION_API_ENDPOINT'))
->setProject(getenv('APPWRITE_FUNCTION_PROJECT_ID'))
->setKey($context->req->headers['x-appwrite-key']);
$users = new Users($client);
$response = $users->list();
$context->log($response);
return $context->res->json($response);
};
@@ -1,7 +0,0 @@
<?php
return function ($req, $res) {
$res->json([
'v2Woks' => true
]);
};
-39
View File
@@ -1,39 +0,0 @@
<?php
return function ($context) {
if ($context->req->path === '/custom-response') {
$code = (int) ($context->req->query['code'] ?? '200');
$body = $context->req->query['body'] ?? '';
return $context->res->send($body, $code);
}
$context->log('body-is-' . ($context->req->body ?? ''));
$context->log('custom-header-is-' . ($context->req->headers['x-custom-header'] ?? ''));
$context->log('method-is-' . \strtolower($context->req->method ?? ''));
$context->log('path-is-' . ($context->req->path ?? ''));
$context->log('user-is-' . $context->req->headers['x-appwrite-user-id'] ?? '');
if (empty($context->req->headers['x-appwrite-user-jwt'] ?? '')) {
$context->log('jwt-is-invalid');
} else {
$context->log('jwt-is-valid');
}
$context->error('error-log-works');
$statusCode = $context->req->query['code'] ?? '200';
return $context->res->json([
'APPWRITE_FUNCTION_ID' => \getenv('APPWRITE_FUNCTION_ID') ?: '',
'APPWRITE_FUNCTION_NAME' => \getenv('APPWRITE_FUNCTION_NAME') ?: '',
'APPWRITE_FUNCTION_DEPLOYMENT' => \getenv('APPWRITE_FUNCTION_DEPLOYMENT') ?: '',
'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'UNICODE_TEST' => "êä",
'GLOBAL_VARIABLE' => \getenv('GLOBAL_VARIABLE') ?: '',
'APPWRITE_FUNCTION_CPUS' => \getenv('APPWRITE_FUNCTION_CPUS') ?: '',
'APPWRITE_FUNCTION_MEMORY' => \getenv('APPWRITE_FUNCTION_MEMORY') ?: '',
], \intval($statusCode));
};
-21
View File
@@ -1,21 +0,0 @@
import json
import os
def main(context):
context.log('Amazing Function Log')
return context.res.json({
'APPWRITE_FUNCTION_ID' : os.environ.get('APPWRITE_FUNCTION_ID',''),
'APPWRITE_FUNCTION_NAME' : os.environ.get('APPWRITE_FUNCTION_NAME',''),
'APPWRITE_FUNCTION_DEPLOYMENT' : os.environ.get('APPWRITE_FUNCTION_DEPLOYMENT',''),
'APPWRITE_FUNCTION_TRIGGER' : context.req.headers.get('x-appwrite-trigger', ''),
'APPWRITE_FUNCTION_RUNTIME_NAME' : os.environ.get('APPWRITE_FUNCTION_RUNTIME_NAME',''),
'APPWRITE_FUNCTION_RUNTIME_VERSION' : os.environ.get('APPWRITE_FUNCTION_RUNTIME_VERSION',''),
'APPWRITE_FUNCTION_EVENT' : context.req.headers.get('x-appwrite-event', ''),
'APPWRITE_FUNCTION_EVENT_DATA' : context.req.body_raw,
'APPWRITE_FUNCTION_DATA' : context.req.body_raw,
'APPWRITE_FUNCTION_USER_ID' : context.req.headers.get('x-appwrite-user-id', ''),
'APPWRITE_FUNCTION_JWT' : context.req.headers.get('x-appwrite-user-jwt', ''),
'APPWRITE_FUNCTION_PROJECT_ID' : os.environ.get('APPWRITE_FUNCTION_PROJECT_ID',''),
'CUSTOM_VARIABLE' : os.environ.get('CUSTOM_VARIABLE',''),
})
-19
View File
@@ -1,19 +0,0 @@
def main(context)
context.log('Amazing Function Log')
return context.res.json({
'APPWRITE_FUNCTION_ID' => ENV['APPWRITE_FUNCTION_ID'] || '',
'APPWRITE_FUNCTION_NAME' => ENV['APPWRITE_FUNCTION_NAME'] || '',
'APPWRITE_FUNCTION_DEPLOYMENT' => ENV['APPWRITE_FUNCTION_DEPLOYMENT'] || '',
'APPWRITE_FUNCTION_TRIGGER' => context.req.headers['x-appwrite-trigger'] || '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => ENV['APPWRITE_FUNCTION_RUNTIME_NAME'] || '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => ENV['APPWRITE_FUNCTION_RUNTIME_VERSION'] || '',
'APPWRITE_FUNCTION_EVENT' => context.req.headers['x-appwrite-event'] || '',
'APPWRITE_FUNCTION_EVENT_DATA' => context.req.body_raw || '',
'APPWRITE_FUNCTION_DATA' => context.req.body_raw || '',
'APPWRITE_FUNCTION_USER_ID' => context.req.headers['x-appwrite-user-id'] || '',
'APPWRITE_FUNCTION_JWT' => context.req.headers['x-appwrite-user-jwt'] || '',
'APPWRITE_FUNCTION_PROJECT_ID' => ENV['APPWRITE_FUNCTION_PROJECT_ID'] || '',
'CUSTOM_VARIABLE' => ENV['CUSTOM_VARIABLE'] || ''
})
end
@@ -1,17 +0,0 @@
func main(req: RequestValue, res: RequestResponse) throws -> RequestResponse {
return res.json(data: [
"APPWRITE_FUNCTION_ID": req.variables["APPWRITE_FUNCTION_ID"],
"APPWRITE_FUNCTION_NAME": req.variables["APPWRITE_FUNCTION_NAME"],
"APPWRITE_FUNCTION_DEPLOYMENT": req.variables["APPWRITE_FUNCTION_DEPLOYMENT"],
"APPWRITE_FUNCTION_TRIGGER": req.variables["APPWRITE_FUNCTION_TRIGGER"],
"APPWRITE_FUNCTION_RUNTIME_NAME": req.variables["APPWRITE_FUNCTION_RUNTIME_NAME"],
"APPWRITE_FUNCTION_RUNTIME_VERSION": req.variables["APPWRITE_FUNCTION_RUNTIME_VERSION"],
"APPWRITE_FUNCTION_EVENT": req.variables["APPWRITE_FUNCTION_EVENT"],
"APPWRITE_FUNCTION_EVENT_DATA": req.variables["APPWRITE_FUNCTION_EVENT_DATA"],
"APPWRITE_FUNCTION_DATA": req.variables["APPWRITE_FUNCTION_DATA"],
"APPWRITE_FUNCTION_USER_ID": req.variables["APPWRITE_FUNCTION_USER_ID"],
"APPWRITE_FUNCTION_JWT": req.variables["APPWRITE_FUNCTION_JWT"],
"APPWRITE_FUNCTION_PROJECT_ID": req.variables["APPWRITE_FUNCTION_PROJECT_ID"],
"CUSTOM_VARIABLE": req.variables["CUSTOM_VARIABLE"]
])
}
@@ -0,0 +1,4 @@
module.exports = async(context) => {
await new Promise(resolve => setTimeout(resolve, 1000 * 60));
return context.res.send('OK');
};
@@ -1,6 +0,0 @@
<?php
return function ($context) {
sleep(60);
return $context->res->send('OK');
};
@@ -94,5 +94,20 @@ class OriginTest extends TestCase
$this->assertEquals(false, $validator->isValid('appwrite-windows://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Windows platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('chrome-extension://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Chrome Extension) platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('moz-extension://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Firefox Extension) platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('safari-web-extension://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Safari Extension) platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('ms-browser-extension://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Edge Extension) platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('random-scheme://localhost'));
$this->assertEquals('Invalid Scheme. The scheme used (random-scheme) in the Origin (random-scheme://localhost) is not supported. If you are using a custom scheme, please change it to `appwrite-callback-<PROJECT_ID>`', $validator->getDescription());
}
}
+22
View File
@@ -26,6 +26,15 @@ class URLTest extends TestCase
$this->assertEquals(null, $url['port']);
$this->assertEquals('', $url['path']);
$this->assertEquals('', $url['query']);
$url = URL::parse('appwrite-callback-project://');
$this->assertIsArray($url);
$this->assertEquals('appwrite-callback-project', $url['scheme']);
$this->assertEquals('', $url['host']);
$this->assertEquals(null, $url['port']);
$this->assertEquals('', $url['path']);
$this->assertEquals('', $url['query']);
}
public function testUnparse(): void
@@ -86,6 +95,19 @@ class URLTest extends TestCase
$this->assertIsString($url);
$this->assertEquals('https://eldad:fux@appwrite.io/#bottom', $url);
$url = URL::unparse([
'scheme' => 'https',
'user' => '',
'pass' => '',
'host' => 'appwrite.io',
'port' => null,
'path' => '',
'fragment' => '',
]);
$this->assertIsString($url);
$this->assertEquals('https://appwrite.io/#', $url);
}
public function testParseQuery(): void