diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..95188b5fa --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,70 @@ +name: CI Workflow + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ['8.1'] + sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode14, CLINode16, DartBeta, DartStable, Deno1193, Deno1303, DotNet60, DotNet70, FlutterStable, FlutterBeta, Go112, Go118, KotlinJava8, KotlinJava11, KotlinJava17, Node12, Node14, Node16, PHP74, PHP80, Python38, Python39, Python310, Ruby27, Ruby30, Ruby31, AppleSwift55, Swift55, WebChromium, WebNode] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Docker Setup Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: curl + + - name: Before Install + run: | + if [ ! -z "${DOCKERHUB_PULL_USERNAME:-}" ]; then + echo "${DOCKERHUB_PULL_PASSWORD}" | docker login --username "${DOCKERHUB_PULL_USERNAME}" --password-stdin + fi + + - name: Install + run: | + docker --version + composer install + + - name: Lint + if: matrix.sdk == 'Lint' + run: | + composer lint + + - name: Run Tests + run: | + composer test tests/${{ matrix.sdk }}Test.php + + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: curl + + - name: Install + run: composer install + + - name: Lint + run: composer lint \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 663053727..000000000 --- a/.travis.yml +++ /dev/null @@ -1,79 +0,0 @@ - -dist: focal - -arch: - - amd64 - -services: - - docker - -os: linux - -language: php - -php: - - 8.1 - -stages: - - lint - - test - -env: - - SDK=Android11Java8 - - SDK=Android11Java11 - - SDK=Android12Java8 - - SDK=Android12Java11 - - SDK=CLINode14 - - SDK=CLINode16 - - SDK=DartBeta - - SDK=DartStable - - SDK=Deno1193 - - SDK=Deno1303 - - SDK=DotNet60 - - SDK=DotNet70 - - SDK=FlutterStable - - SDK=FlutterBeta - - SDK=Go112 - - SDK=Go118 - - SDK=KotlinJava8 - - SDK=KotlinJava11 - - SDK=KotlinJava17 - - SDK=Node12 - - SDK=Node14 - - SDK=Node16 - - SDK=PHP74 - - SDK=PHP80 - - SDK=Python38 - - SDK=Python39 - - SDK=Python310 - - SDK=Ruby27 - - SDK=Ruby30 - - SDK=Ruby31 - - SDK=AppleSwift55 - - SDK=Swift55 - - SDK=WebChromium - - SDK=WebNode - -notifications: - email: - - team@appwrite.io - -jobs: - include: - - stage: lint - name: Lint - install: - - composer install - script: - - composer lint - -before_install: - - > - if [ ! -z "${DOCKERHUB_PULL_USERNAME:-}" ]; then - echo "${DOCKERHUB_PULL_PASSWORD}" | docker login --username "${DOCKERHUB_PULL_USERNAME}" --password-stdin - fi -install: - - docker --version - - composer install -script: - - composer test tests/${SDK}Test.php \ No newline at end of file diff --git a/composer.lock b/composer.lock index eb0e7c2a9..8f0e09370 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "matthiasmullie/minify", - "version": "1.3.70", + "version": "1.3.71", "source": { "type": "git", "url": "https://github.com/matthiasmullie/minify.git", - "reference": "2807d9f9bece6877577ad44acb5c801bb3ae536b" + "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/2807d9f9bece6877577ad44acb5c801bb3ae536b", - "reference": "2807d9f9bece6877577ad44acb5c801bb3ae536b", + "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1", + "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1", "shasum": "" }, "require": { @@ -67,7 +67,7 @@ ], "support": { "issues": "https://github.com/matthiasmullie/minify/issues", - "source": "https://github.com/matthiasmullie/minify/tree/1.3.70" + "source": "https://github.com/matthiasmullie/minify/tree/1.3.71" }, "funding": [ { @@ -75,7 +75,7 @@ "type": "github" } ], - "time": "2022-12-09T12:56:44+00:00" + "time": "2023-04-25T20:33:03+00:00" }, { "name": "matthiasmullie/path-converter", @@ -132,16 +132,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -156,7 +156,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -194,7 +194,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -210,20 +210,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -238,7 +238,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -277,7 +277,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -293,20 +293,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { "name": "twig/twig", - "version": "v3.5.1", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15" + "reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a6e0510cc793912b451fd40ab983a1d28f611c15", - "reference": "a6e0510cc793912b451fd40ab983a1d28f611c15", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a0ce373a0ca3bf6c64b9e3e2124aca502ba39554", + "reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554", "shasum": "" }, "require": { @@ -315,15 +315,10 @@ "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.5-dev" - } - }, "autoload": { "psr-4": { "Twig\\": "src/" @@ -357,7 +352,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.5.1" + "source": "https://github.com/twigphp/Twig/tree/v3.7.1" }, "funding": [ { @@ -369,22 +364,22 @@ "type": "tidelift" } ], - "time": "2023-02-08T07:49:20+00:00" + "time": "2023-08-28T11:09:02+00:00" } ], "packages-dev": [ { "name": "brianium/paratest", - "version": "v6.9.1", + "version": "v6.10.1", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "51691208db882922c55d6c465be3e7d95028c449" + "reference": "d6f32a91302b74458e8ef5d132bb2215a5edb34b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/51691208db882922c55d6c465be3e7d95028c449", - "reference": "51691208db882922c55d6c465be3e7d95028c449", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/d6f32a91302b74458e8ef5d132bb2215a5edb34b", + "reference": "d6f32a91302b74458e8ef5d132bb2215a5edb34b", "shasum": "" }, "require": { @@ -451,7 +446,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.9.1" + "source": "https://github.com/paratestphp/paratest/tree/v6.10.1" }, "funding": [ { @@ -463,7 +458,7 @@ "type": "paypal" } ], - "time": "2023-03-03T09:35:17+00:00" + "time": "2023-10-04T13:33:07+00:00" }, { "name": "doctrine/instantiator", @@ -716,16 +711,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.4", + "version": "v4.17.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", - "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", "shasum": "" }, "require": { @@ -766,9 +761,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" }, - "time": "2023-03-05T19:49:14+00:00" + "time": "2023-08-13T19:53:39+00:00" }, { "name": "phar-io/manifest", @@ -883,16 +878,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.26", + "version": "9.2.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", - "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", "shasum": "" }, "require": { @@ -948,7 +943,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" }, "funding": [ { @@ -956,7 +952,7 @@ "type": "github" } ], - "time": "2023-03-06T12:58:08+00:00" + "time": "2023-09-19T04:57:46+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1201,16 +1197,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.6", + "version": "9.6.13", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115" + "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b65d59a059d3004a040c16a82e07bbdf6cfdd115", - "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", + "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", "shasum": "" }, "require": { @@ -1225,7 +1221,7 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-code-coverage": "^9.2.28", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -1284,7 +1280,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.6" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" }, "funding": [ { @@ -1300,7 +1296,7 @@ "type": "tidelift" } ], - "time": "2023-03-27T11:43:46+00:00" + "time": "2023-09-19T05:39:22+00:00" }, { "name": "psr/container", @@ -1655,16 +1651,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "shasum": "" }, "require": { @@ -1709,7 +1705,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" }, "funding": [ { @@ -1717,7 +1713,7 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-05-07T05:35:17+00:00" }, { "name": "sebastian/environment", @@ -1861,16 +1857,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.5", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + "reference": "bde739e7565280bda77be70044ac1047bc007e34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", "shasum": "" }, "require": { @@ -1913,7 +1909,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" }, "funding": [ { @@ -1921,7 +1917,7 @@ "type": "github" } ], - "time": "2022-02-14T08:28:10+00:00" + "time": "2023-08-02T09:26:13+00:00" }, { "name": "sebastian/lines-of-code", @@ -2378,23 +2374,23 @@ }, { "name": "symfony/console", - "version": "v6.2.8", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b" + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3582d68a64a86ec25240aaa521ec8bc2342b369b", - "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b", + "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6", + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.1|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^1.1|^2|^3", + "symfony/service-contracts": "^2.5|^3", "symfony/string": "^5.4|^6.0" }, "conflict": { @@ -2416,12 +2412,6 @@ "symfony/process": "^5.4|^6.0", "symfony/var-dumper": "^5.4|^6.0" }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, "type": "library", "autoload": { "psr-4": { @@ -2454,7 +2444,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.2.8" + "source": "https://github.com/symfony/console/tree/v6.3.4" }, "funding": [ { @@ -2470,20 +2460,20 @@ "type": "tidelift" } ], - "time": "2023-03-29T21:42:15+00:00" + "time": "2023-08-16T10:10:12+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", - "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -2492,7 +2482,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -2521,7 +2511,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" }, "funding": [ { @@ -2537,20 +2527,20 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:25:55+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -2562,7 +2552,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2602,7 +2592,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -2618,20 +2608,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -2643,7 +2633,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2686,7 +2676,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -2702,20 +2692,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/process", - "version": "v6.2.8", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "75ed64103df4f6615e15a7fe38b8111099f47416" + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/75ed64103df4f6615e15a7fe38b8111099f47416", - "reference": "75ed64103df4f6615e15a7fe38b8111099f47416", + "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", "shasum": "" }, "require": { @@ -2747,7 +2737,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.2.8" + "source": "https://github.com/symfony/process/tree/v6.3.4" }, "funding": [ { @@ -2763,20 +2753,20 @@ "type": "tidelift" } ], - "time": "2023-03-09T16:20:02+00:00" + "time": "2023-08-07T10:39:22+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.2.1", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "a8c9cedf55f314f3a186041d19537303766df09a" + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a", - "reference": "a8c9cedf55f314f3a186041d19537303766df09a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", "shasum": "" }, "require": { @@ -2786,13 +2776,10 @@ "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -2832,7 +2819,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.2.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" }, "funding": [ { @@ -2848,20 +2835,20 @@ "type": "tidelift" } ], - "time": "2023-03-01T10:32:47+00:00" + "time": "2023-05-23T14:45:45+00:00" }, { "name": "symfony/string", - "version": "v6.2.8", + "version": "v6.3.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef" + "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/193e83bbd6617d6b2151c37fff10fa7168ebddef", - "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef", + "url": "https://api.github.com/repos/symfony/string/zipball/13d76d0fb049051ed12a04bef4f9de8715bea339", + "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339", "shasum": "" }, "require": { @@ -2872,13 +2859,13 @@ "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/translation-contracts": "<2.0" + "symfony/translation-contracts": "<2.5" }, "require-dev": { "symfony/error-handler": "^5.4|^6.0", "symfony/http-client": "^5.4|^6.0", "symfony/intl": "^6.2", - "symfony/translation-contracts": "^2.0|^3.0", + "symfony/translation-contracts": "^2.5|^3.0", "symfony/var-exporter": "^5.4|^6.0" }, "type": "library", @@ -2918,7 +2905,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.2.8" + "source": "https://github.com/symfony/string/tree/v6.3.5" }, "funding": [ { @@ -2934,7 +2921,7 @@ "type": "tidelift" } ], - "time": "2023-03-20T16:06:02+00:00" + "time": "2023-09-18T10:38:32+00:00" }, { "name": "theseer/tokenizer", @@ -2999,5 +2986,5 @@ "ext-json": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/mock-server/.gitignore b/mock-server/.gitignore new file mode 100644 index 000000000..3e61b0a96 --- /dev/null +++ b/mock-server/.gitignore @@ -0,0 +1,27 @@ +.idea/ +.dart_tool +/vendor/ +/tests/sdks +/.vscode +.vs +.phpunit.* +.env +.envrc +.hatch + +# exclude everything +examples/* +tests/tmp +.phpunit.result.cache + +# exception to the rule +!examples/.gitkeep + +**/.DS_Store +templates/swift/example/.build +templates/swift/example/Example.xcodeproj/project.xcworkspace/xcuserdata +templates/swift/example/Example.xcodeproj/xcuserdata +**/xcuserdata +# exclude go checksum files +go.sum + diff --git a/mock-server/Dockerfile b/mock-server/Dockerfile new file mode 100644 index 000000000..456aa530e --- /dev/null +++ b/mock-server/Dockerfile @@ -0,0 +1,35 @@ +FROM composer:2.0 as composer + +LABEL maintainer="team@appwrite.io" + +ARG TESTING=false +ENV TESTING=$TESTING + +WORKDIR /usr/local/src/ + +COPY composer.lock /usr/local/src/ +COPY composer.json /usr/local/src/ + +RUN composer install --ignore-platform-reqs --optimize-autoloader \ + --no-plugins --no-scripts --prefer-dist \ + `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` + +FROM phpswoole/swoole:4.8.7-php8.1-alpine as final +RUN ["apk", "add", "docker"] + +ENV _APP_REDIS_HOST=redis \ + _APP_REDIS_PORT=6379 + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +WORKDIR /usr/src/code + +COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor + +# Add Source Code +COPY ./src /usr/src/code/src +COPY ./app /usr/src/code/app + +EXPOSE 80 + +CMD ["php", "app/http.php"] diff --git a/mock-server/app/http.php b/mock-server/app/http.php new file mode 100644 index 000000000..848fb56f5 --- /dev/null +++ b/mock-server/app/http.php @@ -0,0 +1,782 @@ +set([ + 'worker_num' => $workerNumber, + 'open_http2_protocol' => true, + // 'document_root' => __DIR__.'/../public', + // 'enable_static_handler' => true, + 'http_compression' => true, + 'http_compression_level' => 6, + 'package_max_length' => $payloadSize, + 'buffer_output_size' => $payloadSize, + ]); + +// Mock Routes +App::get('/v1/mock/tests/foo') + ->desc('Get Foo') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'foo') + ->label('sdk.method', 'get') + ->label('sdk.description', 'Mock a get request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('x', '', new Text(100), 'Sample string param') + ->param('y', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($x, $y, $z) { + }); + +App::post('/v1/mock/tests/foo') + ->desc('Post Foo') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'foo') + ->label('sdk.method', 'post') + ->label('sdk.description', 'Mock a post request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('x', '', new Text(100), 'Sample string param') + ->param('y', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($x, $y, $z) { + }); + +App::patch('/v1/mock/tests/foo') + ->desc('Patch Foo') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'foo') + ->label('sdk.method', 'patch') + ->label('sdk.description', 'Mock a patch request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('x', '', new Text(100), 'Sample string param') + ->param('y', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($x, $y, $z) { + }); + +App::put('/v1/mock/tests/foo') + ->desc('Put Foo') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'foo') + ->label('sdk.method', 'put') + ->label('sdk.description', 'Mock a put request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('x', '', new Text(100), 'Sample string param') + ->param('y', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($x, $y, $z) { + }); + +App::delete('/v1/mock/tests/foo') + ->desc('Delete Foo') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'foo') + ->label('sdk.method', 'delete') + ->label('sdk.description', 'Mock a delete request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('x', '', new Text(100), 'Sample string param') + ->param('y', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($x, $y, $z) { + }); + +App::get('/v1/mock/tests/bar') + ->desc('Get Bar') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'bar') + ->label('sdk.method', 'get') + ->label('sdk.description', 'Mock a get request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('required', '', new Text(100), 'Sample string param') + ->param('default', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($required, $default, $z) { + }); + +App::post('/v1/mock/tests/bar') + ->desc('Post Bar') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'bar') + ->label('sdk.method', 'post') + ->label('sdk.description', 'Mock a post request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.offline.model', '/mock/tests/bar') + ->label('sdk.offline.key', '{required}') + ->label('sdk.mock', true) + ->param('required', '', new Text(100), 'Sample string param') + ->param('default', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($required, $default, $z) { + }); + +App::patch('/v1/mock/tests/bar') + ->desc('Patch Bar') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'bar') + ->label('sdk.method', 'patch') + ->label('sdk.description', 'Mock a patch request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('required', '', new Text(100), 'Sample string param') + ->param('default', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($required, $default, $z) { + }); + +App::put('/v1/mock/tests/bar') + ->desc('Put Bar') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'bar') + ->label('sdk.method', 'put') + ->label('sdk.description', 'Mock a put request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('required', '', new Text(100), 'Sample string param') + ->param('default', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($required, $default, $z) { + }); + +App::delete('/v1/mock/tests/bar') + ->desc('Delete Bar') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'bar') + ->label('sdk.method', 'delete') + ->label('sdk.description', 'Mock a delete request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('required', '', new Text(100), 'Sample string param') + ->param('default', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->action(function ($required, $default, $z) { + }); + +App::get('/v1/mock/tests/general/headers') + ->desc('Get headers') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'headers') + ->label('sdk.description', 'Return headers from the request') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->inject('request') + ->inject('response') + ->action(function (Request $request, UtopiaSwooleResponse $response) { + $res = [ + 'x-sdk-name' => $request->getHeader('x-sdk-name'), + 'x-sdk-platform' => $request->getHeader('x-sdk-platform'), + 'x-sdk-language' => $request->getHeader('x-sdk-language'), + 'x-sdk-version' => $request->getHeader('x-sdk-version'), + ]; + $res = array_map(function ($key, $value) { + return $key . ': ' . $value; + }, array_keys($res), $res); + $res = implode("; ", $res); + + $response->json(['result' => $res]); + }); + +App::get('/v1/mock/tests/general/download') + ->desc('Download File') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'download') + ->label('sdk.methodType', 'location') + ->label('sdk.description', 'Mock a file download request.') + ->label('sdk.response.type', '*/*') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.mock', true) + ->inject('response') + ->action(function (UtopiaSwooleResponse $response) { + + $response + ->setContentType('text/plain') + ->addHeader('Content-Disposition', 'attachment; filename="test.txt"') + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache + ->addHeader('X-Peak', \memory_get_peak_usage()) + ->send("GET:/v1/mock/tests/general/download:passed"); + }); + +App::post('/v1/mock/tests/general/upload') + ->desc('Upload File') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'upload') + ->label('sdk.description', 'Mock a file upload request.') + ->label('sdk.request.type', 'multipart/form-data') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->param('x', '', new Text(100), 'Sample string param') + ->param('y', '', new Integer(true), 'Sample numeric param') + ->param('z', null, new ArrayList(new Text(256), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Sample array param') + ->param('file', [], new File(), 'Sample file param', skipValidation: true) + ->inject('request') + ->inject('response') + ->action(function (string $x, int $y, array $z, mixed $file, Request $request, UtopiaSwooleResponse $response) { + + $file = $request->getFiles('file'); + + $contentRange = $request->getHeader('content-range'); + + $chunkSize = 5 * 1024 * 1024; // 5MB + + if (!empty($contentRange)) { + $start = $request->getContentRangeStart(); + $end = $request->getContentRangeEnd(); + $size = $request->getContentRangeSize(); + $id = $request->getHeader('x-appwrite-id', ''); + $file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size']; + + if (is_null($start) || is_null($end) || is_null($size) || $end >= $size) { + throw new Exception(Exception::GENERAL_MOCK, 'Invalid content-range header'); + } + + if ($start > $end || $end > $size) { + throw new Exception(Exception::GENERAL_MOCK, 'Invalid content-range header'); + } + + if ($start === 0 && !empty($id)) { + throw new Exception(Exception::GENERAL_MOCK, 'First chunked request cannot have id header'); + } + + if ($start !== 0 && $id !== 'newfileid') { + throw new Exception(Exception::GENERAL_MOCK, 'All chunked request must have id header (except first)'); + } + + if ($end !== $size - 1 && $end - $start + 1 !== $chunkSize) { + throw new Exception(Exception::GENERAL_MOCK, 'Chunk size must be 5MB (except last chunk)'); + } + + if ($end !== $size - 1 && $file['size'] !== $chunkSize) { + throw new Exception(Exception::GENERAL_MOCK, 'Wrong chunk size'); + } + + if ($file['size'] > $chunkSize) { + throw new Exception(Exception::GENERAL_MOCK, 'Chunk size must be 5MB or less'); + } + + if ($end !== $size - 1) { + $response->json([ + '$id' => ID::custom('newfileid'), + 'chunksTotal' => (int) ceil($size / ($end + 1 - $start)), + 'chunksUploaded' => ceil($start / $chunkSize) + 1 + ]); + } + } else { + $file['tmp_name'] = (\is_array($file['tmp_name'])) ? $file['tmp_name'][0] : $file['tmp_name']; + $file['name'] = (\is_array($file['name'])) ? $file['name'][0] : $file['name']; + $file['size'] = (\is_array($file['size'])) ? $file['size'][0] : $file['size']; + + if ($file['name'] !== 'file.png') { + throw new Exception(Exception::GENERAL_MOCK, 'Wrong file name'); + } + + if ($file['size'] !== 38756) { + throw new Exception(Exception::GENERAL_MOCK, 'Wrong file size'); + } + + if (\md5(\file_get_contents($file['tmp_name'])) !== 'd80e7e6999a3eb2ae0d631a96fe135a4') { + throw new Exception(Exception::GENERAL_MOCK, 'Wrong file uploaded'); + } + } + }); + +App::get('/v1/mock/tests/general/redirect') + ->desc('Redirect') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'redirect') + ->label('sdk.description', 'Mock a redirect request.') + ->label('sdk.response.code', Response::STATUS_CODE_MOVED_PERMANENTLY) + ->label('sdk.response.type', Response::CONTENT_TYPE_HTML) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->inject('response') + ->action(function (UtopiaSwooleResponse $response) { + + $response->redirect('/v1/mock/tests/general/redirect/done'); + }); + +App::get('/v1/mock/tests/general/redirect/done') + ->desc('Redirected') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'redirected') + ->label('sdk.description', 'Mock a redirected request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->action(function () { + }); + +App::get('/v1/mock/tests/general/set-cookie') + ->desc('Set Cookie') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'setCookie') + ->label('sdk.description', 'Mock a set cookie request.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->inject('response') + ->inject('request') + ->action(function (UtopiaSwooleResponse $response, Request $request) { + + $response->addCookie('cookieName', 'cookieValue', \time() + 31536000, '/', $request->getHostname(), true, true); + }); + +App::get('/v1/mock/tests/general/get-cookie') + ->desc('Get Cookie') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'getCookie') + ->label('sdk.description', 'Mock a cookie response.') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MOCK) + ->label('sdk.mock', true) + ->inject('request') + ->action(function (Request $request) { + + if ($request->getCookie('cookieName', '') !== 'cookieValue') { + throw new Exception(Exception::GENERAL_MOCK, 'Missing cookie value'); + } + }); + +App::get('/v1/mock/tests/general/empty') + ->desc('Empty Response') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'empty') + ->label('sdk.description', 'Mock an empty response.') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.model', Response::MODEL_NONE) + ->label('sdk.mock', true) + ->inject('response') + ->action(function (UtopiaSwooleResponse $response) { + + $response->noContent(); + }); + +App::post('/v1/mock/tests/general/nullable') + ->desc('Nullable Test') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'nullable') + ->label('sdk.description', 'Mock a nullable parameter.') + ->label('sdk.mock', true) + ->param('required', '', new Text(100), 'Sample string param') + ->param('nullable', '', new Nullable(new Text(100)), 'Sample string param') + ->param('optional', '', new Text(100), 'Sample string param', true) + ->action(function (string $required, string $nullable, ?string $optional) { + }); + +App::post('/v1/mock/tests/general/enum') + ->desc('Enum Test') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'enum') + ->label('sdk.description', 'Mock an enum parameter.') + ->label('sdk.mock', true) + ->param('mockType', '', new WhiteList(['first', 'second', 'third']), 'Sample enum param') + ->action(function (string $mockType) { + }); + +App::get('/v1/mock/tests/general/400-error') + ->desc('400 Error') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'error400') + ->label('sdk.description', 'Mock a 400 failed request.') + ->label('sdk.response.code', Response::STATUS_CODE_BAD_REQUEST) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_ERROR) + ->label('sdk.mock', true) + ->action(function () { + throw new Exception(Exception::GENERAL_MOCK, 'Mock 400 error'); + }); + +App::get('/v1/mock/tests/general/500-error') + ->desc('500 Error') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'error500') + ->label('sdk.description', 'Mock a 500 failed request.') + ->label('sdk.response.code', Response::STATUS_CODE_INTERNAL_SERVER_ERROR) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_ERROR) + ->label('sdk.mock', true) + ->action(function () { + throw new Exception(Exception::GENERAL_MOCK, 'Mock 500 error', 500); + }); + +App::get('/v1/mock/tests/general/502-error') + ->desc('502 Error') + ->groups(['mock']) + ->label('scope', 'public') + ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'general') + ->label('sdk.method', 'error502') + ->label('sdk.description', 'Mock a 502 bad gateway.') + ->label('sdk.response.code', Response::STATUS_CODE_BAD_GATEWAY) + ->label('sdk.response.type', Response::CONTENT_TYPE_TEXT) + ->label('sdk.response.model', Response::MODEL_ANY) + ->label('sdk.mock', true) + ->inject('response') + ->action(function (UtopiaSwooleResponse $response) { + + $response + ->setStatusCode(502) + ->text('This is a text error'); + }); + +App::get('/v1/mock/tests/general/oauth2') + ->desc('OAuth Login') + ->groups(['mock']) + ->label('scope', 'public') + ->label('docs', false) + ->label('sdk.mock', true) + ->param('client_id', '', new Text(100), 'OAuth2 Client ID.') + ->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.') // Important to deny an open redirect attack + ->param('scope', '', new Text(100), 'OAuth2 scope list.') + ->param('state', '', new Text(1024), 'OAuth2 state.') + ->inject('response') + ->action(function (string $client_id, string $redirectURI, string $scope, string $state, UtopiaSwooleResponse $response) { + + $response->redirect($redirectURI . '?' . \http_build_query(['code' => 'abcdef', 'state' => $state])); + }); + +App::get('/v1/mock/tests/general/oauth2/token') + ->desc('OAuth2 Token') + ->groups(['mock']) + ->label('scope', 'public') + ->label('docs', false) + ->label('sdk.mock', true) + ->param('client_id', '', new Text(100), 'OAuth2 Client ID.') + ->param('client_secret', '', new Text(100), 'OAuth2 scope list.') + ->param('grant_type', 'authorization_code', new WhiteList(['refresh_token', 'authorization_code']), 'OAuth2 Grant Type.', true) + ->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.', true) + ->param('code', '', new Text(100), 'OAuth2 state.', true) + ->param('refresh_token', '', new Text(100), 'OAuth2 refresh token.', true) + ->inject('response') + ->action(function (string $client_id, string $client_secret, string $grantType, string $redirectURI, string $code, string $refreshToken, UtopiaSwooleResponse $response) { + + if ($client_id != '1') { + throw new Exception(Exception::GENERAL_MOCK, 'Invalid client ID'); + } + + if ($client_secret != '123456') { + throw new Exception(Exception::GENERAL_MOCK, 'Invalid client secret'); + } + + $responseJson = [ + 'access_token' => '123456', + 'refresh_token' => 'tuvwxyz', + 'expires_in' => 14400 + ]; + + if ($grantType === 'authorization_code') { + if ($code !== 'abcdef') { + throw new Exception(Exception::GENERAL_MOCK, 'Invalid token'); + } + + $response->json($responseJson); + } elseif ($grantType === 'refresh_token') { + if ($refreshToken !== 'tuvwxyz') { + throw new Exception(Exception::GENERAL_MOCK, 'Invalid refresh token'); + } + + $response->json($responseJson); + } else { + throw new Exception(Exception::GENERAL_MOCK, 'Invalid grant type'); + } + }); + +App::get('/v1/mock/tests/general/oauth2/user') + ->desc('OAuth2 User') + ->groups(['mock']) + ->label('scope', 'public') + ->label('docs', false) + ->param('token', '', new Text(100), 'OAuth2 Access Token.') + ->inject('response') + ->action(function (string $token, UtopiaSwooleResponse $response) { + + if ($token != '123456') { + throw new Exception(Exception::GENERAL_MOCK, 'Invalid token'); + } + + $response->json([ + 'id' => 1, + 'name' => 'User Name', + 'email' => 'useroauth@localhost.test', + ]); + }); + +App::get('/v1/mock/tests/general/oauth2/success') + ->desc('OAuth2 Success') + ->groups(['mock']) + ->label('scope', 'public') + ->label('docs', false) + ->inject('response') + ->action(function (UtopiaSwooleResponse $response) { + + $response->json([ + 'result' => 'success', + ]); + }); + +App::get('/v1/mock/tests/general/oauth2/failure') + ->desc('OAuth2 Failure') + ->groups(['mock']) + ->label('scope', 'public') + ->label('docs', false) + ->inject('response') + ->action(function (UtopiaSwooleResponse $response) { + + $response + ->setStatusCode(Response::STATUS_CODE_BAD_REQUEST) + ->json([ + 'result' => 'failure', + ]); + }); + +App::shutdown() + ->groups(['mock']) + ->inject('utopia') + ->inject('response') + ->inject('request') + ->action(function (App $utopia, UtopiaSwooleResponse $response, Request $request) { + + $result = []; + $route = $utopia->getRoute(); + $path = APP_STORAGE_CACHE . '/tests.json'; + $tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : []; + + if (!\is_array($tests)) { + throw new Exception(Exception::GENERAL_MOCK, 'Failed to read results', 500); + } + + $result[$route->getMethod() . ':' . $route->getPath()] = true; + + $tests = \array_merge($tests, $result); + + if (!\file_put_contents($path, \json_encode($tests), LOCK_EX)) { + throw new Exception(Exception::GENERAL_MOCK, 'Failed to save results', 500); + } + + $response->json(['result' => $route->getMethod() . ':' . $route->getPath() . ':passed']); + }); + +App::error() + ->inject('utopia') + ->inject('error') + ->inject('request') + ->inject('response') + ->action( + function ($utopia, $error, $request, $response) { + $route = $utopia->match($request); + + $code = $error->getCode(); + $message = $error->getMessage(); + $file = $error->getFile(); + $line = $error->getLine(); + $trace = $error->getTrace(); + + if (php_sapi_name() === 'cli') { + Console::error('[Error] Timestamp: ' . date('c', time())); + + if ($route) { + Console::error('[Error] Method: ' . $route->getMethod()); + Console::error('[Error] URL: ' . $route->getPath()); + } + + Console::error('[Error] Type: ' . get_class($error)); + Console::error('[Error] Message: ' . $message); + Console::error('[Error] File: ' . $file); + Console::error('[Error] Line: ' . $line); + } + + switch ($code) { + case 400: // Error allowed publicly + case 401: // Error allowed publicly + case 402: // Error allowed publicly + case 403: // Error allowed publicly + case 404: // Error allowed publicly + case 406: // Error allowed publicly + case 409: // Error allowed publicly + case 412: // Error allowed publicly + case 425: // Error allowed publicly + case 429: // Error allowed publicly + case 501: // Error allowed publicly + case 503: // Error allowed publicly + $code = $error->getCode(); + break; + default: + $code = 500; // All other errors get the generic 500 server error status code + } + + $output = ((App::isDevelopment())) ? [ + 'message' => $message, + 'code' => $code, + 'file' => $file, + 'line' => $line, + 'trace' => $trace, + ] : [ + 'message' => $message, + 'code' => $code, + ]; + + $response + ->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate') + ->addHeader('Expires', '0') + ->addHeader('Pragma', 'no-cache') + ->setStatusCode($code); + + $response->json($output); + }, + ['utopia', 'error', 'request', 'response'] + ); + +$http->on( + 'start', + function (Server $http) use ($payloadSize) { + Console::success('Server started successfully (max payload is ' . number_format($payloadSize) . ' bytes)'); + Console::info("Master pid {$http->master_pid}, manager pid {$http->manager_pid}"); + // listen ctrl + c + Process::signal( + 2, + function () use ($http) { + Console::log('Stop by Ctrl+C'); + $http->shutdown(); + } + ); + } +); + +$http->on( + 'request', + function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { + $request = new Request($swooleRequest); + $response = new UtopiaSwooleResponse($swooleResponse); + + $app = new App('UTC'); + + $app->run($request, $response); + } +); + +$http->start(); diff --git a/mock-server/composer.json b/mock-server/composer.json new file mode 100644 index 000000000..faf40c69f --- /dev/null +++ b/mock-server/composer.json @@ -0,0 +1,24 @@ +{ + "name": "utopia/mock-server", + "description": "Mock server for appwrite/sdk-generator", + "autoload": { + "psr-4": { + "Utopia\\MockServer\\": "src/" + } + }, + "authors": [ + { + "name": "Bradley Schofield", + "email": "bradley@appwrite.io" + } + ], + "require": { + "utopia-php/framework": "^0.31.0", + "utopia-php/database": "^0.44.2", + "utopia-php/cli": "^0.16.0", + "utopia-php/swoole": "^0.5.0" + }, + "require-dev": { + "swoole/ide-helper": "^5.1" + } +} diff --git a/mock-server/composer.lock b/mock-server/composer.lock new file mode 100644 index 000000000..45311434f --- /dev/null +++ b/mock-server/composer.lock @@ -0,0 +1,573 @@ +{ + "_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": "64978d2ed8d7602b1a2430086a54fbbd", + "packages": [ + { + "name": "jean85/pretty-package-versions", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.17", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^0.12.66", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + }, + "time": "2021-10-08T21:21:46+00:00" + }, + { + "name": "mongodb/mongodb", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/mongodb/mongo-php-library.git", + "reference": "b0bbd657f84219212487d01a8ffe93a789e1e488" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/b0bbd657f84219212487d01a8ffe93a789e1e488", + "reference": "b0bbd657f84219212487d01a8ffe93a789e1e488", + "shasum": "" + }, + "require": { + "ext-hash": "*", + "ext-json": "*", + "ext-mongodb": "^1.11.0", + "jean85/pretty-package-versions": "^1.2 || ^2.0.1", + "php": "^7.1 || ^8.0", + "symfony/polyfill-php80": "^1.19" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "squizlabs/php_codesniffer": "^3.6", + "symfony/phpunit-bridge": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "MongoDB\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Andreas Braun", + "email": "andreas.braun@mongodb.com" + }, + { + "name": "Jeremy Mikola", + "email": "jmikola@gmail.com" + } + ], + "description": "MongoDB driver library", + "homepage": "https://jira.mongodb.org/browse/PHPLIB", + "keywords": [ + "database", + "driver", + "mongodb", + "persistence" + ], + "support": { + "issues": "https://github.com/mongodb/mongo-php-library/issues", + "source": "https://github.com/mongodb/mongo-php-library/tree/1.10.0" + }, + "time": "2021-10-20T22:22:37+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "utopia-php/cache", + "version": "0.8.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/cache.git", + "reference": "212e66100a1f32e674fca5d9bc317cc998303089" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/212e66100a1f32e674fca5d9bc317cc998303089", + "reference": "212e66100a1f32e674fca5d9bc317cc998303089", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-memcached": "*", + "ext-redis": "*", + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.13.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Cache\\": "src/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple cache library to manage application cache storing, loading and purging", + "keywords": [ + "cache", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/cache/issues", + "source": "https://github.com/utopia-php/cache/tree/0.8.0" + }, + "time": "2022-10-16T16:48:09+00:00" + }, + { + "name": "utopia-php/cli", + "version": "0.16.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/cli.git", + "reference": "5b936638c90c86d1bae83d0dbe81fe14d12ff8ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/5b936638c90c86d1bae83d0dbe81fe14d12ff8ff", + "reference": "5b936638c90c86d1bae83d0dbe81fe14d12ff8ff", + "shasum": "" + }, + "require": { + "php": ">=7.4", + "utopia-php/framework": "0.*.*" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.6", + "vimeo/psalm": "4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\CLI\\": "src/CLI" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple CLI library to manage command line applications", + "keywords": [ + "cli", + "command line", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/cli/issues", + "source": "https://github.com/utopia-php/cli/tree/0.16.0" + }, + "time": "2023-08-05T13:13:08+00:00" + }, + { + "name": "utopia-php/database", + "version": "0.44.2", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/database.git", + "reference": "591cadbc2c622a3304aae9a16ebfdbe75ef33a06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/database/zipball/591cadbc2c622a3304aae9a16ebfdbe75ef33a06", + "reference": "591cadbc2c622a3304aae9a16ebfdbe75ef33a06", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-pdo": "*", + "php": ">=8.0", + "utopia-php/cache": "0.8.*", + "utopia-php/framework": "0.*.*", + "utopia-php/mongo": "0.3.*" + }, + "require-dev": { + "fakerphp/faker": "^1.14", + "laravel/pint": "1.4.*", + "pcov/clobber": "^2.0", + "phpstan/phpstan": "1.10.*", + "phpunit/phpunit": "^9.4", + "rregeer/phpunit-coverage-check": "^0.3.1", + "swoole/ide-helper": "4.8.0", + "utopia-php/cli": "^0.14.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Database\\": "src/Database" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple library to manage application persistence using multiple database adapters", + "keywords": [ + "database", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/database/issues", + "source": "https://github.com/utopia-php/database/tree/0.44.2" + }, + "time": "2023-10-19T07:39:00+00:00" + }, + { + "name": "utopia-php/framework", + "version": "0.31.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/framework.git", + "reference": "207f77378965fca9a9bc3783ea379d3549f86bc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/207f77378965fca9a9bc3783ea379d3549f86bc0", + "reference": "207f77378965fca9a9bc3783ea379d3549f86bc0", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple, light and advanced PHP framework", + "keywords": [ + "framework", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/framework/issues", + "source": "https://github.com/utopia-php/framework/tree/0.31.0" + }, + "time": "2023-08-30T16:10:04+00:00" + }, + { + "name": "utopia-php/mongo", + "version": "0.3.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/mongo.git", + "reference": "52326a9a43e2d27ff0c15c48ba746dacbe9a7aee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/52326a9a43e2d27ff0c15c48ba746dacbe9a7aee", + "reference": "52326a9a43e2d27ff0c15c48ba746dacbe9a7aee", + "shasum": "" + }, + "require": { + "ext-mongodb": "*", + "mongodb/mongodb": "1.10.0", + "php": ">=8.0" + }, + "require-dev": { + "fakerphp/faker": "^1.14", + "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.8.*", + "phpunit/phpunit": "^9.4", + "swoole/ide-helper": "4.8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Mongo\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + }, + { + "name": "Wess", + "email": "wess@appwrite.io" + } + ], + "description": "A simple library to manage Mongo database", + "keywords": [ + "database", + "mongo", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/mongo/issues", + "source": "https://github.com/utopia-php/mongo/tree/0.3.1" + }, + "time": "2023-09-01T17:25:28+00:00" + }, + { + "name": "utopia-php/swoole", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/swoole.git", + "reference": "c2a3a4f944a2f22945af3cbcb95b13f0769628b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/swoole/zipball/c2a3a4f944a2f22945af3cbcb95b13f0769628b1", + "reference": "c2a3a4f944a2f22945af3cbcb95b13f0769628b1", + "shasum": "" + }, + "require": { + "ext-swoole": "*", + "php": ">=8.0", + "utopia-php/framework": "0.*.*" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpunit/phpunit": "^9.3", + "swoole/ide-helper": "4.8.3", + "vimeo/psalm": "4.15.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Swoole\\": "src/Swoole" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "An extension for Utopia Framework to work with PHP Swoole as a PHP FPM alternative", + "keywords": [ + "framework", + "http", + "php", + "server", + "swoole", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/swoole/issues", + "source": "https://github.com/utopia-php/swoole/tree/0.5.0" + }, + "time": "2022-10-19T22:19:07+00:00" + } + ], + "packages-dev": [ + { + "name": "swoole/ide-helper", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/swoole/ide-helper.git", + "reference": "07692fa8f1bb8eac828410acd613ea5877237b09" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swoole/ide-helper/zipball/07692fa8f1bb8eac828410acd613ea5877237b09", + "reference": "07692fa8f1bb8eac828410acd613ea5877237b09", + "shasum": "" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Team Swoole", + "email": "team@swoole.com" + } + ], + "description": "IDE help files for Swoole.", + "support": { + "issues": "https://github.com/swoole/ide-helper/issues", + "source": "https://github.com/swoole/ide-helper/tree/5.1.0" + }, + "time": "2023-10-05T04:52:59+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/mock-server/docker-compose.yml b/mock-server/docker-compose.yml new file mode 100644 index 000000000..35227488b --- /dev/null +++ b/mock-server/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3' + +services: + mockapi: + container_name: mockapi + build: + context: . + args: + DEBUG: "false" + TESTING: "true" + VERSION: "dev" + entrypoint: + - php + - -e + - app/http.php + volumes: + - ./app:/usr/src/code/app + - ./src:/usr/src/code/src + - mockapi-cache:/storage/cache + networks: + - mockapi + +networks: + mockapi: + name: mockapi + +volumes: + mockapi-cache: \ No newline at end of file diff --git a/mock-server/src/Utopia/Exception.php b/mock-server/src/Utopia/Exception.php new file mode 100644 index 000000000..9a6d3b45d --- /dev/null +++ b/mock-server/src/Utopia/Exception.php @@ -0,0 +1,190 @@ + [ + 'name' => Exception::GENERAL_UNKNOWN, + 'description' => 'An unknown error has occured. Please check the logs for more information.', + 'code' => 500, + ], + Exception::GENERAL_MOCK => [ + 'name' => Exception::GENERAL_MOCK, + 'description' => 'General errors thrown by the mock controller used for testing.', + 'code' => 400, + ], + Exception::GENERAL_ACCESS_FORBIDDEN => [ + 'name' => Exception::GENERAL_ACCESS_FORBIDDEN, + 'description' => 'Access to this API is forbidden.', + 'code' => 401, + ], + Exception::GENERAL_UNKNOWN_ORIGIN => [ + 'name' => Exception::GENERAL_UNKNOWN_ORIGIN, + 'description' => 'The request originated from an unknown origin. If you trust this domain, please list it as a trusted platform in the Appwrite console.', + 'code' => 403, + ], + Exception::GENERAL_SERVICE_DISABLED => [ + 'name' => Exception::GENERAL_SERVICE_DISABLED, + 'description' => 'The requested service is disabled. You can enable the service from the Appwrite console.', + 'code' => 503, + ], + Exception::GENERAL_UNAUTHORIZED_SCOPE => [ + 'name' => Exception::GENERAL_UNAUTHORIZED_SCOPE, + 'description' => 'The current user or API key does not have the required scopes to access the requested resource.', + 'code' => 401, + ], + Exception::GENERAL_RATE_LIMIT_EXCEEDED => [ + 'name' => Exception::GENERAL_RATE_LIMIT_EXCEEDED, + 'description' => 'Rate limit for the current endpoint has been exceeded. Please try again after some time.', + 'code' => 429, + ], + Exception::GENERAL_SMTP_DISABLED => [ + 'name' => Exception::GENERAL_SMTP_DISABLED, + 'description' => 'SMTP is disabled on your Appwrite instance. You can learn more about setting up SMTP in our docs.', + 'code' => 503, + ], + Exception::GENERAL_PHONE_DISABLED => [ + 'name' => Exception::GENERAL_PHONE_DISABLED, + 'description' => 'Phone provider is not configured. Please check the _APP_SMS_PROVIDER environment variable of your Appwrite server.', + 'code' => 503, + ], + Exception::GENERAL_ARGUMENT_INVALID => [ + 'name' => Exception::GENERAL_ARGUMENT_INVALID, + 'description' => 'The request contains one or more invalid arguments. Please refer to the endpoint documentation.', + 'code' => 400, + ], + Exception::GENERAL_QUERY_LIMIT_EXCEEDED => [ + 'name' => Exception::GENERAL_QUERY_LIMIT_EXCEEDED, + 'description' => 'Query limit exceeded for the current attribute. Usage of more than 100 query values on a single attribute is prohibited.', + 'code' => 400, + ], + Exception::GENERAL_QUERY_INVALID => [ + 'name' => Exception::GENERAL_QUERY_INVALID, + 'description' => 'The query\'s syntax is invalid. Please check the query and try again.', + 'code' => 400, + ], + Exception::GENERAL_ROUTE_NOT_FOUND => [ + 'name' => Exception::GENERAL_ROUTE_NOT_FOUND, + 'description' => 'The requested route was not found. Please refer to the API docs and try again.', + 'code' => 404, + ], + Exception::GENERAL_CURSOR_NOT_FOUND => [ + 'name' => Exception::GENERAL_CURSOR_NOT_FOUND, + 'description' => 'The cursor is invalid. This can happen if the item represented by the cursor has been deleted.', + 'code' => 400, + ], + Exception::GENERAL_SERVER_ERROR => [ + 'name' => Exception::GENERAL_SERVER_ERROR, + 'description' => 'An internal server error occurred.', + 'code' => 500, + ], + Exception::GENERAL_PROTOCOL_UNSUPPORTED => [ + 'name' => Exception::GENERAL_PROTOCOL_UNSUPPORTED, + 'description' => 'The request cannot be fulfilled with the current protocol. Please check the value of the _APP_OPTIONS_FORCE_HTTPS environment variable.', + 'code' => 500, + ], + Exception::GENERAL_CODES_DISABLED => [ + 'name' => Exception::GENERAL_CODES_DISABLED, + 'description' => 'Invitation codes are disabled on this server. Please contact the server administrator.', + 'code' => 500, + ], + Exception::GENERAL_USAGE_DISABLED => [ + 'name' => Exception::GENERAL_USAGE_DISABLED, + 'description' => 'Usage stats is not configured. Please check the value of the _APP_USAGE_STATS environment variable of your Appwrite server.', + 'code' => 501, + ], + Exception::GENERAL_NOT_IMPLEMENTED => [ + 'name' => Exception::GENERAL_NOT_IMPLEMENTED, + 'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.', + 'code' => 405, + ], + + /** Functions */ + Exception::FUNCTION_NOT_FOUND => [ + 'name' => Exception::FUNCTION_NOT_FOUND, + 'description' => 'Function with the requested ID could not be found.', + 'code' => 404, + ], +]; + +class Exception extends \Exception +{ + /** + * Error Codes + * + * Naming the error types based on the following convention + * _ + * + * Appwrite has the following entities: + * - General + */ + + /** General */ + public const GENERAL_UNKNOWN = 'general_unknown'; + public const GENERAL_MOCK = 'general_mock'; + public const GENERAL_ACCESS_FORBIDDEN = 'general_access_forbidden'; + public const GENERAL_UNKNOWN_ORIGIN = 'general_unknown_origin'; + public const GENERAL_SERVICE_DISABLED = 'general_service_disabled'; + public const GENERAL_UNAUTHORIZED_SCOPE = 'general_unauthorized_scope'; + public const GENERAL_RATE_LIMIT_EXCEEDED = 'general_rate_limit_exceeded'; + public const GENERAL_SMTP_DISABLED = 'general_smtp_disabled'; + public const GENERAL_PHONE_DISABLED = 'general_phone_disabled'; + public const GENERAL_ARGUMENT_INVALID = 'general_argument_invalid'; + public const GENERAL_QUERY_LIMIT_EXCEEDED = 'general_query_limit_exceeded'; + public const GENERAL_QUERY_INVALID = 'general_query_invalid'; + public const GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found'; + public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found'; + public const GENERAL_SERVER_ERROR = 'general_server_error'; + public const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported'; + public const GENERAL_CODES_DISABLED = 'general_codes_disabled'; + public const GENERAL_USAGE_DISABLED = 'general_usage_disabled'; + public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented'; + + /** Functions */ + public const FUNCTION_NOT_FOUND = 'function_not_found'; + + protected $type = ''; + protected $errors = []; + + public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int $code = null, \Throwable $previous = null) + { + $this->errors = errorConfig; + $this->type = $type; + + if (isset($this->errors[$type])) { + $this->code = $this->errors[$type]['code']; + $this->message = $this->errors[$type]['description']; + } + + $this->message = $message ?? $this->message; + $this->code = $code ?? $this->code; + + parent::__construct($this->message, $this->code, $previous); + } + + /** + * Get the type of the exception. + * + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * Set the type of the exception. + * + * @param string $type + * + * @return void + */ + public function setType(string $type): void + { + $this->type = $type; + } +} diff --git a/mock-server/src/Utopia/File.php b/mock-server/src/Utopia/File.php new file mode 100644 index 000000000..8eafd1925 --- /dev/null +++ b/mock-server/src/Utopia/File.php @@ -0,0 +1,50 @@ +rules; + } + + /** + * Add a New Rule + * If rule is an array of documents with varying models + * + * @param string $key + * @param array $options + * @return Model + */ + protected function addRule(string $key, array $options): self + { + $this->rules[$key] = array_merge([ + 'required' => true, + 'array' => false, + 'description' => '', + 'example' => '' + ], $options); + + return $this; + } + + /** + * Delete an existing Rule + * If rule exists, it will be removed + * + * @param string $key + * @return Model + */ + protected function removeRule(string $key): self + { + if (isset($this->rules[$key])) { + unset($this->rules[$key]); + } + + return $this; + } + + /** + * @return array + */ + public function getRequired(): array + { + $list = []; + + foreach ($this->rules as $key => $rule) { + if ($rule['required'] ?? false) { + $list[] = $key; + } + } + + return $list; + } + + /** + * Is None + * + * Use to check if response is empty + * + * @return bool + */ + public function isNone(): bool + { + return $this->none; + } + + /** + * Is Any + * + * Use to check if response is a wildcard + * + * @return bool + */ + public function isAny(): bool + { + return $this->any; + } + + /** + * Is Public + * + * Should this model be publicly available in docs and spec files? + * + * @return bool + */ + public function isPublic(): bool + { + return $this->public; + } +} diff --git a/mock-server/src/Utopia/Response.php b/mock-server/src/Utopia/Response.php new file mode 100644 index 000000000..756049364 --- /dev/null +++ b/mock-server/src/Utopia/Response.php @@ -0,0 +1,279 @@ +models[$instance->getType()] = $instance; + + return $this; + } + + /** + * Get Model Object + * + * @param string $key + * @return Model + * @throws Exception + */ + public function getModel(string $key): Model + { + if (!isset($this->models[$key])) { + throw new \Exception('Undefined model: ' . $key); + } + + return $this->models[$key]; + } + + /** + * Get Models List + * + * @return Model[] + */ + public function getModels(): array + { + return $this->models; + } + + /** + * Validate response objects and outputs + * the response according to given format type + * + * @param Document $document + * @param string $model + * + * return void + * @throws Exception + */ + public function dynamic(Document $document, string $model): void + { + $output = $this->output(clone $document, $model); + + // If filter is set, parse the output + if (self::hasFilter()) { + $output = self::getFilter()->parse($output, $model); + } + + switch ($this->getContentType()) { + case self::CONTENT_TYPE_JSON: + $this->json(!empty($output) ? $output : new \stdClass()); + break; + + case self::CONTENT_TYPE_NULL: + break; + + default: + if ($model === self::MODEL_NONE) { + $this->noContent(); + } else { + $this->json(!empty($output) ? $output : new \stdClass()); + } + break; + } + } + + /** + * Generate valid response object from document data + * + * @param Document $document + * @param string $model + * + * return array + * @return array + * @throws Exception + */ + public function output(Document $document, string $model): array + { + $data = clone $document; + $model = $this->getModel($model); + $output = []; + + $data = $model->filter($data); + + if ($model->isAny()) { + $this->payload = $data->getArrayCopy(); + + return $this->payload; + } + + foreach ($model->getRules() as $key => $rule) { + if (!$data->isSet($key) && $rule['required']) { // do not set attribute in response if not required + if (\array_key_exists('default', $rule)) { + $data->setAttribute($key, $rule['default']); + } else { + throw new \Exception('Model ' . $model->getName() . ' is missing response key: ' . $key); + } + } + + if ($rule['array']) { + if (!is_array($data[$key])) { + throw new \Exception($key . ' must be an array of type ' . $rule['type']); + } + + foreach ($data[$key] as $index => $item) { + if ($item instanceof Document) { + if (\is_array($rule['type'])) { + foreach ($rule['type'] as $type) { + $condition = false; + foreach ($this->getModel($type)->conditions as $attribute => $val) { + $condition = $item->getAttribute($attribute) === $val; + if (!$condition) { + break; + } + } + if ($condition) { + $ruleType = $type; + break; + } + } + } else { + $ruleType = $rule['type']; + } + + if (!array_key_exists($ruleType, $this->models)) { + throw new \Exception('Missing model for rule: ' . $ruleType); + } + + $data[$key][$index] = $this->output($item, $ruleType); + } + } + } else { + if ($data[$key] instanceof Document) { + $data[$key] = $this->output($data[$key], $rule['type']); + } + } + + $output[$key] = $data[$key]; + } + + $this->payload = $output; + + return $this->payload; + } + + /** + * Output response + * + * Generate HTTP response output including the response header (+cookies) and body and prints them. + * + * @param string $body + * + * @return void + */ + public function file(string $body = ''): void + { + $this->payload = [ + 'payload' => $body + ]; + + $this->send($body); + } + + /** + * @return array + */ + public function getPayload(): array + { + return $this->payload; + } + + /** + * Function to set a response filter + * + * @param $filter the response filter to set + * + * @return void + */ + public static function setFilter(?Filter $filter) + { + self::$filter = $filter; + } + + /** + * Return the currently set filter + * + * @return Filter + */ + public static function getFilter(): ?Filter + { + return self::$filter; + } + + /** + * Check if a filter has been set + * + * @return bool + */ + public static function hasFilter(): bool + { + return self::$filter != null; + } + + /** + * Set Header + * + * @param string $key + * @param string $value + * @return void + */ + public function setHeader(string $key, string $value): void + { + $this->sendHeader($key, $value); + } +} diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 0b894bf66..fed1e0388 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -142,6 +142,11 @@ public function getFiles(): array 'destination' => 'lib/config.js', 'template' => 'cli/lib/config.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/paginate.js', + 'template' => 'cli/lib/paginate.js.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/client.js', diff --git a/src/SDK/Language/Deno.php b/src/SDK/Language/Deno.php index 1fdef0c0d..3027ed503 100644 --- a/src/SDK/Language/Deno.php +++ b/src/SDK/Language/Deno.php @@ -33,21 +33,41 @@ public function getFiles(): array 'destination' => 'src/permission.ts', 'template' => 'deno/src/permission.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'test/permission.test.ts', + 'template' => 'deno/test/permission.test.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'src/role.ts', 'template' => 'deno/src/role.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'test/role.test.ts', + 'template' => 'deno/test/role.test.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'src/id.ts', 'template' => 'deno/src/id.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'test/id.test.ts', + 'template' => 'deno/test/id.test.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'src/query.ts', 'template' => 'deno/src/query.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'test/query.test.ts', + 'template' => 'deno/test/query.test.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'src/inputFile.ts', @@ -73,6 +93,11 @@ public function getFiles(): array 'destination' => '/src/services/{{service.name | caseDash}}.ts', 'template' => 'deno/src/services/service.ts.twig', ], + [ + 'scope' => 'service', + 'destination' => '/test/services/{{service.name | caseDash}}.test.ts', + 'template' => 'deno/test/services/service.test.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'README.md', diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index 709f56ea5..f569225f2 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -129,7 +129,8 @@ public function getKeywords(): array 'when', 'where', 'while', - 'yield' + 'yield', + 'path' ]; } diff --git a/src/SDK/Language/JS.php b/src/SDK/Language/JS.php index 3ed496b24..c41727e36 100644 --- a/src/SDK/Language/JS.php +++ b/src/SDK/Language/JS.php @@ -105,6 +105,7 @@ public function getKeywords(): array 'while', 'with', 'yield', + 'path' ]; } diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index f6a2af02b..a7a4c5aea 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -86,7 +86,8 @@ public function getKeywords(): array "vararg", "when", "where", - "while" + "while", + "path" ]; } diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 3f6968814..035621534 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -117,7 +117,8 @@ public function getKeywords(): array 'use', 'var', 'while', - 'xor' + 'xor', + 'path' ]; } @@ -176,21 +177,41 @@ public function getFiles(): array 'destination' => 'src/{{ spec.title | caseUcfirst}}/Permission.php', 'template' => 'php/src/Permission.php.twig', ], + [ + 'scope' => 'default', + 'destination' => 'tests/{{ spec.title | caseUcfirst}}/PermissionTest.php', + 'template' => 'php/tests/PermissionTest.php.twig', + ], [ 'scope' => 'default', 'destination' => 'src/{{ spec.title | caseUcfirst}}/Role.php', 'template' => 'php/src/Role.php.twig', ], + [ + 'scope' => 'default', + 'destination' => 'tests/{{ spec.title | caseUcfirst}}/RoleTest.php', + 'template' => 'php/tests/RoleTest.php.twig', + ], [ 'scope' => 'default', 'destination' => 'src/{{ spec.title | caseUcfirst}}/ID.php', 'template' => 'php/src/ID.php.twig', ], + [ + 'scope' => 'default', + 'destination' => 'tests/{{ spec.title | caseUcfirst}}/IDTest.php', + 'template' => 'php/tests/IDTest.php.twig', + ], [ 'scope' => 'default', 'destination' => 'src/{{ spec.title | caseUcfirst}}/Query.php', 'template' => 'php/src/Query.php.twig', ], + [ + 'scope' => 'default', + 'destination' => 'tests/{{ spec.title | caseUcfirst}}/QueryTest.php', + 'template' => 'php/tests/QueryTest.php.twig', + ], [ 'scope' => 'default', 'destination' => 'src/{{ spec.title | caseUcfirst}}/InputFile.php', @@ -211,6 +232,11 @@ public function getFiles(): array 'destination' => '/src/{{ spec.title | caseUcfirst}}/Services/{{service.name | caseUcfirst}}.php', 'template' => 'php/src/Services/Service.php.twig', ], + [ + 'scope' => 'service', + 'destination' => '/tests/{{ spec.title | caseUcfirst}}/Services/{{service.name | caseUcfirst}}Test.php', + 'template' => 'php/tests/Services/ServiceTest.php.twig', + ], ]; } diff --git a/src/SDK/Language/Ruby.php b/src/SDK/Language/Ruby.php index d05021146..eb50551b0 100644 --- a/src/SDK/Language/Ruby.php +++ b/src/SDK/Language/Ruby.php @@ -74,6 +74,7 @@ public function getKeywords(): array 'until', 'when', 'while', + 'path' ]; } diff --git a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig index 930b74edd..210ef4fbe 100644 --- a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig @@ -374,7 +374,7 @@ class Client @JvmOverloads constructor( responseType = Map::class.java, ) val chunksUploaded = current["chunksUploaded"] as Long - offset = (chunksUploaded * CHUNK_SIZE).coerceAtMost(size) + offset = chunksUploaded * CHUNK_SIZE } while (offset < size) { @@ -385,7 +385,7 @@ class Client @JvmOverloads constructor( } "bytes" -> { val end = if (offset + CHUNK_SIZE < size) { - offset + CHUNK_SIZE + offset + CHUNK_SIZE - 1 } else { size - 1 } @@ -405,7 +405,7 @@ class Client @JvmOverloads constructor( ) headers["Content-Range"] = - "bytes $offset-${((offset + CHUNK_SIZE) - 1).coerceAtMost(size)}/$size" + "bytes $offset-${((offset + CHUNK_SIZE) - 1).coerceAtMost(size - 1)}/$size" result = call( method = "POST", diff --git a/templates/android/library/src/main/java/io/appwrite/Query.kt.twig b/templates/android/library/src/main/java/io/appwrite/Query.kt.twig index e9c5a9c88..7f4fbcd81 100644 --- a/templates/android/library/src/main/java/io/appwrite/Query.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Query.kt.twig @@ -20,11 +20,11 @@ class Query { fun isNotNull(attribute: String) = "isNotNull(\"${attribute}\")" - fun between(attribute: String, start: Int, end: Int) = Query.addQuery(attribute, "between", listOf(start, end)) + fun between(attribute: String, start: Int, end: Int) = "between(\"${attribute}\", ${start}, ${end})" - fun between(attribute: String, start: Double, end: Double) = Query.addQuery(attribute, "between", listOf(start, end)) + fun between(attribute: String, start: Double, end: Double) = "between(\"${attribute}\", ${start}, ${end})" - fun between(attribute: String, start: String, end: String) = Query.addQuery(attribute, "between", listOf(start, end)) + fun between(attribute: String, start: String, end: String) = "between(\"${attribute}\", \"${start}\", \"${end}\")" fun startsWith(attribute: String, value: String) = Query.addQuery(attribute, "startsWith", value) diff --git a/templates/android/library/src/main/java/io/appwrite/Role.kt.twig b/templates/android/library/src/main/java/io/appwrite/Role.kt.twig index d5f59c3a6..2e4de9861 100644 --- a/templates/android/library/src/main/java/io/appwrite/Role.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Role.kt.twig @@ -1,29 +1,72 @@ package {{ sdk.namespace | caseDot }} +/** + * Helper class to generate role strings for [Permission]. + */ class Role { companion object { + + /** + * Grants access to anyone. + * + * This includes authenticated and unauthenticated users. + */ fun any(): String = "any" + /** + * Grants access to a specific user by user ID. + * + * You can optionally pass verified or unverified for + * [status] to target specific types of users. + */ fun user(id: String, status: String = ""): String = if(status.isEmpty()) { "user:$id" } else { "user:$id/$status" } + /** + * Grants access to any authenticated or anonymous user. + * + * You can optionally pass verified or unverified for + * [status] to target specific types of users. + */ fun users(status: String = ""): String = if(status.isEmpty()) { "users" } else { "users/$status" } + /** + * Grants access to any guest user without a session. + * + * Authenticated users don't have access to this role. + */ fun guests(): String = "guests" + /** + * Grants access to a team by team ID. + * + * You can optionally pass a role for [role] to target + * team members with the specified role. + */ fun team(id: String, role: String = ""): String = if(role.isEmpty()) { "team:$id" } else { "team:$id/$role" } + /** + * Grants access to a specific member of a team. + * + * When the member is removed from the team, they will + * no longer have access. + */ fun member(id: String): String = "member:$id" + + /** + * Grants access to a user with the specified label. + */ + fun label(name: String): String = "label:$name" } } \ No newline at end of file diff --git a/templates/android/library/src/main/java/io/appwrite/services/ServiceTemplate.kt.twig b/templates/android/library/src/main/java/io/appwrite/services/ServiceTemplate.kt.twig index f2a331c4e..a461b5f91 100644 --- a/templates/android/library/src/main/java/io/appwrite/services/ServiceTemplate.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/services/ServiceTemplate.kt.twig @@ -53,12 +53,12 @@ class {{ service.name | caseUcfirst }} : Service { onProgress: ((UploadProgress) -> Unit)? = null {%~ endif %} ){% if method.type != "webAuth" %}: {{ method | returnType(spec, sdk.namespace | caseDot) | raw }}{% endif %} { - val path = "{{ method.path }}" + val apiPath = "{{ method.path }}" {%~ for parameter in method.parameters.path %} .replace("{{ '{' ~ parameter.name | caseCamel ~ '}' }}", {{ parameter.name | caseCamel }}) {%~ endfor %} - val params = mutableMapOf( + val apiParams = mutableMapOf( {%~ for parameter in method.parameters.query | merge(method.parameters.body) %} "{{ parameter.name }}" to {{ parameter.name | caseCamel }}, {%~ endfor %} @@ -73,25 +73,25 @@ class {{ service.name | caseUcfirst }} : Service { {%~ endif %} ) {%~ if method.type == 'webAuth' %} - val query = mutableListOf() - params.forEach { + val apiQuery = mutableListOf() + apiParams.forEach { when (it.value) { null -> { return@forEach } is List<*> -> { - query.add("${it.key}[]=${it.value.toString()}") + apiQuery.add("${it.key}[]=${it.value.toString()}") } else -> { - query.add("${it.key}=${it.value.toString()}") + apiQuery.add("${it.key}=${it.value.toString()}") } } } - val url = Uri.parse("${client.endPoint}${path}?${query.joinToString("&")}") + val apiUrl = Uri.parse("${client.endPoint}${apiPath}?${apiQuery.joinToString("&")}") val callbackUrlScheme = "{{ spec.title | caseLower }}-callback-${client.config["project"]}" - WebAuthComponent.authenticate(activity, url, callbackUrlScheme) { + WebAuthComponent.authenticate(activity, apiUrl, callbackUrlScheme) { if (it.isFailure) { throw it.exceptionOrNull()!! } @@ -118,12 +118,12 @@ class {{ service.name | caseUcfirst }} : Service { {%~ elseif method.type == 'location' %} return client.call( "{{ method.method | caseUpper }}", - path, - params = params, + apiPath, + params = apiParams, responseType = {{ method | returnType(spec, sdk.namespace | caseDot) | raw }}::class.java ) {%~ else %} - val headers = mutableMapOf( + val apiHeaders = mutableMapOf( {%~ for key, header in method.headers %} "{{ key }}" to "{{ header }}", {%~ endfor %} @@ -146,9 +146,9 @@ class {{ service.name | caseUcfirst }} : Service { {%~ endif %} {%~ endfor %} return client.chunkedUpload( - path, - headers, - params, + apiPath, + apiHeaders, + apiParams, responseType = {{ method | returnType(spec, sdk.namespace | caseDot) | raw }}::class.java, {%~ if method.responseModel %} converter, @@ -160,9 +160,9 @@ class {{ service.name | caseUcfirst }} : Service { {%~ else %} return client.call( "{{ method.method | caseUpper }}", - path, - headers, - params, + apiPath, + apiHeaders, + apiParams, {%~ if method.responseModel | hasGenericType(spec) %} responseType = classOf(), {%~ else %} diff --git a/templates/cli/lib/commands/command.js.twig b/templates/cli/lib/commands/command.js.twig index 3c9addc23..52bc99044 100644 --- a/templates/cli/lib/commands/command.js.twig +++ b/templates/cli/lib/commands/command.js.twig @@ -21,7 +21,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ { {% endfor %} let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} : sdk; - let path = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; + let apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; let payload = {}; {% if method.parameters.query|length > 0 %} @@ -95,9 +95,9 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ { payload['project'] = localConfig.getProject().projectId payload['key'] = globalConfig.getKey(); const queryParams = new URLSearchParams(payload); - path = `${globalConfig.getEndpoint()}${path}?${queryParams.toString()}`; + apiPath = `${globalConfig.getEndpoint()}${apiPath}?${queryParams.toString()}`; - const response = await client.call('{{ method.method | caseLower }}', path, { + const response = await client.call('{{ method.method | caseLower }}', apiPath, { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} @@ -122,7 +122,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ { if (size <= libClient.CHUNK_SIZE) { payload['{{ parameter.name }}'] = fs.createReadStream(payload['{{ parameter.name }}']); - response = await client.call('{{ method.method | caseLower }}', path, { + response = await client.call('{{ method.method | caseLower }}', apiPath, { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} @@ -136,12 +136,8 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ { } else { const streamFilePath = payload['{{ parameter.name }}']; - let id = undefined; - let counter = 0; - const totalCounters = Math.ceil(size / libClient.CHUNK_SIZE); - - const headers = { + const apiHeaders = { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} @@ -150,55 +146,50 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ { {% endfor %} }; + let offset = 0; {% for parameter in method.parameters.all %} {% if parameter.isUploadID %} if({{ parameter.name | caseCamel | escapeKeyword }} != 'unique()') { try { - response = await client.call('get', path + '/' + {{ parameter.name }}, headers); - counter = response.chunksUploaded; + response = await client.call('get', apiPath + '/' + {{ parameter.name }}, apiHeaders); + offset = response.chunksUploaded * libClient.CHUNK_SIZE; } catch(e) { } } {% endif %} {% endfor %} - for (counter; counter < totalCounters; counter++) { - const start = (counter * libClient.CHUNK_SIZE); - const end = Math.min((((counter * libClient.CHUNK_SIZE) + libClient.CHUNK_SIZE) - 1), size); - - headers['content-range'] = 'bytes ' + start + '-' + end + '/' + size; + while (offset < size) { + let end = Math.min(offset + libClient.CHUNK_SIZE - 1, size - 1); - if (id) { - headers['x-appwrite-id'] = id; + apiHeaders['content-range'] = 'bytes ' + offset + '-' + end + '/' + size; + if (response && response.$id) { + apiHeaders['x-{{spec.title | caseLower }}-id'] = response.$id; } const stream = fs.createReadStream(streamFilePath, { - start, + start: offset, end }); payload['{{ parameter.name }}'] = stream; + response = await client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); - response = await client.call('{{ method.method | caseLower }}', path, headers, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); - - if (!id) { - id = response['$id']; - } - - if (onProgress !== null) { + if (onProgress) { onProgress({ - $id: response['$id'], - progress: Math.min((counter+1) * libClient.CHUNK_SIZE, size) / size * 100, - sizeUploaded: end+1, - chunksTotal: response['chunksTotal'], - chunksUploaded: response['chunksUploaded'] + $id: response.$id, + progress: ( offset / size ) * 100, + sizeUploaded: offset, + chunksTotal: response.chunksTotal, + chunksUploaded: response.chunksUploaded }); } + offset += libClient.CHUNK_SIZE; } } {% endif %} {% endfor %} {% else %} - response = await client.call('{{ method.method | caseLower }}', path, { + response = await client.call('{{ method.method | caseLower }}', apiPath, { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index f7c5ec043..420bd3f85 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -2,6 +2,7 @@ const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); +const { paginate } = require('../paginate'); const { questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsConfirmDeployCollections } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); @@ -37,113 +38,163 @@ const { teamsCreate } = require("./teams"); -const POOL_DEBOUNCE = 2000; // in milliseconds -const POOL_MAX_DEBOUNCES = 30; +const STEP_SIZE = 100; // Resources +const POOL_DEBOUNCE = 2000; // Milliseconds + +let poolMaxDebounces = 30; const awaitPools = { wipeAttributes: async (databaseId, collectionId, iteration = 1) => { - if (iteration > POOL_MAX_DEBOUNCES) { + if (iteration > poolMaxDebounces) { return false; } - // TODO: Pagination? - const { attributes: remoteAttributes } = await databasesListAttributes({ + const { total } = await databasesListAttributes({ databaseId, collectionId, - queries: ['limit(100)'], + queries: ['limit(1)'], parseOutput: false }); - if (remoteAttributes.length <= 0) { + if (total === 0) { return true; } + let steps = Math.max(1, Math.ceil(total / STEP_SIZE)); + if (steps > 1 && iteration === 1) { + poolMaxDebounces *= steps; + + log('Found a large number of attributes, increasing timeout to ' + (poolMaxDebounces * POOL_DEBOUNCE / 1000 / 60) + ' minutes') + } + await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); - return await awaitPools.wipeAttributes(databaseId, collectionId, iteration + 1); + + return await awaitPools.wipeAttributes( + databaseId, + collectionId, + iteration + 1 + ); }, wipeIndexes: async (databaseId, collectionId, iteration = 1) => { - if (iteration > POOL_MAX_DEBOUNCES) { + if (iteration > poolMaxDebounces) { return false; } - // TODO: Pagination? - const { indexes: remoteIndexes } = await databasesListIndexes({ + const { total } = await databasesListIndexes({ databaseId, collectionId, queries: ['limit(100)'], parseOutput: false }); - if (remoteIndexes.length <= 0) { + if (total === 0) { return true; } + let steps = Math.max(1, Math.ceil(total / STEP_SIZE)); + if (steps > 1 && iteration === 1) { + poolMaxDebounces *= steps; + + log('Found a large number of indexes, increasing timeout to ' + (poolMaxDebounces * POOL_DEBOUNCE / 1000 / 60) + ' minutes') + } + await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); - return await awaitPools.wipeIndexes(databaseId, collectionId, iteration + 1); + + return await awaitPools.wipeIndexes( + databaseId, + collectionId, + iteration + 1 + ); }, expectAttributes: async (databaseId, collectionId, attributeKeys, iteration = 1) => { - if (iteration > POOL_MAX_DEBOUNCES) { + if (iteration > poolMaxDebounces) { return false; } - // TODO: Pagination? - const { attributes: remoteAttributes } = await databasesListAttributes({ + let steps = Math.max(1, Math.ceil(attributeKeys.length / STEP_SIZE)); + if (steps > 1 && iteration === 1) { + poolMaxDebounces *= steps; + + log('Creating a large number of attributes, increasing timeout to ' + (poolMaxDebounces * POOL_DEBOUNCE / 1000 / 60) + ' minutes') + } + + const { attributes } = await paginate(databasesListAttributes, { databaseId, collectionId, - queries: ['limit(100)'], parseOutput: false - }); + }, 100, 'attributes'); - const readyAttributeKeys = remoteAttributes.filter((attribute) => { - if (attributeKeys.includes(attribute.key)) { - if (['stuck', 'failed'].includes(attribute.status)) { - throw new Error(`Attribute '${attribute.key}' failed!`); - } + const ready = attributes + .filter(attribute => { + if (attributeKeys.includes(attribute.key)) { + if (['stuck', 'failed'].includes(attribute.status)) { + throw new Error(`Attribute '${attribute.key}' failed!`); + } - return attribute.status === 'available'; - } + return attribute.status === 'available'; + } - return false; - }).map(attribute => attribute.key); + return false; + }) + .map(attribute => attribute.key); - if (readyAttributeKeys.length >= attributeKeys.length) { + if (ready.length === attributeKeys.length) { return true; } await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); - return await awaitPools.expectAttributes(databaseId, collectionId, attributeKeys, iteration + 1); + + return await awaitPools.expectAttributes( + databaseId, + collectionId, + attributeKeys, + iteration + 1 + ); }, expectIndexes: async (databaseId, collectionId, indexKeys, iteration = 1) => { - if (iteration > POOL_MAX_DEBOUNCES) { + if (iteration > poolMaxDebounces) { return false; } - // TODO: Pagination? - const { indexes: remoteIndexes } = await databasesListIndexes({ + let steps = Math.max(1, Math.ceil(indexKeys.length / STEP_SIZE)); + if (steps > 1 && iteration === 1) { + poolMaxDebounces *= steps; + + log('Creating a large number of indexes, increasing timeout to ' + (poolMaxDebounces * POOL_DEBOUNCE / 1000 / 60) + ' minutes') + } + + const { indexes } = await paginate(databasesListIndexes, { databaseId, collectionId, - queries: ['limit(100)'], parseOutput: false - }); + }, 100, 'indexes'); - const readyIndexKeys = remoteIndexes.filter((index) => { - if (indexKeys.includes(index.key)) { - if (['stuck', 'failed'].includes(index.status)) { - throw new Error(`Index '${index.key}' failed!`); - } + const ready = indexes + .filter((index) => { + if (indexKeys.includes(index.key)) { + if (['stuck', 'failed'].includes(index.status)) { + throw new Error(`Index '${index.key}' failed!`); + } - return index.status === 'available'; - } + return index.status === 'available'; + } - return false; - }).map(index => index.key); + return false; + }) + .map(index => index.key); - if (readyIndexKeys.length >= indexKeys.length) { + if (ready.length >= indexKeys.length) { return true; } await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); - return await awaitPools.expectIndexes(databaseId, collectionId, indexKeys, iteration + 1); + + return await awaitPools.expectIndexes( + databaseId, + collectionId, + indexKeys, + iteration + 1 + ); }, } @@ -206,10 +257,14 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { functionId: func['$id'], name: func.name, execute: func.execute, - vars: JSON.stringify(response.vars), events: func.events, schedule: func.schedule, timeout: func.timeout, + enabled: func.enabled, + logging: func.logging, + entrypoint: func.entrypoint, + commands: func.commands, + vars: JSON.stringify(response.vars), parseOutput: false }); } catch (e) { @@ -220,10 +275,14 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { name: func.name, runtime: func.runtime, execute: func.execute, - vars: JSON.stringify(func.vars), events: func.events, schedule: func.schedule, timeout: func.timeout, + enabled: func.enabled, + logging: func.logging, + entrypoint: func.entrypoint, + commands: func.commands, + vars: JSON.stringify(func.vars), parseOutput: false }); @@ -241,19 +300,19 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { if (func.variables) { // Delete existing variables - // TODO: Pagination? - const { variables: remoteVariables } = await functionsListVariables({ + const { total } = await functionsListVariables({ functionId: func['$id'], - queries: ['limit(100)'], + queries: ['limit(1)'], parseOutput: false }); let deployVariables = yes; - if (remoteVariables.length == 0) { + + if (total === 0) { deployVariables = true; } else if (remoteVariables.length > 0 && !yes) { const variableAnswers = await inquirer.prompt(questionsDeployFunctions[1]) - deployVariables = variableAnswers.override === "YES"; + deployVariables = variableAnswers.override.toLowerCase() === "yes"; } if (!deployVariables) { @@ -283,7 +342,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { // Create tag if (!func.entrypoint) { - answers = await inquirer.prompt(questionsGetEntrypoint) + const answers = await inquirer.prompt(questionsGetEntrypoint) func.entrypoint = answers.entrypoint; localConfig.updateFunction(func['$id'], func); } @@ -292,6 +351,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { response = await functionsCreateDeployment({ functionId: func['$id'], entrypoint: func.entrypoint, + commands: func.commands, code: func.path, activate: true, parseOutput: false @@ -464,22 +524,27 @@ const deployCollection = async ({ all, yes } = {}) => { databaseId: collection.databaseId, parseOutput: false, }); + databaseId = database.$id; - await databasesUpdate({ - databaseId: collection.databaseId, - name: localDatabase.name ?? collection.databaseId, - parseOutput: false - }) + if (database.name !== (localDatabase.name ?? collection.databaseId)) { + await databasesUpdate({ + databaseId: collection.databaseId, + name: localDatabase.name ?? collection.databaseId, + parseOutput: false + }) - success(`Updated ${localDatabase.name} ( ${collection.databaseId} )`); + success(`Updated ${localDatabase.name} ( ${collection.databaseId} )`); + } } catch (err) { log(`Database ${collection.databaseId} not found. Creating it now...`); + const database = await databasesCreate({ databaseId: collection.databaseId, name: localDatabase.name ?? collection.databaseId, parseOutput: false, }); + databaseId = database.$id; } @@ -489,11 +554,12 @@ const deployCollection = async ({ all, yes } = {}) => { collectionId: collection['$id'], parseOutput: false, }) + log(`Collection ${collection.name} ( ${collection['$id']} ) already exists.`); if (!yes) { - answers = await inquirer.prompt(questionsDeployCollections[1]) - if (answers.override !== "YES") { + const answers = await inquirer.prompt(questionsDeployCollections[1]) + if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${collection.name} ( ${collection['$id']} )`); continue; } @@ -501,15 +567,13 @@ const deployCollection = async ({ all, yes } = {}) => { log(`Deleting indexes and attributes ... `); - // TODO: Pagination? - const { indexes: remoteIndexes } = await databasesListIndexes({ + const { indexes } = await paginate(databasesListIndexes, { databaseId, collectionId: collection['$id'], - queries: ['limit(100)'], parseOutput: false - }); + }, 100, 'indexes'); - await Promise.all(remoteIndexes.map(async index => { + await Promise.all(indexes.map(async index => { await databasesDeleteIndex({ databaseId, collectionId: collection['$id'], @@ -518,20 +582,18 @@ const deployCollection = async ({ all, yes } = {}) => { }); })); - const deleteIndexesPoolStatus = await awaitPools.wipeIndexes(databaseId, collection['$id']); - if (!deleteIndexesPoolStatus) { - throw new Error("Index deletion did not finish for too long."); + let result = await awaitPools.wipeIndexes(databaseId, collection['$id']); + if (!result) { + throw new Error("Index deletion timed out."); } - // TODO: Pagination? - const { attributes: remoteAttributes } = await databasesListAttributes({ + const { attributes } = await paginate(databasesListAttributes, { databaseId, collectionId: collection['$id'], - queries: ['limit(100)'], parseOutput: false - }); + }, 100, 'attributes'); - await Promise.all(remoteAttributes.map(async attribute => { + await Promise.all(attributes.map(async attribute => { await databasesDeleteAttribute({ databaseId, collectionId: collection['$id'], @@ -542,7 +604,7 @@ const deployCollection = async ({ all, yes } = {}) => { const deleteAttributesPoolStatus = await awaitPools.wipeAttributes(databaseId, collection['$id']); if (!deleteAttributesPoolStatus) { - throw new Error("Attribute deletion did not finish for too long."); + throw new Error("Attribute deletion timed out."); } await databasesUpdateCollection({ @@ -572,20 +634,26 @@ const deployCollection = async ({ all, yes } = {}) => { } // Create all non-relationship attributes first - const nonRelationshipAttributes = collection.attributes.filter(attribute => attribute.type !== 'relationship'); - await Promise.all(nonRelationshipAttributes.map(attribute => { + const attributes = collection.attributes.filter(attribute => attribute.type !== 'relationship'); + + await Promise.all(attributes.map(attribute => { return createAttribute(databaseId, collection['$id'], attribute); })); - const nonRelationshipAttributeKeys = nonRelationshipAttributes.map(attribute => attribute.key); - const createPoolStatus = await awaitPools.expectAttributes(databaseId, collection['$id'], nonRelationshipAttributeKeys); - if (!createPoolStatus) { - throw new Error("Attribute creation did not finish for too long."); + let result = await awaitPools.expectAttributes( + databaseId, + collection['$id'], + attributes.map(attribute => attribute.key) + ); + + if (!result) { + throw new Error("Attribute creation timed out."); } - success(`Created ${nonRelationshipAttributeKeys.length} non-relationship attributes`); + success(`Created ${attributes.length} non-relationship attributes`); log(`Creating indexes ...`) + await Promise.all(collection.indexes.map(async index => { await databasesCreateIndex({ databaseId, @@ -598,10 +666,14 @@ const deployCollection = async ({ all, yes } = {}) => { }); })); - const indexKeys = collection.indexes.map(attribute => attribute.key); - const indexPoolStatus = await awaitPools.expectIndexes(databaseId, collection['$id'], indexKeys); - if (!indexPoolStatus) { - throw new Error("Index creation did not finish for too long."); + result = await awaitPools.expectIndexes( + databaseId, + collection['$id'], + collection.indexes.map(attribute => attribute.key) + ); + + if (!result) { + throw new Error("Index creation timed out."); } success(`Created ${collection.indexes.length} indexes`); @@ -611,23 +683,31 @@ const deployCollection = async ({ all, yes } = {}) => { // Create the relationship attributes for (let collection of collections) { - const relationshipAttributes = collection.attributes.filter(attribute => attribute.type === 'relationship' && attribute.side === 'parent'); + const relationships = collection.attributes.filter(attribute => + attribute.type === 'relationship' && attribute.side === 'parent' + ); - if (relationshipAttributes.length === 0) continue; + if (relationships.length === 0) { + continue; + } log(`Deploying relationships for collection ${collection.name} ( ${collection['$id']} )`); - await Promise.all(relationshipAttributes.map(attribute => { + await Promise.all(relationships.map(attribute => { return createAttribute(collection['databaseId'], collection['$id'], attribute); })); - const nonRelationshipAttributeKeys = relationshipAttributes.map(attribute => attribute.key); - const createPoolStatus = await awaitPools.expectAttributes(collection['databaseId'], collection['$id'], nonRelationshipAttributeKeys); - if (!createPoolStatus) { - throw new Error("Attribute creation did not finish for too long."); + let result = await awaitPools.expectAttributes( + collection['databaseId'], + collection['$id'], + relationships.map(attribute => attribute.key) + ); + + if (!result) { + throw new Error("Attribute creation timed out."); } - success(`Created ${nonRelationshipAttributeKeys.length} relationship attributes`); + success(`Created ${relationships.length} relationship attributes`); } } @@ -645,7 +725,7 @@ const deployBucket = async ({ all, yes } = {}) => { } if (bucketIds.length === 0) { - let answers = await inquirer.prompt(questionsDeployBuckets[0]) + const answers = await inquirer.prompt(questionsDeployBuckets[0]) bucketIds.push(...answers.buckets); } @@ -667,8 +747,8 @@ const deployBucket = async ({ all, yes } = {}) => { log(`Bucket ${bucket.name} ( ${bucket['$id']} ) already exists.`); if (!yes) { - answers = await inquirer.prompt(questionsDeployBuckets[1]) - if (answers.override !== "YES") { + const answers = await inquirer.prompt(questionsDeployBuckets[1]) + if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${bucket.name} ( ${bucket['$id']} )`); continue; } @@ -732,7 +812,7 @@ const deployTeam = async ({ all, yes } = {}) => { } if (teamIds.length === 0) { - let answers = await inquirer.prompt(questionsDeployTeams[0]) + const answers = await inquirer.prompt(questionsDeployTeams[0]) teamIds.push(...answers.teams); } @@ -754,8 +834,8 @@ const deployTeam = async ({ all, yes } = {}) => { log(`Team ${team.name} ( ${team['$id']} ) already exists.`); if (!yes) { - answers = await inquirer.prompt(questionsDeployTeams[1]) - if (answers.override !== "YES") { + const answers = await inquirer.prompt(questionsDeployTeams[1]) + if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${team.name} ( ${team['$id']} )`); continue; } @@ -799,7 +879,7 @@ deploy deploy .command("collection") .description("Deploy collections in the current project.") - .option(`--all`, `Flag to deploy all functions`) + .option(`--all`, `Flag to deploy all collections`) .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(deployCollection)); diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index cdd1bea2c..b212d9160 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -82,6 +82,7 @@ const client = new Command("client") } let client = new Client().setEndpoint(endpoint); + client.setProject('console'); if (selfSigned || globalConfig.getSelfSigned()) { client.setSelfSigned(true); } diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 06e400d70..4e4f1a39f 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -10,6 +10,7 @@ const { databasesGet, databasesListCollections, databasesList } = require("./dat const { storageListBuckets } = require("./storage"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); +const { paginate } = require("../paginate"); const { questionsInitProject, questionsInitFunction, questionsInitCollection } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); @@ -24,11 +25,11 @@ const init = new Command("init") const initProject = async () => { let response = {} - let answers = await inquirer.prompt(questionsInitProject) + const answers = await inquirer.prompt(questionsInitProject) if (!answers.project) process.exit(1) let sdk = await sdkForConsole(); - if (answers.start == "new") { + if (answers.start === "new") { response = await teamsCreate({ teamId: 'unique()', name: answers.project, @@ -53,8 +54,8 @@ const initProject = async () => { const initFunction = async () => { // TODO: Add CI/CD support (ID, name, runtime) - let answers = await inquirer.prompt(questionsInitFunction) - let functionFolder = path.join(process.cwd(), 'functions'); + const answers = await inquirer.prompt(questionsInitFunction) + const functionFolder = path.join(process.cwd(), 'functions'); if (!fs.existsSync(functionFolder)) { fs.mkdirSync(functionFolder, { @@ -72,21 +73,27 @@ const initFunction = async () => { log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first deploy the function.`); } + if (!answers.runtime.commands) { + log(`Installation command for this runtime not found. You will be asked to configure the install command when you first deploy the function.`); + } + let response = await functionsCreate({ functionId: answers.id, name: answers.name, runtime: answers.runtime.id, + entrypoint: answers.runtime.entrypoint || '', + commands: answers.runtime.commands || '', parseOutput: false }) fs.mkdirSync(functionDir, "777"); - let gitInitCommands = "git clone --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched + let gitInitCommands = "git clone -b v3 --single-branch --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched let gitPullCommands = `git sparse-checkout add ${answers.runtime.id}`; /* Force use CMD as powershell does not support && */ - if (process.platform == 'win32') { + if (process.platform === 'win32') { gitInitCommands = 'cmd /c "' + gitInitCommands + '"'; gitPullCommands = 'cmd /c "' + gitPullCommands + '"'; } @@ -138,13 +145,16 @@ const initFunction = async () => { $id: response['$id'], name: response.name, runtime: response.runtime, - path: `functions/${answers.name}`, - entrypoint: answers.runtime.entrypoint || '', - ignore: answers.runtime.ignore || null, execute: response.execute, events: response.events, schedule: response.schedule, timeout: response.timeout, + enabled: response.enabled, + logging: response.logging, + entrypoint: response.entrypoint, + commands: response.commands, + ignore: answers.runtime.ignore || null, + path: `functions/${answers.name}`, }; localConfig.addFunction(data); @@ -178,15 +188,12 @@ const initCollection = async ({ all, databaseId } = {}) => { localConfig.addDatabase(database); - // TODO: Pagination? - let response = await databasesListCollections({ + const { collections, total } = await paginate(databasesListCollections, { databaseId, - queries: ['limit(100)'], parseOutput: false - }) + }, 100, 'collections'); - let collections = response.collections; - log(`Found ${collections.length} collections`); + log(`Found ${total} collections`); collections.forEach(async collection => { log(`Fetching ${collection.name} ...`); @@ -202,13 +209,8 @@ const initCollection = async ({ all, databaseId } = {}) => { } const initBucket = async () => { - // TODO: Pagination? - let response = await storageListBuckets({ - queries: ['limit(100)'], - parseOutput: false - }) + const { buckets } = await paginate(storageListBuckets, { parseOutput: false }, 100, 'buckets'); - let buckets = response.buckets; log(`Found ${buckets.length} buckets`); buckets.forEach(async bucket => { @@ -220,13 +222,8 @@ const initBucket = async () => { } const initTeam = async () => { - // TODO: Pagination? - let response = await teamsList({ - queries: ['limit(100)'], - parseOutput: false - }) + const { teams } = await paginate(teamsList, { parseOutput: false }, 100, 'teams'); - let teams = response.teams; log(`Found ${teams.length} teams`); teams.forEach(async team => { diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 744013e38..916865f44 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -15,7 +15,6 @@ class Config { const file = fs.readFileSync(this.path).toString(); this.data = JSONbig.parse(file); } catch (e) { - // console.log(`${this.path} not found. Empty data`); this.data = {}; } } @@ -25,7 +24,7 @@ class Config { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } - fs.writeFileSync(this.path, JSONbig.stringify(this.data, null, 4)); + fs.writeFileSync(this.path, JSONbig.stringify(this.data, null, 4), { mode: 0o600 }); } get(key) { diff --git a/templates/cli/lib/paginate.js.twig b/templates/cli/lib/paginate.js.twig new file mode 100644 index 000000000..309e2ef0b --- /dev/null +++ b/templates/cli/lib/paginate.js.twig @@ -0,0 +1,51 @@ +const paginate = async (action, args = {}, limit = 100, wrapper = '') => { + let pageNumber = 0; + let results = []; + let total = 0; + + while (true) { + const offset = pageNumber * limit; + + // Merge the limit and offset into the args + const response = await action({ + ...args, + queries: [ + `limit(${limit})`, + `offset(${offset})` + ] + }); + + if (wrapper === '') { + if (response.length === 0) { + break; + } + results = results.concat(response); + } else { + if (response[wrapper].length === 0) { + break; + } + results = results.concat(response[wrapper]); + } + + total = response.total; + + if (results.length >= total) { + break; + } + + pageNumber++; + } + + if (wrapper === '') { + return results; + } + + return { + [wrapper]: results, + total, + }; +} + +module.exports = { + paginate +}; \ No newline at end of file diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index a1634c3ed..ed6ba8d8c 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -169,6 +169,11 @@ const commandDescriptions = { "login": `The login command allows you to authenticate and manage a user account.`, "logout": `The logout command allows you to logout of your {{ spec.title|caseUcfirst }} account.`, "console" : `The console command allows gives you access to the APIs used by the Appwrite console.`, + "assistant": `The assistant command allows you to interact with the Appwrite Assistant AI`, + "migrations": `The migrations command allows you to migrate data between services.`, + "project": `The project command is for overall project administration.`, + "proxy": `The proxy command allows you to configure behavior for your attached domains.`, + "vcs": `The vcs command allows you to interact with VCS providers and manage your code repositories.`, "main": chalk.redBright(`${logo}${description}`), {% if sdk.test == "true" %} {% for service in spec.services %} diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 0a8fb5857..0cb4540f3 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -20,6 +20,7 @@ const getIgnores = (runtime) => { case 'kotlin': return ['build']; case 'node': + case 'bun': return ['node_modules', '.npm']; case 'php': return ['vendor']; @@ -43,27 +44,61 @@ const getEntrypoint = (runtime) => { case 'dart': return 'lib/main.dart'; case 'deno': - return 'src/mod.ts'; + return 'src/main.ts'; case 'node': - return 'src/index.js'; + return 'src/main.js'; + case 'bun': + return 'src/main.ts'; case 'php': return 'src/index.php'; case 'python': - return 'src/index.py'; + return 'src/main.py'; case 'ruby': - return 'src/index.rb'; + return 'lib/main.rb'; case 'rust': return 'main.rs'; case 'swift': - return 'Sources/swift-5.5/main.swift'; + return 'Sources/index.swift'; case 'cpp': - return 'src/index.cc'; + return 'src/main.cc'; case 'dotnet': return 'src/Index.cs'; case 'java': - return 'src/Index.java'; + return 'src/Main.java'; case 'kotlin': - return 'src/Index.kt'; + return 'src/Main.kt'; + } + + return undefined; +}; + +const getInstallCommand = (runtime) => { + const languge = runtime.split('-')[0]; + + switch (languge) { + case 'dart': + return 'dart pub get'; + case 'deno': + return "deno install"; + case 'node': + return 'npm install'; + case 'bun': + return 'bun install'; + case 'php': + return 'composer install'; + case 'python': + return 'pip install -r requirements.txt'; + case 'ruby': + return 'bundle install'; + case 'rust': + return 'cargo install'; + case 'dotnet': + return 'dotnet restore'; + case 'swift': + case 'java': + case 'kotlin': + case 'cpp': + return ''; } return undefined; @@ -171,10 +206,15 @@ const questionsInitFunction = [ parseOutput: false }) let runtimes = response["runtimes"] - let choices = runtimes.map((runtime, idx) => { + let choices = runtimes.map((runtime, idx) => { return { name: `${runtime.name} (${runtime['$id']})`, - value: { id: runtime['$id'], entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']) }, + value: { + id: runtime['$id'], + entrypoint: getEntrypoint(runtime['$id']), + ignore: getIgnores(runtime['$id']), + commands : getInstallCommand(runtime['$id']) + }, } }) return choices; @@ -269,13 +309,12 @@ const questionsDeployCollections = [ if (collections.length === 0) { throw new Error("No collections found in the current directory. Run `{{ language.params.executableName }} init collection` to fetch all your collections."); } - let choices = collections.map((collection, idx) => { + return collections.map(collection => { return { name: `${collection.name} (${collection['databaseId']} - ${collection['$id']})`, value: `${collection['databaseId']}|${collection['$id']}` } - }) - return choices; + }); } }, { @@ -295,13 +334,12 @@ const questionsDeployBuckets = [ if (buckets.length === 0) { throw new Error("No buckets found in the current directory. Run `appwrite init bucket` to fetch all your buckets."); } - let choices = buckets.map((bucket, idx) => { + return buckets.map(bucket => { return { name: `${bucket.name} (${bucket['$id']})`, value: bucket.$id } - }) - return choices; + }); } }, { @@ -319,7 +357,7 @@ const questionsGetEntrypoint = [ default: null, validate(value) { if (!value) { - return "Please enter your enrtypoint"; + return "Please enter your entrypoint"; } return true; } @@ -336,13 +374,12 @@ const questionsDeployTeams = [ if (teams.length === 0) { throw new Error("No teams found in the current directory. Run `appwrite init team` to fetch all your teams."); } - let choices = teams.map((team, idx) => { + return teams.map(team => { return { name: `${team.name} (${team['$id']})`, value: team.$id } - }) - return choices; + }); } }, { diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index 0afdca6fc..b4c51eb16 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -22,7 +22,7 @@ "windows-arm64": "pkg -t node16-win-arm64 -o build/appwrite-cli-win-arm64.exe package.json" }, "dependencies": { - "axios": "^0.27.2", + "axios": "1.5.0", "chalk": "4.1.2", "cli-table3": "^0.6.2", "commander": "^9.2.0", diff --git a/templates/dart/base/requests/api.twig b/templates/dart/base/requests/api.twig index 950658e81..43683fd6c 100644 --- a/templates/dart/base/requests/api.twig +++ b/templates/dart/base/requests/api.twig @@ -1,13 +1,13 @@ {% import 'dart/base/utils.twig' as utils %} - final Map params = { + final Map apiParams = { {{ utils.map_parameter(method.parameters.query) }} {{ utils.map_parameter(method.parameters.body) }} }; - final Map headers = { + final Map apiHeaders = { {{ utils.map_headers(method.headers) }} }; - final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: path, params: params, headers: headers); + final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: apiPath, params: apiParams, headers: apiHeaders); return {% if method.responseModel and method.responseModel != 'any' %}models.{{method.responseModel | caseUcfirst | overrideIdentifier}}.fromMap(res.data){% else %} res.data{% endif %}; diff --git a/templates/dart/base/requests/file.twig b/templates/dart/base/requests/file.twig index 97d3bcf1a..28c31c0a2 100644 --- a/templates/dart/base/requests/file.twig +++ b/templates/dart/base/requests/file.twig @@ -1,10 +1,10 @@ {% import 'dart/base/utils.twig' as utils %} - final Map params = { + final Map apiParams = { {{ utils.map_parameter(method.parameters.query) }} {{ utils.map_parameter(method.parameters.body) }} }; - final Map headers = { + final Map apiHeaders = { {{ utils.map_headers(method.headers) }} }; @@ -19,11 +19,11 @@ {% endif %} {% endfor %} final res = await client.chunkedUpload( - path: path, - params: params, + path: apiPath, + params: apiParams, paramName: paramName, idParamName: idParamName, - headers: headers, + headers: apiHeaders, onProgress: onProgress, ); diff --git a/templates/dart/base/requests/location.twig b/templates/dart/base/requests/location.twig index ab3e3dc4a..5e947787d 100644 --- a/templates/dart/base/requests/location.twig +++ b/templates/dart/base/requests/location.twig @@ -10,5 +10,5 @@ {% endif %} }; - final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: path, params: params, responseType: ResponseType.bytes); + final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: apiPath, params: params, responseType: ResponseType.bytes); return res.data; \ No newline at end of file diff --git a/templates/dart/base/requests/oauth.twig b/templates/dart/base/requests/oauth.twig index 237079f36..fd82d84ac 100644 --- a/templates/dart/base/requests/oauth.twig +++ b/templates/dart/base/requests/oauth.twig @@ -27,7 +27,7 @@ Uri url = Uri(scheme: endpoint.scheme, host: endpoint.host, port: endpoint.port, - path: endpoint.path + path, + path: endpoint.path + apiPath, query: query.join('&') ); diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index 249a5f7db..a5763d79d 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -48,7 +48,7 @@ class Query { /// Filter resources where [attribute] is between [start] and [end] (inclusive). static String between(String attribute, dynamic start, dynamic end) => - _addQuery(attribute, 'between', [start, end]); + 'between("$attribute", ${_parseValues(start)}, ${_parseValues(end)})'; /// Filter resources where [attribute] starts with [value]. static String startsWith(String attribute, String value) => diff --git a/templates/dart/lib/role.dart.twig b/templates/dart/lib/role.dart.twig index eea9e3c65..e4c968965 100644 --- a/templates/dart/lib/role.dart.twig +++ b/templates/dart/lib/role.dart.twig @@ -58,4 +58,9 @@ class Role { static String member(String id) { return 'member:$id'; } + + /// Grants access to a user with the specified label. + static String label(String name) { + return 'label:$name'; + } } \ No newline at end of file diff --git a/templates/dart/lib/services/service.dart.twig b/templates/dart/lib/services/service.dart.twig index 00141e5a3..71cba9ccc 100644 --- a/templates/dart/lib/services/service.dart.twig +++ b/templates/dart/lib/services/service.dart.twig @@ -20,7 +20,7 @@ class {{ service.name | caseUcfirst }} extends Service { {{ method.description | dartComment }} {% endif %} {% if method.type == 'location' %}Future{% else %}{% if method.responseModel and method.responseModel != 'any' %}Future{% else %}Future{% endif %}{% endif %} {{ method.name | caseCamel }}({{ _self.method_parameters(method.parameters.all, method.consumes) }}) async { - final String path = '{{ method.path }}'{% for parameter in method.parameters.path %}.replaceAll('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | overrideIdentifier }}){% endfor %}; + final String apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replaceAll('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | overrideIdentifier }}){% endfor %}; {% if 'multipart/form-data' in method.consumes %} {{ include('dart/base/requests/file.twig') }} diff --git a/templates/dart/lib/src/client_browser.dart.twig b/templates/dart/lib/src/client_browser.dart.twig index 221f7b4e1..c386af787 100644 --- a/templates/dart/lib/src/client_browser.dart.twig +++ b/templates/dart/lib/src/client_browser.dart.twig @@ -114,18 +114,18 @@ class ClientBrowser extends ClientBase with ClientMixin { headers: headers, ); final int chunksUploaded = res.data['chunksUploaded'] as int; - offset = min(size, chunksUploaded * CHUNK_SIZE); + offset = chunksUploaded * CHUNK_SIZE; } on {{spec.title | caseUcfirst}}Exception catch (_) {} } while (offset < size) { - List chunk; + var chunk; final end = min(offset + CHUNK_SIZE, size); chunk = file.bytes!.getRange(offset, end).toList(); params[paramName] = http.MultipartFile.fromBytes(paramName, chunk, filename: file.filename); headers['content-range'] = - 'bytes $offset-${min(((offset + CHUNK_SIZE) - 1), size)}/$size'; + 'bytes $offset-${min((offset + CHUNK_SIZE - 1), size - 1)}/$size'; res = await call(HttpMethod.post, path: path, headers: headers, params: params); offset += CHUNK_SIZE; @@ -134,8 +134,8 @@ class ClientBrowser extends ClientBase with ClientMixin { } final progress = UploadProgress( $id: res.data['\$id'] ?? '', - progress: min(offset - 1, size) / size * 100, - sizeUploaded: min(offset - 1, size), + progress: min(offset, size) / size * 100, + sizeUploaded: min(offset, size), chunksTotal: res.data['chunksTotal'] ?? 0, chunksUploaded: res.data['chunksUploaded'] ?? 0, ); diff --git a/templates/dart/lib/src/client_io.dart.twig b/templates/dart/lib/src/client_io.dart.twig index d90ba15c0..bcacd2895 100644 --- a/templates/dart/lib/src/client_io.dart.twig +++ b/templates/dart/lib/src/client_io.dart.twig @@ -143,7 +143,7 @@ class ClientIO extends ClientBase with ClientMixin { headers: headers, ); final int chunksUploaded = res.data['chunksUploaded'] as int; - offset = min(size, chunksUploaded * CHUNK_SIZE); + offset = chunksUploaded * CHUNK_SIZE; } on {{spec.title | caseUcfirst}}Exception catch (_) {} } @@ -156,7 +156,7 @@ class ClientIO extends ClientBase with ClientMixin { while (offset < size) { List chunk = []; if (file.bytes != null) { - final end = min(offset + CHUNK_SIZE-1, size-1); + final end = min(offset + CHUNK_SIZE, size); chunk = file.bytes!.getRange(offset, end).toList(); } else { raf!.setPositionSync(offset); @@ -165,7 +165,7 @@ class ClientIO extends ClientBase with ClientMixin { params[paramName] = http.MultipartFile.fromBytes(paramName, chunk, filename: file.filename); headers['content-range'] = - 'bytes $offset-${min(((offset + CHUNK_SIZE) - 1), size)}/$size'; + 'bytes $offset-${min((offset + CHUNK_SIZE - 1), size - 1)}/$size'; res = await call(HttpMethod.post, path: path, headers: headers, params: params); offset += CHUNK_SIZE; @@ -174,8 +174,8 @@ class ClientIO extends ClientBase with ClientMixin { } final progress = UploadProgress( $id: res.data['\$id'] ?? '', - progress: min(offset - 1, size) / size * 100, - sizeUploaded: min(offset - 1, size), + progress: min(offset, size) / size * 100, + sizeUploaded: min(offset, size), chunksTotal: res.data['chunksTotal'] ?? 0, chunksUploaded: res.data['chunksUploaded'] ?? 0, ); diff --git a/templates/dart/test/role_test.dart.twig b/templates/dart/test/role_test.dart.twig index 1c06086b7..cc0fff4b4 100644 --- a/templates/dart/test/role_test.dart.twig +++ b/templates/dart/test/role_test.dart.twig @@ -53,4 +53,10 @@ void main() { expect(Role.member('custom'), 'member:custom'); }); }); + + group('label()', () { + test('returns label', () { + expect(Role.label('admin'), 'label:admin'); + }); + }); } diff --git a/templates/deno/src/exception.ts.twig b/templates/deno/src/exception.ts.twig index 61b90cff5..aaa5e5f41 100644 --- a/templates/deno/src/exception.ts.twig +++ b/templates/deno/src/exception.ts.twig @@ -1,17 +1,17 @@ export class {{ spec.title | caseUcfirst}}Exception { - message: String; - code: Number; + message: string; + code: number; response: any; - type: String; + type: string; - constructor(message: String, code: Number = 0, type: String = "", response: any = "") { + constructor(message: string, code: number = 0, type: string = "", response: any = "") { this.message = message; this.code = code; this.type = type; this.response = response; } - public toString(): String { + public toString(): string { return `${this.message} - ${this.code} - ${this.type} - ${JSON.stringify(this.response)}`; } -} \ No newline at end of file +} diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index dd7c7a1cc..ce87185fb 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -31,7 +31,7 @@ export class Query { `isNotNull("${attribute}")`; static between = (attribute: string, start: string|number, end: string|number): string => - `between("${attribute}", [${Query.parseValues(start)},${Query.parseValues(end)}])`; + `between("${attribute}", ${Query.parseValues(start)}, ${Query.parseValues(end)})`; static startsWith = (attribute: string, value: string): string => Query.addQuery(attribute, "startsWith", value); diff --git a/templates/deno/src/role.ts.twig b/templates/deno/src/role.ts.twig index d94e398c6..79f8c6b62 100644 --- a/templates/deno/src/role.ts.twig +++ b/templates/deno/src/role.ts.twig @@ -1,34 +1,100 @@ +/** + * Helper class to generate role strings for `Permission`. + */ export class Role { + + /** + * Grants access to anyone. + * + * This includes authenticated and unauthenticated users. + * + * @returns {string} + */ public static any(): string { return 'any' } + /** + * Grants access to a specific user by user ID. + * + * You can optionally pass verified or unverified for + * `status` to target specific types of users. + * + * @param {string} id + * @param {string} status + * @returns {string} + */ public static user(id: string, status: string = ''): string { - if(status === '') { + if (status === '') { return `user:${id}` } return `user:${id}/${status}` } - + + /** + * Grants access to any authenticated or anonymous user. + * + * You can optionally pass verified or unverified for + * `status` to target specific types of users. + * + * @param {string} status + * @returns {string} + */ public static users(status: string = ''): string { - if(status === '') { + if (status === '') { return 'users' } return `users/${status}` } - + + /** + * Grants access to any guest user without a session. + * + * Authenticated users don't have access to this role. + * + * @returns {string} + */ public static guests(): string { return 'guests' } - + + /** + * Grants access to a team by team ID. + * + * You can optionally pass a role for `role` to target + * team members with the specified role. + * + * @param {string} id + * @param {string} role + * @returns {string} + */ public static team(id: string, role: string = ''): string { - if(role === '') { + if (role === '') { return `team:${id}` } return `team:${id}/${role}` } + /** + * Grants access to a specific member of a team. + * + * When the member is removed from the team, they will + * no longer have access. + * + * @param {string} id + * @returns {string} + */ public static member(id: string): string { return `member:${id}` } + + /** + * Grants access to a user with the specified label. + * + * @param {string} name + * @returns {string} + */ + public static label(name: string): string { + return `label:${name}` + } } \ No newline at end of file diff --git a/templates/deno/src/services/service.ts.twig b/templates/deno/src/services/service.ts.twig index 8fdd11841..dbaecfca6 100644 --- a/templates/deno/src/services/service.ts.twig +++ b/templates/deno/src/services/service.ts.twig @@ -72,8 +72,8 @@ export class {{ service.name | caseUcfirst }} extends Service { {% endif %} {% endfor %} - let path = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; - let payload: Payload = {}; + const apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; + const payload: Payload = {}; {% for parameter in method.parameters.query %} if (typeof {{ parameter.name | caseCamel | escapeKeyword }} !== 'undefined') { @@ -92,7 +92,7 @@ export class {{ service.name | caseUcfirst }} extends Service { const size = {{ parameter.name | caseCamel | escapeKeyword }}.size; - const headers: { [header: string]: string } = { + const apiHeaders: { [header: string]: string } = { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} @@ -110,7 +110,7 @@ export class {{ service.name | caseUcfirst }} extends Service { {% if parameter.isUploadID %} if({{ parameter.name | caseCamel | escapeKeyword }} != 'unique()') { try { - response = await this.client.call('get', path + '/' + {{ parameter.name }}, headers); + response = await this.client.call('get', apiPath + '/' + {{ parameter.name }}, apiHeaders); chunksUploaded = response.chunksUploaded; } catch(e) { } @@ -131,7 +131,7 @@ export class {{ service.name | caseUcfirst }} extends Service { const end = start + currentPosition; if(!lastUpload || currentChunk !== 1) { - headers['content-range'] = 'bytes ' + start + '-' + end + '/' + size; + apiHeaders['content-range'] = 'bytes ' + start + '-' + end + '/' + size; } let uploadableChunkTrimmed: Uint8Array; @@ -146,12 +146,12 @@ export class {{ service.name | caseUcfirst }} extends Service { } if (id) { - headers['x-{{spec.title | caseLower }}-id'] = id; + apiHeaders['x-{{spec.title | caseLower }}-id'] = id; } payload['{{ parameter.name }}'] = { type: 'file', file: new File([uploadableChunkTrimmed], {{ parameter.name | caseCamel | escapeKeyword }}.filename), filename: {{ parameter.name | caseCamel | escapeKeyword }}.filename }; - response = await this.client.call('{{ method.method | caseLower }}', path, headers, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); + response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); if (!id) { id = response['$id']; @@ -193,7 +193,7 @@ export class {{ service.name | caseUcfirst }} extends Service { {% endif %} {% endfor %} {% else %} - return await this.client.call('{{ method.method | caseLower }}', path, { + return await this.client.call('{{ method.method | caseLower }}', apiPath, { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} diff --git a/templates/deno/test/id.test.ts.twig b/templates/deno/test/id.test.ts.twig new file mode 100644 index 000000000..6e7dcbed0 --- /dev/null +++ b/templates/deno/test/id.test.ts.twig @@ -0,0 +1,8 @@ +import {assertEquals} from "https://deno.land/std@0.204.0/assert/mod.ts"; +import {describe, it as test} from "https://deno.land/std@0.149.0/testing/bdd.ts"; +import {ID} from "../src/id.ts"; + +describe("ID", () => { + test('unique', () => assertEquals(ID.unique(), 'unique()')); + test('custom', () => assertEquals(ID.custom('custom'), 'custom')); +}); diff --git a/templates/deno/test/permission.test.ts.twig b/templates/deno/test/permission.test.ts.twig new file mode 100644 index 000000000..a9366fd43 --- /dev/null +++ b/templates/deno/test/permission.test.ts.twig @@ -0,0 +1,12 @@ +import { assertEquals } from "https://deno.land/std@0.204.0/assert/mod.ts"; +import { describe, it as test } from "https://deno.land/std@0.149.0/testing/bdd.ts"; +import {Permission} from "../src/permission.ts"; +import {Role} from "../src/role.ts"; + +describe('Permission', () => { + test('read', () => assertEquals(Permission.read(Role.any()), 'read("any")')); + test('write', () => assertEquals(Permission.write(Role.any()), 'write("any")')); + test('create', () => assertEquals(Permission.create(Role.any()), 'create("any")')); + test('update', () => assertEquals(Permission.update(Role.any()), 'update("any")')); + test('delete', () => assertEquals(Permission.delete(Role.any()), 'delete("any")')); +}) diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig new file mode 100644 index 000000000..a307e55d2 --- /dev/null +++ b/templates/deno/test/query.test.ts.twig @@ -0,0 +1,175 @@ +import {describe, it as test} from "https://deno.land/std@0.149.0/testing/bdd.ts"; +import {assertEquals} from "https://deno.land/std@0.204.0/assert/assert_equals.ts"; +import {Query, QueryTypes} from "../src/query.ts"; + +type BasicFilterQueryTest = { + description: string; + value: QueryTypes; + expectedValues: string; +} + +const tests: BasicFilterQueryTest[] = [ + { + description: 'with a string', + value: 's', + expectedValues: '["s"]' + }, + { + description: 'with a integer', + value: 1, + expectedValues: '[1]' + }, + { + description: 'with a double', + value: 1.2, + expectedValues: '[1.2]' + }, + { + description: 'with a whole number double', + value: 1.0, + expectedValues: '[1]' + }, + { + description: 'with a bool', + value: false, + expectedValues: '[false]' + }, + { + description: 'with a list', + value: ['a', 'b', 'c'], + expectedValues: '["a","b","c"]' + } +]; + +describe('Query', () => { + describe('basic filter equal', () => { + for (const t of tests) { + test(t.description, () => + assertEquals( + Query.equal("attr", t.value), + `equal("attr", ${t.expectedValues})`, + ) + ) + } + }) + + describe('basic filter notEqual', () => { + for (const t of tests) { + test(t.description, () => + assertEquals( + Query.notEqual("attr", t.value), + `notEqual("attr", ${t.expectedValues})`, + ) + ) + } + }); + + describe('basic filter lessThan', () => { + for (const t of tests) { + test(t.description, () => + assertEquals( + Query.lessThan("attr", t.value), + `lessThan("attr", ${t.expectedValues})`, + ) + ) + } + }); + + describe('basic filter lessThanEqual', () => { + for (const t of tests) { + test(t.description, () => + assertEquals( + Query.lessThanEqual("attr", t.value), + `lessThanEqual("attr", ${t.expectedValues})`, + ) + ) + } + }); + + describe('basic filter greaterThan', () => { + for (const t of tests) { + test(t.description, () => + assertEquals( + Query.greaterThan("attr", t.value), + `greaterThan("attr", ${t.expectedValues})`, + ) + ) + } + }); + + describe('basic filter greaterThanEqual', () => { + for (const t of tests) { + test(t.description, () => + assertEquals( + Query.greaterThanEqual("attr", t.value), + `greaterThanEqual("attr", ${t.expectedValues})`, + ) + ) + } + }); + + test('search', () => assertEquals( + Query.search('attr', 'keyword1 keyword2'), + 'search("attr", ["keyword1 keyword2"])', + )); + + test('isNull', () => assertEquals( + Query.isNull('attr'), + 'isNull("attr")', + )); + + test('isNotNull', () => assertEquals( + Query.isNotNull('attr'), + 'isNotNull("attr")', + )); + + describe('between', () => { + test('with integers', () => assertEquals( + Query.between('attr', 1, 2), + 'between("attr", 1, 2)' + )); + test('with doubles', () => assertEquals( + Query.between('attr', 1.2, 2.2), + 'between("attr", 1.2, 2.2)' + )); + test('with strings', () => assertEquals( + Query.between('attr', "a", "z"), + 'between("attr", "a", "z")' + )); + }); + + test('select', () => assertEquals( + Query.select(['attr1', 'attr2']), + 'select(["attr1","attr2"])', + )); + + test('orderAsc', () => assertEquals( + Query.orderAsc('attr'), + 'orderAsc("attr")', + )); + + test('orderDesc', () => assertEquals( + Query.orderDesc('attr'), + 'orderDesc("attr")', + )); + + test('cursorBefore', () => assertEquals( + Query.cursorBefore('attr'), + 'cursorBefore("attr")', + )); + + test('cursorAfter', () => assertEquals( + Query.cursorAfter('attr'), + 'cursorAfter("attr")', + )); + + test('limit', () => assertEquals( + Query.limit(1), + 'limit(1)' + )); + + test('offset', () => assertEquals( + Query.offset(1), + 'offset(1)' + )); +}) diff --git a/templates/deno/test/role.test.ts.twig b/templates/deno/test/role.test.ts.twig new file mode 100644 index 000000000..e3f155da0 --- /dev/null +++ b/templates/deno/test/role.test.ts.twig @@ -0,0 +1,16 @@ +import { assertEquals } from "https://deno.land/std@0.204.0/assert/mod.ts"; +import { describe, it as test } from "https://deno.land/std@0.149.0/testing/bdd.ts"; +import {Role} from "../src/role.ts"; + +describe('Role', () => { + test('any', () => assertEquals(Role.any(), 'any')); + test('user without status', () => assertEquals(Role.user('custom'), 'user:custom')); + test('user with status', () => assertEquals(Role.user('custom', 'verified'), 'user:custom/verified')); + test('users without status', () => assertEquals(Role.users(), 'users')); + test('users with status', () => assertEquals(Role.users('verified'), 'users/verified')); + test('guests', () => assertEquals(Role.guests(), 'guests')); + test('team without role', () => assertEquals(Role.team('custom'), 'team:custom')) + test('team with role', () => assertEquals(Role.team('custom', 'owner'), 'team:custom/owner')) + test('member', () => assertEquals(Role.member('custom'), 'member:custom')) + test('label', () => assertEquals(Role.label('admin'), 'label:admin')) +}) diff --git a/templates/deno/test/services/service.test.ts.twig b/templates/deno/test/services/service.test.ts.twig new file mode 100644 index 000000000..00743c165 --- /dev/null +++ b/templates/deno/test/services/service.test.ts.twig @@ -0,0 +1,56 @@ +import {afterEach, describe, it as test} from "https://deno.land/std@0.204.0/testing/bdd.ts"; +import {restore, stub} from "https://deno.land/std@0.204.0/testing/mock.ts"; +import {assertEquals} from "https://deno.land/std@0.204.0/assert/assert_equals.ts"; +import { {{ service.name | caseUcfirst }} } from "../../src/services/{{ service.name | caseCamel }}.ts"; +import {Client} from "../../src/client.ts"; +import {InputFile} from "../../src/inputFile.ts" + +describe('{{ service.name | caseUcfirst }} service', () => { + const client = new Client(); + const {{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}(client); + + afterEach(() => restore()) + + {% for method in service.methods ~%} + test('test method {{ method.name | caseCamel }}()', async () => { + {%~ if method.type == 'webAuth' %} + const data = ''; + {%~ elseif method.type == 'location' %} + const data = new Uint8Array(0); + {%~ else %} + {%- if method.responseModel and method.responseModel != 'any' %} + const data = { + {%- for definition in spec.definitions ~%}{%~ if definition.name == method.responseModel -%}{%~ for property in definition.properties | filter((param) => param.required) ~%} + '{{property.name | escapeDollarSign}}': {% if property.type == 'object' %}{}{% elseif property.type == 'array' %}[]{% elseif property.type == 'string' %}'{{property.example | escapeDollarSign}}'{% elseif property.type == 'boolean' %}true{% else %}{{property.example}}{% endif %},{%~ endfor ~%}{% set break = true %}{%- else -%}{% set continue = true %}{%- endif -%}{%~ endfor -%} + }; + {%~ else %} + const data = ''; + {%~ endif %} + {%~ endif %} + + {%~ if method.type == 'location' %} + const stubbedFetch = stub(globalThis, 'fetch', () => Promise.resolve(new Response(data.buffer))); + {%~ elseif method.responseModel and method.responseModel != 'any' %} + const stubbedFetch = stub(globalThis, 'fetch', () => Promise.resolve(Response.json(data))); + {%~ else %} + const stubbedFetch = stub(globalThis, 'fetch', () => Promise.resolve(new Response(data))) + {%~ endif %} + + const response = await {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} + {% if parameter.type == 'object' %}{}{% elseif parameter.type == 'array' %}[]{% elseif parameter.type == 'file' %}InputFile.fromBuffer(new Uint8Array(0), 'image.png'){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}'{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}'{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%},{%~ endfor ~%} + ); + + {%~ if method.type == 'location' %} + const buffer = await response.arrayBuffer(); + assertEquals(buffer.byteLength, 0); + {%~ elseif not method.responseModel or method.responseModel == 'any' %} + const text = await response.text(); + assertEquals(text, data); + {%~ else %} + assertEquals(response, data); + {%~ endif %} + stubbedFetch.restore(); + }); + + {% endfor %} +}) diff --git a/templates/dotnet/README.md.twig b/templates/dotnet/README.md.twig index 8315cd5fc..86b47f298 100644 --- a/templates/dotnet/README.md.twig +++ b/templates/dotnet/README.md.twig @@ -45,10 +45,31 @@ dotnet add package {{ spec.title | caseUcfirst }} --version {{ sdk.version }} {{ sdk.gettingStarted|raw }} {% endif %} +### Preparing Models for Databases API + +For the .NET SDK, we use the `Newtonsoft.Json` library for serialization/deserialization support. The default behavior converts property names from `PascalCase` to `camelCase` on serializing to JSON. In case the names of attributes in your Appwrite collection are not created in `camelCase`, this serializer behavior can cause errors due to mismatches in the names in the serialized JSON and the actual attribute names in your collection. + +The way to fix this is to add the `JsonProperty` attribute to the properties in the POCO class you create for your model. + +For e.g., if you have two attributes, `name` (`string` type) and `release_date` (`DateTime` type), your POCO class would be created as follows: + +```csharp +public class TestModel +{ + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("release_date")] + public DateTime ReleaseDate { get; set; } +} +``` + +The `JsonProperty` attribute will ensure that your data object for the Appwrite database is serialized with the correct names. + ## Contribution This library is auto-generated by Appwrite custom [SDK Generator](https://github.com/appwrite/sdk-generator). To learn more about how you can help us improve this SDK, please check the [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md) before sending a pull-request. ## License -Please see the [{{spec.licenseName}} license]({{spec.licenseURL}}) file for more information. \ No newline at end of file +Please see the [{{spec.licenseName}} license]({{spec.licenseURL}}) file for more information. diff --git a/templates/dotnet/base/params.twig b/templates/dotnet/base/params.twig index 1ce2c6fd2..0918c9d65 100644 --- a/templates/dotnet/base/params.twig +++ b/templates/dotnet/base/params.twig @@ -4,7 +4,7 @@ {%~ endfor %} - var parameters = new Dictionary() + var apiParameters = new Dictionary() { {%~ for parameter in method.parameters.query | merge(method.parameters.body) %} { "{{ parameter.name }}", {{ utils.map_parameter(parameter) }} }{% if not loop.last %},{% endif %} @@ -12,7 +12,7 @@ {%~ endfor %} }; - var headers = new Dictionary() + var apiHeaders = new Dictionary() { {%~ for key, header in method.headers %} { "{{ key }}", "{{ header }}" }{% if not loop.last %},{% endif %} diff --git a/templates/dotnet/base/requests/api.twig b/templates/dotnet/base/requests/api.twig index bf030e99c..0b10b627d 100644 --- a/templates/dotnet/base/requests/api.twig +++ b/templates/dotnet/base/requests/api.twig @@ -1,11 +1,11 @@ {% import 'dotnet/base/utils.twig' as utils %} return _client.Call{% if method.type != 'webAuth' %}<{{ utils.resultType(spec.title, method) }}>{% endif %}( method: "{{ method.method | caseUpper }}", - path: path, - headers: headers, + path: apiPath, + headers: apiHeaders, {%~ if not method.responseModel %} - parameters: parameters.Where(it => it.Value != null).ToDictionary(it => it.Key, it => it.Value)!); + parameters: apiParameters.Where(it => it.Value != null).ToDictionary(it => it.Key, it => it.Value)!); {%~ else %} - parameters: parameters.Where(it => it.Value != null).ToDictionary(it => it.Key, it => it.Value)!, + parameters: apiParameters.Where(it => it.Value != null).ToDictionary(it => it.Key, it => it.Value)!, convert: Convert); {%~ endif %} \ No newline at end of file diff --git a/templates/dotnet/base/requests/file.twig b/templates/dotnet/base/requests/file.twig index 902b5043d..83bb3d3e7 100644 --- a/templates/dotnet/base/requests/file.twig +++ b/templates/dotnet/base/requests/file.twig @@ -7,9 +7,9 @@ {%~ endfor %} return _client.ChunkedUpload( - path, - headers, - parameters, + apiPath, + apiHeaders, + apiParameters, {%~ if method.responseModel %} Convert, {%~ endif %} diff --git a/templates/dotnet/base/requests/location.twig b/templates/dotnet/base/requests/location.twig index 6b6b1301e..d9f25ea1c 100644 --- a/templates/dotnet/base/requests/location.twig +++ b/templates/dotnet/base/requests/location.twig @@ -1,5 +1,5 @@ return _client.Call( method: "{{ method.method | caseUpper }}", - path: path, - headers: headers, - parameters: parameters.Where(it => it.Value != null).ToDictionary(it => it.Key, it => it.Value)!); \ No newline at end of file + path: apiPath, + headers: apiHeaders, + parameters: apiParameters.Where(it => it.Value != null).ToDictionary(it => it.Key, it => it.Value)!); diff --git a/templates/dotnet/docs/example.md.twig b/templates/dotnet/docs/example.md.twig index d90cde076..2ce25f701 100644 --- a/templates/dotnet/docs/example.md.twig +++ b/templates/dotnet/docs/example.md.twig @@ -1,4 +1,5 @@ using {{ spec.title | caseUcfirst }}; +using {{ spec.title | caseUcfirst }}.Services; using {{ spec.title | caseUcfirst }}.Models; var client = new Client() diff --git a/templates/dotnet/icon.png b/templates/dotnet/icon.png index 460b9f040..dadbae8ba 100644 Binary files a/templates/dotnet/icon.png and b/templates/dotnet/icon.png differ diff --git a/templates/dotnet/src/Appwrite/Appwrite.csproj.twig b/templates/dotnet/src/Appwrite/Appwrite.csproj.twig index fbb3d1b75..74673a1eb 100644 --- a/templates/dotnet/src/Appwrite/Appwrite.csproj.twig +++ b/templates/dotnet/src/Appwrite/Appwrite.csproj.twig @@ -9,6 +9,7 @@ {{sdk.shortDescription}} icon.png + README.md {{spec.licenseName}} {{sdk.gitURL}} git @@ -22,6 +23,7 @@ + diff --git a/templates/dotnet/src/Appwrite/Client.cs.twig b/templates/dotnet/src/Appwrite/Client.cs.twig index 3692008d3..0f11ee45f 100644 --- a/templates/dotnet/src/Appwrite/Client.cs.twig +++ b/templates/dotnet/src/Appwrite/Client.cs.twig @@ -354,7 +354,7 @@ namespace {{ spec.title | caseUcfirst }} parameters[paramName] = content; headers["Content-Range"] = - $"bytes {offset}-{Math.Min(offset + ChunkSize - 1, size)}/{size}"; + $"bytes {offset}-{Math.Min(offset + ChunkSize - 1, size - 1)}/{size}"; result = await Call>( method: "POST", diff --git a/templates/dotnet/src/Appwrite/Exception.cs.twig b/templates/dotnet/src/Appwrite/Exception.cs.twig index eb967f973..e78d78c2c 100644 --- a/templates/dotnet/src/Appwrite/Exception.cs.twig +++ b/templates/dotnet/src/Appwrite/Exception.cs.twig @@ -5,14 +5,17 @@ namespace {{spec.title | caseUcfirst}} public class {{spec.title | caseUcfirst}}Exception : Exception { public int? Code { get; set; } + public string? Type { get; set; } = null; public string? Response { get; set; } = null; public {{spec.title | caseUcfirst}}Exception( string? message = null, int? code = null, + string? type = null, string? response = null) : base(message) { this.Code = code; + this.Type = type; this.Response = response; } public {{spec.title | caseUcfirst}}Exception(string message, Exception inner) diff --git a/templates/dotnet/src/Appwrite/Models/Model.cs.twig b/templates/dotnet/src/Appwrite/Models/Model.cs.twig index f032c2961..6e83a251a 100644 --- a/templates/dotnet/src/Appwrite/Models/Model.cs.twig +++ b/templates/dotnet/src/Appwrite/Models/Model.cs.twig @@ -40,7 +40,7 @@ namespace {{ spec.title | caseUcfirst }}.Models public static {{ definition.name | caseUcfirst | overrideIdentifier}} From(Dictionary map) => new {{ definition.name | caseUcfirst | overrideIdentifier}}( {%~ for property in definition.properties %} - {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject>>().Select(it => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: it)).ToList(){% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: ((JObject)map["{{ property.name }}"]).ToObject>()!){% endif %}{% else %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<{{ property | typeName }}>(){% else %}{% if property.type == "integer" or property.type == "number" %}{% if not property.required %}map["{{ property.name }}"] == null ? null : {% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]){% else %}{% if property.type == "bool" %}({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"]{% endif %}map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString(){% endif %}{% endif %}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + {{ property.name | caseCamel | escapeKeyword | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject>>().Select(it => {{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: it)).ToList(){% else %}{{property.sub_schema | caseUcfirst | overrideIdentifier}}.From(map: ((JObject)map["{{ property.name }}"]).ToObject>()!){% endif %}{% else %}{% if property.type == 'array' %}((JArray)map["{{ property.name }}"]).ToObject<{{ property | typeName }}>(){% else %}{% if property.type == "integer" or property.type == "number" %}{% if not property.required %}map["{{ property.name }}"] == null ? null : {% endif %}Convert.To{% if property.type == "integer" %}Int64{% else %}Double{% endif %}(map["{{ property.name }}"]){% else %}{% if property.type == "boolean" %}({{ property | typeName }}{% if not property.required %}?{% endif %})map["{{ property.name }}"]{% else %}map["{{ property.name }}"]{% if not property.required %}?{% endif %}.ToString(){% endif %}{% endif %}{% endif %}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} {%~ endfor %} {%~ if definition.additionalProperties %} diff --git a/templates/dotnet/src/Appwrite/Query.cs.twig b/templates/dotnet/src/Appwrite/Query.cs.twig index c940f4d6b..f10bac28c 100644 --- a/templates/dotnet/src/Appwrite/Query.cs.twig +++ b/templates/dotnet/src/Appwrite/Query.cs.twig @@ -63,17 +63,17 @@ namespace Appwrite public static string Between(string attribute, string start, string end) { - return AddQuery(attribute, "between", new List { start, end }); + return $"between(\"{attribute}\", \"{start}\", \"{end}\")"; } public static string Between(string attribute, int start, int end) { - return AddQuery(attribute, "between", new List { start, end }); + return $"between(\"{attribute}\", {start}, {end})"; } public static string Between(string attribute, double start, double end) { - return AddQuery(attribute, "between", new List { start, end }); + return $"between(\"{attribute}\", {start}, {end})"; } public static string Select(List attributes) diff --git a/templates/dotnet/src/Appwrite/Role.cs.twig b/templates/dotnet/src/Appwrite/Role.cs.twig index 7fc1fdbcd..b3ecf2610 100644 --- a/templates/dotnet/src/Appwrite/Role.cs.twig +++ b/templates/dotnet/src/Appwrite/Role.cs.twig @@ -1,12 +1,28 @@ namespace Appwrite { + /// + /// Helper class to generate role strings for Permission. + /// public static class Role { + /// + /// Grants access to anyone. + /// + /// This includes authenticated and unauthenticated users. + /// + /// public static string Any() { return "any"; } + /// + /// Grants access to a specific user by user ID. + /// + /// You can optionally pass verified or unverified for + /// status to target specific types of users. + /// + /// public static string User(string id, string status = "") { return status == string.Empty @@ -14,6 +30,13 @@ namespace Appwrite : $"user:{id}/{status}"; } + /// + /// Grants access to any authenticated or anonymous user. + /// + /// You can optionally pass verified or unverified for + /// status to target specific types of users. + /// + /// public static string Users(string status = "") { return status == string.Empty @@ -21,11 +44,24 @@ namespace Appwrite $"users/{status}"; } + /// + /// Grants access to any guest user without a session. + /// + /// Authenticated users don't have access to this role. + /// + /// public static string Guests() { return "guests"; } + /// + /// Grants access to a team by team ID. + /// + /// You can optionally pass a role for role to target + /// team members with the specified role. + /// + /// public static string Team(string id, string role = "") { return role == string.Empty @@ -33,9 +69,24 @@ namespace Appwrite : $"team:{id}/{role}"; } + /// + /// Grants access to a specific member of a team. + /// + /// When the member is removed from the team, they will + /// no longer have access. + /// + /// public static string Member(string id) { return $"member:{id}"; } + + /// + /// Grants access to a user with the specified label. + /// + public static string Label(string name) + { + return $"label:{name}"; + } } } \ No newline at end of file diff --git a/templates/dotnet/src/Appwrite/Services/ServiceTemplate.cs.twig b/templates/dotnet/src/Appwrite/Services/ServiceTemplate.cs.twig index a7711ec93..732d36ac7 100644 --- a/templates/dotnet/src/Appwrite/Services/ServiceTemplate.cs.twig +++ b/templates/dotnet/src/Appwrite/Services/ServiceTemplate.cs.twig @@ -27,7 +27,7 @@ namespace {{ spec.title | caseUcfirst }}.Services /// public Task{% if method.type != "webAuth" %}<{{ utils.resultType(spec.title, method) }}>{% endif %} {{ method.name | caseUcfirst }}({{ utils.method_parameters(method.parameters, method.consumes) }}) { - var path = "{{ method.path }}"{% if method.parameters.path | length == 0 %};{% endif %} + var apiPath = "{{ method.path }}"{% if method.parameters.path | length == 0 %};{% endif %} {{~ include('dotnet/base/params.twig') }} diff --git a/templates/flutter/base/requests/api.twig b/templates/flutter/base/requests/api.twig index 233b53490..512372d45 100644 --- a/templates/flutter/base/requests/api.twig +++ b/templates/flutter/base/requests/api.twig @@ -1,13 +1,13 @@ {% import 'flutter/base/utils.twig' as utils %} - final Map params = { + final Map apiParams = { {{- utils.map_parameter(method.parameters.query) -}} {{~ utils.map_parameter(method.parameters.body) }} }; - final Map headers = { + final Map apiHeaders = { {{~ utils.map_headers(method.headers) }} }; - final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: path, params: params, headers: headers); + final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: apiPath, params: apiParams, headers: apiHeaders); return {% if method.responseModel and method.responseModel != 'any' %}models.{{method.responseModel | caseUcfirst | overrideIdentifier}}.fromMap(res.data){% else %} res.data{% endif %}; diff --git a/templates/flutter/base/requests/file.twig b/templates/flutter/base/requests/file.twig index 7a5777115..3030fb494 100644 --- a/templates/flutter/base/requests/file.twig +++ b/templates/flutter/base/requests/file.twig @@ -1,10 +1,10 @@ {% import 'flutter/base/utils.twig' as utils %} - final Map params = { + final Map apiParams = { {{~ utils.map_parameter(method.parameters.query) }} {{~ utils.map_parameter(method.parameters.body) }} }; - final Map headers = { + final Map apiHeaders = { {{~ utils.map_headers(method.headers) }} }; @@ -19,11 +19,11 @@ {% endif %} {% endfor %} final res = await client.chunkedUpload( - path: path, - params: params, + path: apiPath, + params: apiParams, paramName: paramName, idParamName: idParamName, - headers: headers, + headers: apiHeaders, onProgress: onProgress, ); diff --git a/templates/flutter/base/requests/location.twig b/templates/flutter/base/requests/location.twig index 76be6e723..1135c3cad 100644 --- a/templates/flutter/base/requests/location.twig +++ b/templates/flutter/base/requests/location.twig @@ -10,5 +10,5 @@ {% endif %} }; - final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: path, params: params, responseType: ResponseType.bytes); + final res = await client.call(HttpMethod.{{ method.method | caseLower }}, path: apiPath, params: params, responseType: ResponseType.bytes); return res.data; \ No newline at end of file diff --git a/templates/flutter/base/requests/oauth.twig b/templates/flutter/base/requests/oauth.twig index 36538cd3d..4c5617bd9 100644 --- a/templates/flutter/base/requests/oauth.twig +++ b/templates/flutter/base/requests/oauth.twig @@ -27,7 +27,7 @@ Uri url = Uri(scheme: endpoint.scheme, host: endpoint.host, port: endpoint.port, - path: endpoint.path + path, + path: endpoint.path + apiPath, query: query.join('&') ); diff --git a/templates/flutter/lib/services/service.dart.twig b/templates/flutter/lib/services/service.dart.twig index 0cc609d03..03be8528e 100644 --- a/templates/flutter/lib/services/service.dart.twig +++ b/templates/flutter/lib/services/service.dart.twig @@ -21,7 +21,7 @@ class {{ service.name | caseUcfirst }} extends Service { {{ method.description|dartComment }} {% endif %} {% if method.type == 'webAuth' %}Future{% elseif method.type == 'location' %}Future{% else %}{% if method.responseModel and method.responseModel != 'any' %}Future{% else %}Future{% endif %}{% endif %} {{ method.name | caseCamel }}({{ _self.method_parameters(method.parameters.all, method.consumes) }}) async { - {% if method.parameters.path | length > 0 %}final{% else %}const{% endif %} String path = '{{ method.path }}'{% for parameter in method.parameters.path %}.replaceAll('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | overrideIdentifier }}){% endfor %}; + {% if method.parameters.path | length > 0 %}final{% else %}const{% endif %} String apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replaceAll('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | overrideIdentifier }}){% endfor %}; {% if 'multipart/form-data' in method.consumes %} {{ include('flutter/base/requests/file.twig') }} diff --git a/templates/flutter/lib/src/client_browser.dart.twig b/templates/flutter/lib/src/client_browser.dart.twig index 948915c5f..52132bb9f 100644 --- a/templates/flutter/lib/src/client_browser.dart.twig +++ b/templates/flutter/lib/src/client_browser.dart.twig @@ -143,7 +143,7 @@ class ClientBrowser extends ClientBase with ClientMixin { headers: headers, ); final int chunksUploaded = res.data['chunksUploaded'] as int; - offset = min(size, chunksUploaded * CHUNK_SIZE); + offset = chunksUploaded * CHUNK_SIZE; } on {{spec.title | caseUcfirst}}Exception catch (_) {} } @@ -154,7 +154,7 @@ class ClientBrowser extends ClientBase with ClientMixin { params[paramName] = http.MultipartFile.fromBytes(paramName, chunk, filename: file.filename); headers['content-range'] = - 'bytes $offset-${min(((offset + CHUNK_SIZE) - 1), size)}/$size'; + 'bytes $offset-${min((offset + CHUNK_SIZE - 1), size - 1)}/$size'; res = await call(HttpMethod.post, path: path, headers: headers, params: params); offset += CHUNK_SIZE; @@ -163,8 +163,8 @@ class ClientBrowser extends ClientBase with ClientMixin { } final progress = UploadProgress( $id: res.data['\$id'] ?? '', - progress: min(offset - 1, size) / size * 100, - sizeUploaded: min(offset - 1, size), + progress: min(offset, size) / size * 100, + sizeUploaded: min(offset, size), chunksTotal: res.data['chunksTotal'] ?? 0, chunksUploaded: res.data['chunksUploaded'] ?? 0, ); diff --git a/templates/flutter/lib/src/client_io.dart.twig b/templates/flutter/lib/src/client_io.dart.twig index a200fcb49..371dd51ee 100644 --- a/templates/flutter/lib/src/client_io.dart.twig +++ b/templates/flutter/lib/src/client_io.dart.twig @@ -263,7 +263,7 @@ class ClientIO extends ClientBase with ClientMixin { headers: headers, ); final int chunksUploaded = res.data['chunksUploaded'] as int; - offset = min(size, chunksUploaded * CHUNK_SIZE); + offset = chunksUploaded * CHUNK_SIZE; } on {{spec.title | caseUcfirst}}Exception catch (_) {} } @@ -276,7 +276,7 @@ class ClientIO extends ClientBase with ClientMixin { while (offset < size) { List chunk = []; if (file.bytes != null) { - final end = min(offset + CHUNK_SIZE-1, size-1); + final end = min(offset + CHUNK_SIZE, size); chunk = file.bytes!.getRange(offset, end).toList(); } else { raf!.setPositionSync(offset); @@ -285,7 +285,7 @@ class ClientIO extends ClientBase with ClientMixin { params[paramName] = http.MultipartFile.fromBytes(paramName, chunk, filename: file.filename); headers['content-range'] = - 'bytes $offset-${min(((offset + CHUNK_SIZE) - 1), size)}/$size'; + 'bytes $offset-${min((offset + CHUNK_SIZE - 1), size - 1)}/$size'; res = await call(HttpMethod.post, path: path, headers: headers, params: params); offset += CHUNK_SIZE; @@ -294,8 +294,8 @@ class ClientIO extends ClientBase with ClientMixin { } final progress = UploadProgress( $id: res.data['\$id'] ?? '', - progress: min(offset - 1, size) / size * 100, - sizeUploaded: min(offset - 1, size), + progress: min(offset, size) / size * 100, + sizeUploaded: min(offset, size), chunksTotal: res.data['chunksTotal'] ?? 0, chunksUploaded: res.data['chunksUploaded'] ?? 0, ); @@ -314,7 +314,9 @@ class ClientIO extends ClientBase with ClientMixin { callbackUrlScheme: callbackUrlScheme != null && _customSchemeAllowed ? callbackUrlScheme : "{{spec.title | caseLower}}-callback-" + config['project']!, - preferEphemeral: true, + options: const FlutterWebAuth2Options( + intentFlags: ephemeralIntentFlags, + ), ).then((value) async { Uri url = Uri.parse(value); final key = url.queryParameters['key']; diff --git a/templates/flutter/pubspec.yaml.twig b/templates/flutter/pubspec.yaml.twig index c2463e971..5b0137296 100644 --- a/templates/flutter/pubspec.yaml.twig +++ b/templates/flutter/pubspec.yaml.twig @@ -13,15 +13,15 @@ platforms: web: windows: environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=2.17.0 <4.0.0' dependencies: flutter: sdk: flutter cookie_jar: ^4.0.3 device_info_plus: ^9.0.2 - flutter_web_auth_2: ^2.1.4 - http: '>=0.13.6 <1.0.1' + flutter_web_auth_2: ^3.0.0 + http: '>=0.13.6 <2.0.0' package_info_plus: ^4.0.2 path_provider: ^2.0.15 web_socket_channel: ^2.4.0 @@ -32,4 +32,4 @@ dev_dependencies: flutter_lints: ^2.0.1 flutter_test: sdk: flutter - mockito: ^5.4.0 \ No newline at end of file + mockito: ^5.4.0 diff --git a/templates/go/base/params.twig b/templates/go/base/params.twig index cc62c584b..34cbd8b51 100644 --- a/templates/go/base/params.twig +++ b/templates/go/base/params.twig @@ -1,4 +1,4 @@ - params := map[string]interface{}{ + apiParams := map[string]interface{}{ {% for parameter in method.parameters.query %} "{{ parameter.name }}": {{ parameter.name | caseUcfirst }}, {% endfor %} @@ -7,7 +7,7 @@ {% endfor %} } - headers := map[string]interface{}{ + apiHeaders := map[string]interface{}{ {% for key, header in method.headers %} "{{ key }}": "{{ header }}", {% endfor %} diff --git a/templates/go/base/requests/api.twig b/templates/go/base/requests/api.twig index 034a83680..2b0ec67bb 100644 --- a/templates/go/base/requests/api.twig +++ b/templates/go/base/requests/api.twig @@ -1 +1 @@ - return srv.client.Call("{{ method.method | caseUpper }}", path, headers, params) \ No newline at end of file + return srv.client.Call("{{ method.method | caseUpper }}", apiPath, apiHeaders, apiParams) \ No newline at end of file diff --git a/templates/go/services/service.go.twig b/templates/go/services/service.go.twig index 2ee676ef0..33a91dc86 100644 --- a/templates/go/services/service.go.twig +++ b/templates/go/services/service.go.twig @@ -29,10 +29,10 @@ func New{{ service.name | caseUcfirst }}(clt Client) *{{ service.name | caseUcfi func (srv *{{ service.name | caseUcfirst }}) {{ method.name | caseUcfirst }}({% if method.parameters.all|length > 0 %}{% for parameter in method.parameters.all %}{{ parameter.name | caseUcfirst }} {{ parameter | typeName }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}) (*ClientResponse, error) { {% if method.parameters.path|length > 0 %} r := strings.NewReplacer({% for parameter in method.parameters.path %}"{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}", {{ parameter.name | caseUcfirst }}{% if not loop.last %}, {% endif %}{% endfor %}) - path := r.Replace("{{ method.path }}") + apiPath := r.Replace("{{ method.path }}") {% else %} - path := "{{ method.path }}" + apiPath := "{{ method.path }}" {% endif %} {{include('go/base/params.twig')}} diff --git a/templates/kotlin/base/requests/api.twig b/templates/kotlin/base/requests/api.twig index a823ba506..2b00f48a9 100644 --- a/templates/kotlin/base/requests/api.twig +++ b/templates/kotlin/base/requests/api.twig @@ -1,8 +1,8 @@ return client.call( "{{ method.method | caseUpper }}", - path, - headers, - params, + apiPath, + apiHeaders, + apiParams, {%~ if method.responseModel | hasGenericType(spec) %} responseType = classOf(), {%~ else %} diff --git a/templates/kotlin/base/requests/file.twig b/templates/kotlin/base/requests/file.twig index 4da9ef84d..a1b7c0f85 100644 --- a/templates/kotlin/base/requests/file.twig +++ b/templates/kotlin/base/requests/file.twig @@ -6,9 +6,9 @@ {%~ endif %} {%~ endfor %} return client.chunkedUpload( - path, - headers, - params, + apiPath, + apiHeaders, + apiParams, responseType = {{ method | returnType(spec, sdk.namespace | caseDot) | raw }}::class.java, {%~ if method.responseModel %} converter, diff --git a/templates/kotlin/base/requests/location.twig b/templates/kotlin/base/requests/location.twig index df3f67171..e8d5de57f 100644 --- a/templates/kotlin/base/requests/location.twig +++ b/templates/kotlin/base/requests/location.twig @@ -1,6 +1,6 @@ return client.call( "{{ method.method | caseUpper }}", - path, - params = params, + apiPath, + params = apiParams, responseType = {{ method | returnType(spec, sdk.namespace | caseDot) | raw }}::class.java ) \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index 3c12978d5..7181175ca 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -333,7 +333,7 @@ class Client @JvmOverloads constructor( responseType = Map::class.java, ) val chunksUploaded = current["chunksUploaded"] as Long - offset = (chunksUploaded * CHUNK_SIZE).coerceAtMost(size) + offset = chunksUploaded * CHUNK_SIZE } while (offset < size) { @@ -344,7 +344,7 @@ class Client @JvmOverloads constructor( } "bytes" -> { val end = if (offset + CHUNK_SIZE < size) { - offset + CHUNK_SIZE + offset + CHUNK_SIZE - 1 } else { size - 1 } @@ -364,7 +364,7 @@ class Client @JvmOverloads constructor( ) headers["Content-Range"] = - "bytes $offset-${((offset + CHUNK_SIZE) - 1).coerceAtMost(size)}/$size" + "bytes $offset-${((offset + CHUNK_SIZE) - 1).coerceAtMost(size - 1)}/$size" result = call( method = "POST", diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig index e9c5a9c88..7f4fbcd81 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig @@ -20,11 +20,11 @@ class Query { fun isNotNull(attribute: String) = "isNotNull(\"${attribute}\")" - fun between(attribute: String, start: Int, end: Int) = Query.addQuery(attribute, "between", listOf(start, end)) + fun between(attribute: String, start: Int, end: Int) = "between(\"${attribute}\", ${start}, ${end})" - fun between(attribute: String, start: Double, end: Double) = Query.addQuery(attribute, "between", listOf(start, end)) + fun between(attribute: String, start: Double, end: Double) = "between(\"${attribute}\", ${start}, ${end})" - fun between(attribute: String, start: String, end: String) = Query.addQuery(attribute, "between", listOf(start, end)) + fun between(attribute: String, start: String, end: String) = "between(\"${attribute}\", \"${start}\", \"${end}\")" fun startsWith(attribute: String, value: String) = Query.addQuery(attribute, "startsWith", value) diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Role.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Role.kt.twig index d5f59c3a6..2e4de9861 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Role.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Role.kt.twig @@ -1,29 +1,72 @@ package {{ sdk.namespace | caseDot }} +/** + * Helper class to generate role strings for [Permission]. + */ class Role { companion object { + + /** + * Grants access to anyone. + * + * This includes authenticated and unauthenticated users. + */ fun any(): String = "any" + /** + * Grants access to a specific user by user ID. + * + * You can optionally pass verified or unverified for + * [status] to target specific types of users. + */ fun user(id: String, status: String = ""): String = if(status.isEmpty()) { "user:$id" } else { "user:$id/$status" } + /** + * Grants access to any authenticated or anonymous user. + * + * You can optionally pass verified or unverified for + * [status] to target specific types of users. + */ fun users(status: String = ""): String = if(status.isEmpty()) { "users" } else { "users/$status" } + /** + * Grants access to any guest user without a session. + * + * Authenticated users don't have access to this role. + */ fun guests(): String = "guests" + /** + * Grants access to a team by team ID. + * + * You can optionally pass a role for [role] to target + * team members with the specified role. + */ fun team(id: String, role: String = ""): String = if(role.isEmpty()) { "team:$id" } else { "team:$id/$role" } + /** + * Grants access to a specific member of a team. + * + * When the member is removed from the team, they will + * no longer have access. + */ fun member(id: String): String = "member:$id" + + /** + * Grants access to a user with the specified label. + */ + fun label(name: String): String = "label:$name" } } \ No newline at end of file diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig index 682752960..5fb0f8069 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/services/ServiceTemplate.kt.twig @@ -49,12 +49,12 @@ class {{ service.name | caseUcfirst }} : Service { onProgress: ((UploadProgress) -> Unit)? = null {%~ endif %} ): {{ method | returnType(spec, sdk.namespace | caseDot) | raw }} { - val path = "{{ method.path }}" + val apiPath = "{{ method.path }}" {%~ for parameter in method.parameters.path %} .replace("{{ '{' ~ parameter.name | caseCamel ~ '}' }}", {{ parameter.name | caseCamel }}) {%~ endfor %} - val params = mutableMapOf( + val apiParams = mutableMapOf( {%~ for parameter in method.parameters.query | merge(method.parameters.body) %} "{{ parameter.name }}" to {{ parameter.name | caseCamel }}, {%~ endfor %} @@ -62,7 +62,7 @@ class {{ service.name | caseUcfirst }} : Service { {%~ if method.type == 'location' %} {{~ include('kotlin/base/requests/location.twig') }} {%~ else %} - val headers = mutableMapOf( + val apiHeaders = mutableMapOf( {%~ for key, header in method.headers %} "{{ key }}" to "{{ header }}", {%~ endfor %} diff --git a/templates/node/base/requests/api.twig b/templates/node/base/requests/api.twig index 37ece0471..4c578a889 100644 --- a/templates/node/base/requests/api.twig +++ b/templates/node/base/requests/api.twig @@ -1,4 +1,4 @@ - return await this.client.call('{{ method.method | caseLower }}', path, { + return await this.client.call('{{ method.method | caseLower }}', apiPath, { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} diff --git a/templates/node/base/requests/file.twig b/templates/node/base/requests/file.twig index 533dc5d5b..1f6f4a85b 100644 --- a/templates/node/base/requests/file.twig +++ b/templates/node/base/requests/file.twig @@ -2,7 +2,7 @@ {% if parameter.type == 'file' %} const size = {{ parameter.name | caseCamel | escapeKeyword }}.size; - const headers = { + const apiHeaders = { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} @@ -20,7 +20,7 @@ {% if parameter.isUploadID %} if({{ parameter.name | caseCamel | escapeKeyword }} != 'unique()') { try { - response = await this.client.call('get', path + '/' + {{ parameter.name }}, headers); + response = await this.client.call('get', apiPath + '/' + {{ parameter.name }}, apiHeaders); chunksUploaded = response.chunksUploaded; } catch(e) { } @@ -40,20 +40,24 @@ } const start = currentChunkStart; - const end = Math.min(((start + client.CHUNK_SIZE) - 1), size); + const end = currentChunkStart + currentChunkSize - 1; if(!lastUpload || currentChunkStart !== 0) { - headers['content-range'] = 'bytes ' + start + '-' + end + '/' + size; + apiHeaders['content-range'] = 'bytes ' + start + '-' + end + '/' + size; } if (id) { - headers['x-{{spec.title | caseLower }}-id'] = id; + apiHeaders['x-{{spec.title | caseLower }}-id'] = id; } - const stream = Stream.Readable.from(currentChunk); - payload['{{ parameter.name }}'] = { type: 'file', file: stream, filename: {{ parameter.name }}.filename }; + payload['{{ parameter.name }}'] = { + type: 'file', + file: currentChunk, + filename: {{ parameter.name }}.filename, + size: currentChunkSize + }; - response = await selfClient.call('{{ method.method | caseLower }}', path, headers, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); + response = await selfClient.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); if (!id) { id = response['$id']; @@ -92,6 +96,7 @@ if(chunkSize + currentChunkSize == client.CHUNK_SIZE) { // Upload chunk currentChunk = Buffer.concat([currentChunk, chunk]); + currentChunkSize = Buffer.byteLength(currentChunk); await uploadChunk(); currentChunk = Buffer.from(''); currentChunkSize = 0; diff --git a/templates/node/index.d.ts.twig b/templates/node/index.d.ts.twig index adae9cf5d..721eae574 100644 --- a/templates/node/index.d.ts.twig +++ b/templates/node/index.d.ts.twig @@ -208,6 +208,7 @@ declare module "{{ language.params.npmPackage|caseDash }}" { static guests(): string; static team(id: string, role?: string): string; static member(id: string): string; + static label(name: string): string; } {% for service in spec.services %} diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index b2538e8e8..c3b8130e5 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -1,5 +1,6 @@ const os = require('os'); const URL = require('url').URL; +const https = require("https"); const axios = require('axios'); const FormData = require('form-data'); const {{spec.title | caseUcfirst}}Exception = require('./exception.js'); @@ -81,11 +82,6 @@ class Client { } async call(method, path = '', headers = {}, params = {}, responseType = 'json') { - if(this.selfSigned) { // Allow self signed requests - process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; - } - - headers = Object.assign({}, this.headers, headers); let contentType = headers['content-type'].toLowerCase(); @@ -125,6 +121,10 @@ class Client { json: (contentType.startsWith('application/json')), responseType: responseType }; + if (this.selfSigned) { + // Allow self signed requests + options.httpsAgent = new https.Agent({ rejectUnauthorized: false }); + } try { let response = await axios(options); return response.data; diff --git a/templates/node/lib/query.js.twig b/templates/node/lib/query.js.twig index 06e0b389d..675b02b49 100644 --- a/templates/node/lib/query.js.twig +++ b/templates/node/lib/query.js.twig @@ -24,7 +24,7 @@ class Query { `isNotNull("${attribute}")`; static between = (attribute, start, end) => - Query.addQuery(attribute, "between", [start, end]); + `between("${attribute}", ${Query.parseValues(start)}, ${Query.parseValues(end)})` static startsWith = (attribute, value) => Query.addQuery(attribute, "startsWith", value); diff --git a/templates/node/lib/role.js.twig b/templates/node/lib/role.js.twig index 5c168c239..41b3cb5c4 100644 --- a/templates/node/lib/role.js.twig +++ b/templates/node/lib/role.js.twig @@ -1,31 +1,102 @@ +/** + * Helper class to generate role strings for `Permission`. + */ class Role { + + /** + * Grants access to anyone. + * + * This includes authenticated and unauthenticated users. + * + * @returns {string} + */ static any = () => { return 'any' } + + /** + * Grants access to a specific user by user ID. + * + * You can optionally pass verified or unverified for + * `status` to target specific types of users. + * + * @param {string} id + * @param {string} status + * @returns {string} + */ static user = (id, status = '') => { - if(status === '') { + if (status === '') { return `user:${id}` } return `user:${id}/${status}` } + + /** + * Grants access to any authenticated or anonymous user. + * + * You can optionally pass verified or unverified for + * `status` to target specific types of users. + * + * @param {string} status + * @returns {string} + */ static users = (status = '') => { - if(status === '') { + if (status === '') { return 'users' } return `users/${status}` } + + /** + * Grants access to any guest user without a session. + * + * Authenticated users don't have access to this role. + * + * @returns {string} + */ static guests = () => { return 'guests' } + + /** + * Grants access to a team by team ID. + * + * You can optionally pass a role for `role` to target + * team members with the specified role. + * + * @param {string} id + * @param {string} role + * @returns {string} + */ static team = (id, role = '') => { - if(role === '') { + if (role === '') { return 'team:' + id } return 'team:' + id + '/' + role } + + /** + * Grants access to a specific member of a team. + * + * When the member is removed from the team, they will + * no longer have access. + * + * @param {string} id + * @returns {string} + */ static member = (id) => { return 'member:' + id } + + /** + * Grants access to a user with the specified label. + * + * @param {string} name + * @returns {string} + */ + static label = (name) => { + return 'label:' + name; + } } module.exports = Role; \ No newline at end of file diff --git a/templates/node/lib/services/service.js.twig b/templates/node/lib/services/service.js.twig index 5cc919ab3..acfa340a3 100644 --- a/templates/node/lib/services/service.js.twig +++ b/templates/node/lib/services/service.js.twig @@ -29,7 +29,7 @@ class {{ service.name | caseUcfirst }} extends Service { * @returns {Promise} */ async {{ method.name | caseCamel }}({% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress = () => {}{% endif %}) { - let path = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; + const apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; {{ include ('node/base/params.twig')}} {% if 'multipart/form-data' in method.consumes %} {{ include ('node/base/requests/file.twig')}} diff --git a/templates/node/package.json.twig b/templates/node/package.json.twig index fa7719023..6a8eae40c 100644 --- a/templates/node/package.json.twig +++ b/templates/node/package.json.twig @@ -14,7 +14,7 @@ "@types/node": "^18.16.1" }, "dependencies": { - "axios": "^1.3.6", + "axios": "^1.4.0", "form-data": "^4.0.0" } } diff --git a/templates/php/base/params.twig b/templates/php/base/params.twig index 3e0b08733..8557222a4 100644 --- a/templates/php/base/params.twig +++ b/templates/php/base/params.twig @@ -1,4 +1,4 @@ - $params = []; + $apiParams = []; {% if method.parameters.all | length %} {% for parameter in method.parameters.all %} {% if parameter.required and not parameter.nullable %} @@ -9,19 +9,19 @@ {% endfor %} {% for parameter in method.parameters.query %} if (!is_null(${{ parameter.name | caseCamel | escapeKeyword }})) { - $params['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; + $apiParams['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; } {% endfor %} {% for parameter in method.parameters.body %} if (!is_null(${{ parameter.name | caseCamel | escapeKeyword }})) { - $params['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; + $apiParams['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; } {% endfor %} {% for parameter in method.parameters.formData %} if (!is_null(${{ parameter.name | caseCamel | escapeKeyword }})) { - $params['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; + $apiParams['{{ parameter.name }}'] = ${{ parameter.name | caseCamel | escapeKeyword }}; } {% endfor %} {% endif %} \ No newline at end of file diff --git a/templates/php/base/requests/api.twig b/templates/php/base/requests/api.twig index 72aee3648..b22cf8ca8 100644 --- a/templates/php/base/requests/api.twig +++ b/templates/php/base/requests/api.twig @@ -1,8 +1,8 @@ - return $this->client->call(Client::METHOD_{{ method.method | caseUpper }}, $path, [ + return $this->client->call(Client::METHOD_{{ method.method | caseUpper }}, $apiPath, [ {% for parameter in method.parameters.header %} '{{ parameter.name }}' => ${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} {% for key, header in method.headers %} '{{ key }}' => '{{ header }}', {% endfor %} - ], $params); \ No newline at end of file + ], $apiParams); \ No newline at end of file diff --git a/templates/php/base/requests/file.twig b/templates/php/base/requests/file.twig index 7f9b2d5dc..e80c793da 100644 --- a/templates/php/base/requests/file.twig +++ b/templates/php/base/requests/file.twig @@ -8,15 +8,15 @@ $mimeType = ${{ parameter.name | caseCamel }}->getMimeType(); $postedName = ${{ parameter.name | caseCamel }}->getFilename(); if ($size <= Client::CHUNK_SIZE) { - $params['{{ parameter.name | caseCamel }}'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(${{ parameter.name | caseCamel }}->getData()), $mimeType, $postedName); - return $this->client->call(Client::METHOD_POST, $path, [ + $apiParams['{{ parameter.name | caseCamel }}'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(${{ parameter.name | caseCamel }}->getData()), $mimeType, $postedName); + return $this->client->call(Client::METHOD_POST, $apiPath, [ {% for param in method.parameters.header %} '{{ param.name }}' => ${{ param.name | caseCamel }}, {% endfor %} {% for key, header in method.headers %} '{{ key }}' => '{{ header }}', {% endfor %} - ], $params); + ], $apiParams); } } else { $size = filesize(${{ parameter.name | caseCamel }}->getPath()); @@ -24,15 +24,15 @@ $postedName = ${{ parameter.name | caseCamel }}->getFilename() ?? basename(${{ parameter.name | caseCamel }}->getPath()); //send single file if size is less than or equal to 5MB if ($size <= Client::CHUNK_SIZE) { - $params['{{ parameter.name }}'] = new \CURLFile(${{ parameter.name | caseCamel }}->getPath(), $mimeType, $postedName); - return $this->client->call(Client::METHOD_{{ method.method | caseUpper }}, $path, [ + $apiParams['{{ parameter.name }}'] = new \CURLFile(${{ parameter.name | caseCamel }}->getPath(), $mimeType, $postedName); + return $this->client->call(Client::METHOD_{{ method.method | caseUpper }}, $apiPath, [ {% for param in method.parameters.header %} '{{ param.name }}' => ${{ param.name | caseCamel }}, {% endfor %} {% for key, header in method.headers %} '{{ key }}' => '{{ header }}', {% endfor %} - ], $params); + ], $apiParams); } } @@ -43,7 +43,7 @@ {% if parameter.isUploadID %} if(${{ parameter.name | caseCamel | escapeKeyword }} != 'unique()') { try { - $response = $this->client->call(Client::METHOD_GET, $path . '/' . ${{ parameter.name }}); + $response = $this->client->call(Client::METHOD_GET, $apiPath . '/' . ${{ parameter.name }}); $counter = $response['chunksUploaded'] ?? 0; } catch(\Exception $e) { } @@ -51,7 +51,7 @@ {% endif %} {% endfor %} - $headers = ['content-type' => 'multipart/form-data']; + $apiHeaders = ['content-type' => 'multipart/form-data']; $handle = null; if(!empty(${{parameter.name}}->getPath())) { @@ -67,12 +67,12 @@ } else { $chunk = substr($file->getData(), $start, Client::CHUNK_SIZE); } - $params['{{ parameter.name }}'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); - $headers['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size) . '/' . $size; + $apiParams['{{ parameter.name }}'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); + $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; if(!empty($id)) { - $headers['x-{{spec.title | caseLower }}-id'] = $id; + $apiHeaders['x-{{spec.title | caseLower }}-id'] = $id; } - $response = $this->client->call(Client::METHOD_POST, $path, $headers, $params); + $response = $this->client->call(Client::METHOD_POST, $apiPath, $apiHeaders, $apiParams); $counter++; $start += Client::CHUNK_SIZE; if(empty($id)) { @@ -81,7 +81,7 @@ if($onProgress !== null) { $onProgress([ '$id' => $response['$id'], - 'progress' => min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size) / $size * 100, + 'progress' => min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE)), $size) / $size * 100, 'sizeUploaded' => min($counter * Client::CHUNK_SIZE), 'chunksTotal' => $response['chunksTotal'], 'chunksUploaded' => $response['chunksUploaded'], diff --git a/templates/php/composer.json.twig b/templates/php/composer.json.twig index bf7288abe..d02b22973 100644 --- a/templates/php/composer.json.twig +++ b/templates/php/composer.json.twig @@ -18,7 +18,8 @@ "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "3.7.35" + "phpunit/phpunit": "^10", + "mockery/mockery": "^1.6.6" }, "minimum-stability": "dev" } \ No newline at end of file diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index 5f4d515fd..e249e8fcc 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -120,7 +120,10 @@ class Query */ public static function between(string $attribute, $start, $end): string { - return self::addQuery($attribute, 'between', [$start, $end]); + $start = self::parseValues($start); + $end = self::parseValues($end); + + return "between(\"{$attribute}\", {$start}, {$end})"; } /** diff --git a/templates/php/src/Role.php.twig b/templates/php/src/Role.php.twig index 87cf43a1d..a412ce97e 100644 --- a/templates/php/src/Role.php.twig +++ b/templates/php/src/Role.php.twig @@ -2,12 +2,33 @@ namespace {{ spec.title | caseUcfirst }}; +/** + * Helper class to generate role strings for Permission. + */ class Role { + /** + * Grants access to anyone. + * + * This includes authenticated and unauthenticated users. + * + * @return string + */ public static function any(): string { return 'any'; } + + /** + * Grants access to a specific user by user ID. + * + * You can optionally pass verified or unverified for + * `status` to target specific types of users. + * + * @param string $id + * @param string $status + * @return string + */ public static function user(string $id, string $status = ""): string { if(empty($status)) { @@ -15,6 +36,16 @@ class Role } return "user:$id/$status"; } + + /** + * Grants access to any authenticated or anonymous user. + * + * You can optionally pass verified or unverified for + * `status` to target specific types of users. + * + * @param string $status + * @return string + */ public static function users(string $status = ""): string { if(empty($status)) { @@ -22,10 +53,29 @@ class Role } return "users/$status"; } + + /** + * Grants access to any guest user without a session. + * + * Authenticated users don't have access to this role. + * + * @return string + */ public static function guests(): string { return 'guests'; } + + /** + * Grants access to a team by team ID. + * + * You can optionally pass a role for `role` to target + * team members with the specified role. + * + * @param string $id + * @param string $role + * @return string + */ public static function team(string $id, string $role = ""): string { if(empty($role)) { @@ -33,8 +83,29 @@ class Role } return "team:$id/$role"; } + + /** + * Grants access to a specific member of a team. + * + * When the member is removed from the team, they will + * no longer have access. + * + * @param string $id + * @return string + */ public static function member(string $id): string { return "member:$id"; } + + /** + * Grants access to a user with the specified label. + * + * @param string $name + * @return string + */ + public static function label(string $name): string + { + return "label:$name"; + } } \ No newline at end of file diff --git a/templates/php/src/Services/Service.php.twig b/templates/php/src/Services/Service.php.twig index 3e110cffe..73b9c42c8 100644 --- a/templates/php/src/Services/Service.php.twig +++ b/templates/php/src/Services/Service.php.twig @@ -31,7 +31,7 @@ class {{ service.name | caseUcfirst }} extends Service */ public function {{ method.name | caseCamel }}({% for parameter in method.parameters.all %}{{ parameter | typeName }}${{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required %} = null{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, callable $onProgress = null{% endif %}): {{ method | getReturn }} { - $path = str_replace([{% for parameter in method.parameters.path %}'{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}'{% if not loop.last %}, {% endif %}{% endfor %}], [{% for parameter in method.parameters.path %}${{ parameter.name | caseCamel | escapeKeyword }}{% if not loop.last %}, {% endif %}{% endfor %}], '{{ method.path }}'); + $apiPath = str_replace([{% for parameter in method.parameters.path %}'{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}'{% if not loop.last %}, {% endif %}{% endfor %}], [{% for parameter in method.parameters.path %}${{ parameter.name | caseCamel | escapeKeyword }}{% if not loop.last %}, {% endif %}{% endfor %}], '{{ method.path }}'); {{ include('php/base/params.twig') }} {% if 'multipart/form-data' in method.consumes %} diff --git a/templates/php/tests/IDTest.php.twig b/templates/php/tests/IDTest.php.twig new file mode 100644 index 000000000..a48a6b5d8 --- /dev/null +++ b/templates/php/tests/IDTest.php.twig @@ -0,0 +1,15 @@ +assertSame('unique()', ID::unique()); + } + + public function testCustom(): void { + $this->assertSame('custom', ID::custom('custom')); + } +} diff --git a/templates/php/tests/PermissionTest.php.twig b/templates/php/tests/PermissionTest.php.twig new file mode 100644 index 000000000..cf4078407 --- /dev/null +++ b/templates/php/tests/PermissionTest.php.twig @@ -0,0 +1,27 @@ +assertSame('read("any")', Permission::read(Role::any())); + } + + public function testWrite(): void { + $this->assertSame('write("any")', Permission::write(Role::any())); + } + + public function testCreate(): void { + $this->assertSame('create("any")', Permission::create(Role::any())); + } + + public function testUpdate(): void { + $this->assertSame('update("any")', Permission::update(Role::any())); + } + + public function testDelete(): void { + $this->assertSame('delete("any")', Permission::delete(Role::any())); + } +} diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig new file mode 100644 index 000000000..900c7e429 --- /dev/null +++ b/templates/php/tests/QueryTest.php.twig @@ -0,0 +1,149 @@ +description = $description; + $this->value = $value; + $this->expectedValues = $expectedValues; + } +} + +final class QueryTest extends TestCase { + /** + * @var BasicFilterQueryTest[] $tests + */ + private $tests; + + function __construct(string $name) + { + parent::__construct($name); + $this->tests = array( + new BasicFilterQueryTest('with a string', 's', '["s"]'), + new BasicFilterQueryTest('with a integer', 1, '[1]'), + new BasicFilterQueryTest('with a double', 1.2, '[1.2]'), + new BasicFilterQueryTest('with a whole number double', 1.0, '[1]'), + new BasicFilterQueryTest('with a bool', false, '[false]'), + new BasicFilterQueryTest('with a list', ['a', 'b', 'c'], '["a","b","c"]'), + ); + } + + public function testBasicFilterEqual(): void { + foreach($this->tests as $test) { + $this->assertSame( + "equal(\"attr\", $test->expectedValues)", + Query::equal('attr', $test->value), + $test->description, + ); + } + } + + public function testBasicFilterNotEqual(): void { + foreach($this->tests as $test) { + $this->assertSame( + "notEqual(\"attr\", $test->expectedValues)", + Query::notEqual('attr', $test->value), + $test->description, + ); + } + } + + public function testBasicFilterLessThan(): void { + foreach($this->tests as $test) { + $this->assertSame( + "lessThan(\"attr\", $test->expectedValues)", + Query::lessThan('attr', $test->value), + $test->description, + ); + } + } + + public function testBasicFilterLessThanEqual(): void { + foreach($this->tests as $test) { + $this->assertSame( + "lessThanEqual(\"attr\", $test->expectedValues)", + Query::lessThanEqual('attr', $test->value), + $test->description, + ); + } + } + + public function testBasicFilterGreaterThan(): void { + foreach($this->tests as $test) { + $this->assertSame( + "greaterThan(\"attr\", $test->expectedValues)", + Query::greaterThan('attr', $test->value), + $test->description, + ); + } + } + + public function testBasicFilterGreaterThanEqual(): void { + foreach($this->tests as $test) { + $this->assertSame( + "greaterThanEqual(\"attr\", $test->expectedValues)", + Query::greaterThanEqual('attr', $test->value), + $test->description, + ); + } + } + + public function testSearch(): void { + $this->assertSame('search("attr", ["keyword1 keyword2"])', Query::search('attr', 'keyword1 keyword2')); + } + + public function testIsNull(): void { + $this->assertSame('isNull("attr")', Query::isNull('attr')); + } + + public function testIsNotNull(): void { + $this->assertSame('isNotNull("attr")', Query::isNotNull('attr')); + } + + public function testBetweenWithIntegers(): void { + $this->assertSame('between("attr", 1, 2)', Query::between('attr', 1, 2)); + } + + public function testBetweenWithDoubles(): void { + $this->assertSame('between("attr", 1, 2)', Query::between('attr', 1.0, 2.0)); + } + + public function testBetweenWithStrings(): void { + $this->assertSame('between("attr", "a", "z")', Query::between('attr', 'a', 'z')); + } + + public function testSelect(): void { + $this->assertSame('select(["attr1","attr2"])', Query::select(['attr1', 'attr2'])); + } + + public function testOrderAsc(): void { + $this->assertSame('orderAsc("attr")', Query::orderAsc('attr')); + } + + public function testOrderDesc(): void { + $this->assertSame('orderDesc("attr")', Query::orderDesc('attr')); + } + + public function testCursorBefore(): void { + $this->assertSame('cursorBefore("attr")', Query::cursorBefore('attr')); + } + + public function testCursorAfter(): void { + $this->assertSame('cursorAfter("attr")', Query::cursorAfter('attr')); + } + + public function testLimit(): void { + $this->assertSame('limit(1)', Query::limit(1)); + } + + public function testOffset(): void { + $this->assertSame('offset(1)', Query::offset(1)); + } +} diff --git a/templates/php/tests/RoleTest.php.twig b/templates/php/tests/RoleTest.php.twig new file mode 100644 index 000000000..9a800d54d --- /dev/null +++ b/templates/php/tests/RoleTest.php.twig @@ -0,0 +1,47 @@ +assertSame('any', Role::any()); + } + + public function testUserWithoutStatus(): void { + $this->assertSame('user:custom', Role::user('custom')); + } + + public function testUserWithStatus(): void { + $this->assertSame('user:custom/verified', Role::user('custom', 'verified')); + } + + public function testUsersWithoutStatus(): void { + $this->assertSame('users', Role::users()); + } + + public function testUsersWithStatus(): void { + $this->assertSame('users/verified', Role::users('verified')); + } + + public function testGuests(): void { + $this->assertSame('guests', Role::guests()); + } + + public function testTeamWithoutRole(): void { + $this->assertSame('team:custom', Role::team('custom')); + } + + public function testTeamWithRole(): void { + $this->assertSame('team:custom/owner', Role::team('custom', 'owner')); + } + + public function testMember(): void { + $this->assertSame('member:custom', Role::member('custom')); + } + + public function testLabel(): void { + $this->assertSame('label:admin', Role::label('admin')); + } +} diff --git a/templates/php/tests/Services/ServiceTest.php.twig b/templates/php/tests/Services/ServiceTest.php.twig new file mode 100644 index 000000000..6e31e9bf3 --- /dev/null +++ b/templates/php/tests/Services/ServiceTest.php.twig @@ -0,0 +1,44 @@ +client = Mockery::mock(Client::class); + $this->{{ service.name | caseCamel }} = new {{ service.name | caseUcfirst }}($this->client); + } + +{% for method in service.methods %} + public function testMethod{{method.name | caseUcfirst}}(): void { + {%~ if method.responseModel and method.responseModel != 'any' ~%} + $data = array( + {%- for definition in spec.definitions ~%}{%~ if definition.name == method.responseModel -%}{%~ for property in definition.properties | filter((param) => param.required) ~%} + "{{property.name | escapeDollarSign}}" => {% if property.type == 'object' %}array(){% elseif property.type == 'array' %}array(){% elseif property.type == 'string' %}"{{property.example | escapeDollarSign}}"{% elseif property.type == 'boolean' %}true{% else %}{{property.example}}{% endif %},{%~ endfor ~%}{% set break = true %}{%- else -%}{% set continue = true %}{%- endif -%}{%~ endfor -%} + ); + {%~ elseif (method.responseModel and method.responseModel == 'any') or method.type == 'webAuth' ~%} + $data = array(); + {%~ else ~%} + $data = ''; + {%~ endif ~%} + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + + $response = $this->{{service.name | caseCamel}}->{{method.name | caseCamel}}({%~ for parameter in method.parameters.all | filter((param) => param.required) ~%} + {% if parameter.type == 'object' %}array(){% elseif parameter.type == 'array' %}array(){% elseif parameter.type == 'file' %}InputFile::withData('', "image/png"){% elseif parameter.type == 'boolean' %}true{% elseif parameter.type == 'string' %}"{% if parameter.example is not empty %}{{parameter.example | escapeDollarSign}}{% endif %}"{% elseif parameter.type == 'integer' and parameter['x-example'] is empty %}1{% elseif parameter.type == 'number' and parameter['x-example'] is empty %}1.0{% else %}{{parameter.example}}{%~ endif ~%}{% if not loop.last %},{% endif %}{%~ endfor ~%} + ); + + $this->assertSame($data, $response); + } + +{% endfor %} +} diff --git a/templates/python/.travis.yml.twig b/templates/python/.travis.yml.twig index 712f791bb..bf9a9dffd 100644 --- a/templates/python/.travis.yml.twig +++ b/templates/python/.travis.yml.twig @@ -1,5 +1,7 @@ language: python +dist: bionic + python: - "3.8" diff --git a/templates/python/base/params.twig b/templates/python/base/params.twig index faf595f08..8a574ec96 100644 --- a/templates/python/base/params.twig +++ b/templates/python/base/params.twig @@ -1,31 +1,31 @@ - params = {} + api_params = {} {% if method.parameters.all | length %} {% for parameter in method.parameters.all %} -{% if parameter.required %} +{% if parameter.required and not parameter.nullable %} if {{ parameter.name | escapeKeyword | caseSnake }} is None: raise {{spec.title | caseUcfirst}}Exception('Missing required parameter: "{{ parameter.name | escapeKeyword | caseSnake }}"') {% endif %} {% endfor %} {% for parameter in method.parameters.path %} - path = path.replace('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | escapeKeyword | caseSnake }}) + api_path = api_path.replace('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | escapeKeyword | caseSnake }}) {% endfor %} {% for parameter in method.parameters.query %} - params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} + api_params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} {% endfor %} {% for parameter in method.parameters.body %} {% if method.consumes[0] == "multipart/form-data" and ( parameter.type != "string" and parameter.type != "array" ) %} - params['{{ parameter.name }}'] = str({{ parameter.name | escapeKeyword | caseSnake }}).lower() if type({{ parameter.name | escapeKeyword | caseSnake }}) is bool else {{ parameter.name | escapeKeyword | caseSnake }} + api_params['{{ parameter.name }}'] = str({{ parameter.name | escapeKeyword | caseSnake }}).lower() if type({{ parameter.name | escapeKeyword | caseSnake }}) is bool else {{ parameter.name | escapeKeyword | caseSnake }} {% else %} - params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} + api_params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} {% endif %} {% endfor %} {% for parameter in method.parameters.formData %} {% if method.consumes[0] == "multipart/form-data" and ( parameter.type != "string" and parameter.type != "array" ) %} - params['{{ parameter.name }}'] = str({{ parameter.name | escapeKeyword | caseSnake }}).lower() if type({{ parameter.name | escapeKeyword | caseSnake }}) is bool else {{ parameter.name | escapeKeyword | caseSnake }} + api_params['{{ parameter.name }}'] = str({{ parameter.name | escapeKeyword | caseSnake }}).lower() if type({{ parameter.name | escapeKeyword | caseSnake }}) is bool else {{ parameter.name | escapeKeyword | caseSnake }} {% else %} - params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} + api_params['{{ parameter.name }}'] = {{ parameter.name | escapeKeyword | caseSnake }} {% endif %} {% endfor %} {% endif %} \ No newline at end of file diff --git a/templates/python/base/requests/api.twig b/templates/python/base/requests/api.twig index defae2819..e305a8d9b 100644 --- a/templates/python/base/requests/api.twig +++ b/templates/python/base/requests/api.twig @@ -1,8 +1,8 @@ - return self.client.call('{{ method.method | caseLower }}', path, { + return self.client.call('{{ method.method | caseLower }}', api_path, { {% for parameter in method.parameters.header %} '{{ parameter.name }}': {{ parameter.name | escapeKeyword | caseSnake }}, {% endfor %} {% for key, header in method.headers %} '{{ key }}': '{{ header }}', {% endfor %} - }, params) \ No newline at end of file + }, api_params) \ No newline at end of file diff --git a/templates/python/base/requests/file.twig b/templates/python/base/requests/file.twig index 8a8d223ff..52b3cc691 100644 --- a/templates/python/base/requests/file.twig +++ b/templates/python/base/requests/file.twig @@ -12,11 +12,11 @@ {% endif %} {% endfor %} - return self.client.chunked_upload(path, { + return self.client.chunked_upload(api_path, { {% for parameter in method.parameters.header %} '{{ parameter.name }}': {{ parameter.name | escapeKeyword | caseSnake }}, {% endfor %} {% for key, header in method.headers %} '{{ key }}': '{{ header }}', {% endfor %} - }, params, param_name, on_progress, upload_id) \ No newline at end of file + }, api_params, param_name, on_progress, upload_id) \ No newline at end of file diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index 057693f1f..8fb6947e1 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -50,6 +50,8 @@ class Client: if params is None: params = {} + params = {k: v for k, v in params.items() if v is not None} # Remove None values from params dictionary + data = {} json = {} files = {} @@ -158,7 +160,7 @@ class Client: input_file.data = input[offset:end] params[param_name] = input_file - headers["content-range"] = f'bytes {offset}-{min((offset + self._chunk_size) - 1, size)}/{size}' + headers["content-range"] = f'bytes {offset}-{min((offset + self._chunk_size) - 1, size - 1)}/{size}' result = self.call( 'post', @@ -173,7 +175,7 @@ class Client: headers["x-{{ spec.title | caseLower }}-id"] = result["$id"] if on_progress is not None: - end = min((((counter * self._chunk_size) + self._chunk_size) - 1), size) + end = min((((counter * self._chunk_size) + self._chunk_size) - 1), size - 1) on_progress({ "$id": result["$id"], "progress": min(offset, size)/size * 100, diff --git a/templates/python/package/query.py.twig b/templates/python/package/query.py.twig index d92930477..65e8dc42e 100644 --- a/templates/python/package/query.py.twig +++ b/templates/python/package/query.py.twig @@ -33,7 +33,7 @@ class Query: @staticmethod def between(attribute, start, end) -> str: - return Query.add_query(attribute, "between", [start, end]) + return f'between("{attribute}", {Query.parseValues(start)}, {Query.parseValues(end)})' @staticmethod def starts_with(attribute, value) -> str: diff --git a/templates/python/package/role.py.twig b/templates/python/package/role.py.twig index ae3b7db50..3abf09c7a 100644 --- a/templates/python/package/role.py.twig +++ b/templates/python/package/role.py.twig @@ -1,30 +1,111 @@ class Role: + """Helper class to generate role strings for `Permission`.""" @staticmethod def any() -> str: + """Grants access to anyone. + + This includes authenticated and unauthenticated users. + """ return 'any' @staticmethod def user(id, status: str = "") -> str: + """Grants access to a specific user by user ID. + + You can optionally pass verified or unverified for + `status` to target specific types of users. + + Parameters + ---------- + id : str + status : str, optional + + Returns + ------- + str + """ if status: return f'user:{id}/{status}' return f'user:{id}' @staticmethod def users(status: str = "") -> str: + """Grants access to any authenticated or anonymous user. + + You can optionally pass verified or unverified for + `status` to target specific types of users. + + Parameters + ---------- + status : str, optional + + Returns + ------- + str + """ if status: return f'users/{status}' return 'users' @staticmethod def guests() -> str: + """Grants access to any guest user without a session. + + Authenticated users don't have access to this role. + + Returns + ------- + str + """ return 'guests' @staticmethod def team(id, role: str = "") -> str: + """Grants access to a team by team ID. + + You can optionally pass a role for `role` to target + team members with the specified role. + + Parameters + ---------- + id : str + role : str, optional + + Returns + ------- + str + """ if role: return f'team:{id}/{role}' return f'team:{id}' @staticmethod def member(id) -> str: + """Grants access to a specific member of a team. + + When the member is removed from the team, they will + no longer have access. + + Parameters + ---------- + id : str + + Returns + ------- + str + """ return f'member:{id}' + + @staticmethod + def label(name): + """Grants access to a user with the specified label. + + Parameters + ---------- + name : str + + Returns + ------- + str + """ + return f'label:{name}' diff --git a/templates/python/package/services/service.py.twig b/templates/python/package/services/service.py.twig index df665f003..4f617e437 100644 --- a/templates/python/package/services/service.py.twig +++ b/templates/python/package/services/service.py.twig @@ -12,7 +12,7 @@ class {{ service.name | caseUcfirst }}(Service): """{{ method.title }}""" {% endif %} - path = '{{ method.path }}' + api_path = '{{ method.path }}' {{ include('python/base/params.twig') }} {% if 'multipart/form-data' in method.consumes %} {{ include('python/base/requests/file.twig') }} diff --git a/templates/python/requirements.txt.twig b/templates/python/requirements.txt.twig index bf0d9d411..2c24336eb 100644 --- a/templates/python/requirements.txt.twig +++ b/templates/python/requirements.txt.twig @@ -1 +1 @@ -requests==2.28.2 \ No newline at end of file +requests==2.31.0 diff --git a/templates/python/setup.cfg.twig b/templates/python/setup.cfg.twig index 224a77957..0f94f377b 100644 --- a/templates/python/setup.cfg.twig +++ b/templates/python/setup.cfg.twig @@ -1,2 +1,2 @@ [metadata] -description-file = README.md \ No newline at end of file +description_file = README.md \ No newline at end of file diff --git a/templates/ruby/base/params.twig b/templates/ruby/base/params.twig index 8483a2bf4..8276ff6d4 100644 --- a/templates/ruby/base/params.twig +++ b/templates/ruby/base/params.twig @@ -1,4 +1,4 @@ - path = '{{ method.path }}' + api_path = '{{ method.path }}' {% for parameter in method.parameters.path %} .gsub('{{ '{' }}{{ parameter.name }}{{ '}' }}', {{ parameter.name | caseSnake | escapeKeyword }}) {% endfor %} @@ -11,13 +11,13 @@ {% endif %} {% endfor %} - params = { + api_params = { {% for parameter in method.parameters.query | merge(method.parameters.body) %} {{ parameter.name }}: {{ parameter.name | caseSnake | escapeKeyword }}, {% endfor %} } - headers = { + api_headers = { {% for parameter in method.parameters.header %} "{{ parameter.name }}": {{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} diff --git a/templates/ruby/base/requests/api.twig b/templates/ruby/base/requests/api.twig index 0ac82da49..312e078e8 100644 --- a/templates/ruby/base/requests/api.twig +++ b/templates/ruby/base/requests/api.twig @@ -1,8 +1,8 @@ @client.call( method: '{{ method.method | caseUpper }}', - path: path, - headers: headers, - params: params, + path: api_path, + headers: api_headers, + params: api_params, {% if method.responseModel and method.responseModel != 'any' %} response_type: Models::{{method.responseModel | caseUcfirst}} {% endif %} diff --git a/templates/ruby/base/requests/file.twig b/templates/ruby/base/requests/file.twig index f7dcb443e..fda9ca23c 100644 --- a/templates/ruby/base/requests/file.twig +++ b/templates/ruby/base/requests/file.twig @@ -7,9 +7,9 @@ {% endif %} {% endfor %} @client.chunked_upload( - path: path, - headers: headers, - params: params, + path: api_path, + headers: api_headers, + params: api_params, param_name: param_name, id_param_name: id_param_name, on_progress: on_progress, diff --git a/templates/ruby/lib/container/client.rb.twig b/templates/ruby/lib/container/client.rb.twig index fc0762dc9..4b4892889 100644 --- a/templates/ruby/lib/container/client.rb.twig +++ b/templates/ruby/lib/container/client.rb.twig @@ -141,7 +141,7 @@ module {{ spec.title | caseUcfirst }} params: {} ) chunks_uploaded = current['chunksUploaded'].to_i - offset = [size, (chunks_uploaded * @chunk_size)].min + offset = chunks_uploaded * @chunk_size end while offset < size @@ -158,7 +158,7 @@ module {{ spec.title | caseUcfirst }} mime_type: input_file.mime_type ) - headers['content-range'] = "bytes #{offset}-#{[offset + @chunk_size - 1, size].min}/#{size}" + headers['content-range'] = "bytes #{offset}-#{[offset + @chunk_size - 1, size - 1].min}/#{size}" result = call( method: 'POST', diff --git a/templates/ruby/lib/container/query.rb.twig b/templates/ruby/lib/container/query.rb.twig index 00806943f..e62ee4b92 100644 --- a/templates/ruby/lib/container/query.rb.twig +++ b/templates/ruby/lib/container/query.rb.twig @@ -34,7 +34,7 @@ module {{spec.title | caseUcfirst}} end def between(attribute, start, ending) - return add_query(attribute, "between", [start, ending]) + return "between(\"#{attribute}\", #{parse_values(start)}, #{parse_values(ending)})" end def starts_with(attribute, value) @@ -81,13 +81,13 @@ module {{spec.title | caseUcfirst}} def add_query(attribute, method, value) if value.is_a?(Array) - "#{method}(\"#{attribute}\", [#{value.map {|item| parseValues(item)}.join(',')}])" + "#{method}(\"#{attribute}\", [#{value.map {|item| parse_values(item)}.join(',')}])" else - return "#{method}(\"#{attribute}\", [#{parseValues(value)}])" + return "#{method}(\"#{attribute}\", [#{parse_values(value)}])" end end - def parseValues(value) + def parse_values(value) return value.is_a?(String) ? "\"#{value}\"" : value end end diff --git a/templates/ruby/lib/container/role.rb.twig b/templates/ruby/lib/container/role.rb.twig index 6333fbd64..c3cf9f23d 100644 --- a/templates/ruby/lib/container/role.rb.twig +++ b/templates/ruby/lib/container/role.rb.twig @@ -1,9 +1,26 @@ module {{spec.title | caseUcfirst}} + + # Helper class to generate role strings for `Permission`. class Role + + # Grants access to anyone. + # + # This includes authenticated and unauthenticated users. + # + # @return [String] def self.any 'any' end + # Grants access to a specific user by user ID. + # + # You can optionally pass verified or unverified for + # `status` to target specific types of users. + # + # @param [String] id + # @param [String] status + # + # @return [String] def self.user(id, status = "") if(status.empty?) "user:#{id}" @@ -12,6 +29,14 @@ module {{spec.title | caseUcfirst}} end end + # Grants access to any authenticated or anonymous user. + # + # You can optionally pass verified or unverified for + # `status` to target specific types of users. + # + # @param [String] status + # + # @return [String] def self.users(status = "") if(status.empty?) 'users' @@ -20,10 +45,24 @@ module {{spec.title | caseUcfirst}} end end + # Grants access to any guest user without a session. + # + # Authenticated users don't have access to this role. + # + # @return [String] def self.guests 'guests' end + # Grants access to a team by team ID. + # + # You can optionally pass a role for `role` to target + # team members with the specified role. + # + # @param [String] id + # @param [String] role + # + # @return [String] def self.team(id, role = "") if(role.empty?) "team:#{id}" @@ -32,8 +71,25 @@ module {{spec.title | caseUcfirst}} end end + # Grants access to a specific member of a team. + # + # When the member is removed from the team, they will + # no longer have access. + # + # @param [String] id + # + # @return [String] def self.member(id) "member:#{id}" end + + # Grants access to a user with the specified label. + # + # @param [String] name + # + # @return [String] + def self.label(name) + "label:#{name}" + end end end \ No newline at end of file diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index db36c208f..8be157d3a 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -301,18 +301,21 @@ open class Client { default: var message = "" var data = try await response.body.collect(upTo: Int.max) + var type = "" do { let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] message = dict?["message"] as? String ?? response.status.reasonPhrase + type = dict?["type"] as? String ?? "" } catch { message = data.readString(length: data.readableBytes)! } throw {{ spec.title | caseUcfirst }}Error( message: message, - code: Int(response.status.code) + code: Int(response.status.code), + type: type ) } } @@ -379,7 +382,7 @@ open class Client { converter: { return $0 as! [String: Any] } ) let chunksUploaded = map["chunksUploaded"] as! Int - offset = min(size, (chunksUploaded * Client.chunkSize)) + offset = chunksUploaded * Client.chunkSize } catch { // File does not exist yet, swallow exception } @@ -390,7 +393,7 @@ open class Client { ?? (input.data as! ByteBuffer).getSlice(at: offset, length: Int(size - offset)) params[paramName] = InputFile.fromBuffer(slice!, filename: input.filename, mimeType: input.mimeType) - headers["content-range"] = "bytes \(offset)-\(min((offset + Client.chunkSize) - 1, size))/\(size)" + headers["content-range"] = "bytes \(offset)-\(min((offset + Client.chunkSize) - 1, size - 1))/\(size)" result = try await call( method: "POST", diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index 9fa9893c7..ce1415d75 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -33,15 +33,15 @@ public class Query { } public static func between(_ attribute: String, start: Int, end: Int) -> String { - buildQueryWhere(attribute, is: "between", to: [start, end]) + "between(\"\(attribute)\", \(start), \(end))" } public static func between(_ attribute: String, start: Double, end: Double) -> String { - buildQueryWhere(attribute, is: "between", to: [start, end]) + "between(\"\(attribute)\", \(start), \(end))" } public static func between(_ attribute: String, start: String, end: String) -> String { - buildQueryWhere(attribute, is: "between", to: [start, end]) + "between(\"\(attribute)\", \"\(start)\", \"\(end)\")" } public static func startsWith(_ attribute: String, value: String) -> String { diff --git a/templates/swift/Sources/Role.swift.twig b/templates/swift/Sources/Role.swift.twig index b8acdbb32..1e8e755f5 100644 --- a/templates/swift/Sources/Role.swift.twig +++ b/templates/swift/Sources/Role.swift.twig @@ -1,8 +1,21 @@ +/// Helper class to generate role strings for `Permission`. public class Role { + + /// Grants access to anyone. + /// + /// This includes authenticated and unauthenticated users. public static func any() -> String { return "any" } + /// Grants access to a specific user by user ID. + /// + /// You can optionally pass verified or unverified for + /// `status` to target specific types of users. + /// + /// @param String id + /// @param String status + /// @return String public static func user(_ id: String, _ status: String = "") -> String { if(status.isEmpty) { return "user:\(id)" @@ -10,6 +23,13 @@ public class Role { return "user:\(id)/\(status)" } + /// Grants access to any authenticated or anonymous user. + /// + /// You can optionally pass verified or unverified for + /// `status` to target specific types of users. + /// + /// @param String status + /// @return String public static func users(_ status: String = "") -> String { if(status.isEmpty) { return "users" @@ -17,10 +37,23 @@ public class Role { return "users/\(status)" } + /// Grants access to any guest user without a session. + /// + /// Authenticated users don't have access to this role. + /// + /// @return String public static func guests() -> String { return "guests" } + /// Grants access to a team by team ID. + /// + /// You can optionally pass a role for `role` to target + /// team members with the specified role. + /// + /// @param String id + /// @param String role + /// @return String public static func team(_ id: String, _ role: String = "") -> String { if(role.isEmpty) { return "team:\(id)" @@ -28,7 +61,22 @@ public class Role { return "team:\(id)/\(role)" } + /// Grants access to a specific member of a team. + /// + /// When the member is removed from the team, they will + /// no longer have access. + /// + /// @param String id + /// @return String public static func member(_ id: String) -> String { return "member:\(id)" } + + /// Grants access to a user with the specified label. + /// + /// @param String name + /// @return String + public static func label(_ name: String) -> String { + return "label:\(name)" + } } \ No newline at end of file diff --git a/templates/swift/Sources/Services/Service.swift.twig b/templates/swift/Sources/Services/Service.swift.twig index 7c581f5db..aad435162 100644 --- a/templates/swift/Sources/Services/Service.swift.twig +++ b/templates/swift/Sources/Services/Service.swift.twig @@ -35,19 +35,19 @@ open class {{ service.name | caseUcfirst }}: Service { {%~ if 'multipart/form-data' in method.consumes %} onProgress: ((UploadProgress) -> Void)? = nil {%~ endif %} - ){%~ if method.type != "webAuth" %} async{% endif %} throws -> {{ method | returnType(spec) | raw }} { + ) async throws -> {{ method | returnType(spec) | raw }} { {{~ include('swift/base/params.twig') }} {%~ if method.type == 'webAuth' %} - {{~ include('swift/base/requests/OAuth.twig') }} + {{~ include('swift/base/requests/oauth.twig') }} {%~ elseif method.type == 'location' %} {{~ include('swift/base/requests/location.twig')}} {%~ else %} {%~ if method.headers | length <= 0 %} - let headers: [String: String] = [:] + let apiHeaders: [String: String] = [:] {%~ else %} {% if 'multipart/form-data' in method.consumes -%} var {%- else -%} let - {%- endif %} headers: [String: String] = [ + {%- endif %} apiHeaders: [String: String] = [ {%~ for key, header in method.headers %} "{{ key }}": "{{ header }}"{% if not loop.last %},{% endif %} diff --git a/templates/swift/base/params.twig b/templates/swift/base/params.twig index 4fe11e5aa..518090ea8 100644 --- a/templates/swift/base/params.twig +++ b/templates/swift/base/params.twig @@ -1,14 +1,14 @@ - let path: String = "{{ method.path }}" + let apiPath: String = "{{ method.path }}" {%~ for parameter in method.parameters.path %} .replacingOccurrences(of: "{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}", with: {{ parameter.name | caseCamel | escapeKeyword }}) {%~ endfor %} {%~ if method.parameters.query | merge(method.parameters.body) | length <= 0 %} - let params: [String: Any] = [:] + let apiParams: [String: Any] = [:] {%~ else %} {% if 'multipart/form-data' in method.consumes -%} var {%- else -%} let - {%- endif %} params: [String: Any?] = [ + {%- endif %} apiParams: [String: Any?] = [ {%~ for parameter in method.parameters.query | merge(method.parameters.body) %} "{{ parameter.name }}": {{ parameter.name | caseCamel | escapeKeyword }}{% if not loop.last or (method.type == 'location' or method.type == 'webAuth' and method.auth | length > 0) %},{% endif %} diff --git a/templates/swift/base/requests/OAuth.twig b/templates/swift/base/requests/OAuth.twig deleted file mode 100644 index a0d0b5c7a..000000000 --- a/templates/swift/base/requests/OAuth.twig +++ /dev/null @@ -1,12 +0,0 @@ - let query = "?\(client.parametersToQueryString(params: params))" - let url = URL(string: client.endPoint + path + query)! - let callbackScheme = "appwrite-callback-\(client.config["project"] ?? "")" - let group = DispatchGroup() - - group.enter() - WebAuthComponent.authenticate(url: url, callbackScheme: callbackScheme) { result in - group.leave() - } - group.wait() - - return true diff --git a/templates/swift/base/requests/api.twig b/templates/swift/base/requests/api.twig index 1798b60bc..e93264245 100644 --- a/templates/swift/base/requests/api.twig +++ b/templates/swift/base/requests/api.twig @@ -1,8 +1,8 @@ return try await client.call( method: "{{ method.method | caseUpper }}", - path: path, - headers: headers, - params: params{% if method.responseModel %}, + path: apiPath, + headers: apiHeaders, + params: apiParams{% if method.responseModel %}, converter: converter {% endif %} ) \ No newline at end of file diff --git a/templates/swift/base/requests/file.twig b/templates/swift/base/requests/file.twig index 594bbe38b..2167704ec 100644 --- a/templates/swift/base/requests/file.twig +++ b/templates/swift/base/requests/file.twig @@ -6,9 +6,9 @@ {% endif %} {% endfor %} return try await client.chunkedUpload( - path: path, - headers: &headers, - params: ¶ms, + path: apiPath, + headers: &apiHeaders, + params: &apiParams, paramName: paramName, idParamName: idParamName, {% if method.responseModel %} diff --git a/templates/swift/base/requests/location.twig b/templates/swift/base/requests/location.twig index a115d056d..78d4b1d1a 100644 --- a/templates/swift/base/requests/location.twig +++ b/templates/swift/base/requests/location.twig @@ -1,5 +1,5 @@ return try await client.call( method: "{{ method.method | caseUpper }}", - path: path, - params: params + path: apiPath, + params: apiParams ) \ No newline at end of file diff --git a/templates/swift/base/requests/oauth.twig b/templates/swift/base/requests/oauth.twig new file mode 100644 index 000000000..02174947a --- /dev/null +++ b/templates/swift/base/requests/oauth.twig @@ -0,0 +1,11 @@ + let query = "?\(client.parametersToQueryString(params: apiParams))" + let url = URL(string: client.endPoint + apiPath + query)! + let callbackScheme = "appwrite-callback-\(client.config["project"] ?? "")" + + try await withCheckedThrowingContinuation { continuation in + WebAuthComponent.authenticate(url: url, callbackScheme: callbackScheme) { result in + continuation.resume(with: result) + } + } + + return true diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index 51cbb7a9f..1f880cb1c 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -28,7 +28,7 @@ export class Query { `isNotNull("${attribute}")`; static between = (attribute: string, start: string|number, end: string|number): string => - `between("${attribute}", [${Query.parseValues(start)},${Query.parseValues(end)}])`; + `between("${attribute}", ${Query.parseValues(start)}, ${Query.parseValues(end)})`; static startsWith = (attribute: string, value: string): string => Query.addQuery(attribute, "startsWith", value); diff --git a/templates/web/src/role.ts.twig b/templates/web/src/role.ts.twig index d94e398c6..79f8c6b62 100644 --- a/templates/web/src/role.ts.twig +++ b/templates/web/src/role.ts.twig @@ -1,34 +1,100 @@ +/** + * Helper class to generate role strings for `Permission`. + */ export class Role { + + /** + * Grants access to anyone. + * + * This includes authenticated and unauthenticated users. + * + * @returns {string} + */ public static any(): string { return 'any' } + /** + * Grants access to a specific user by user ID. + * + * You can optionally pass verified or unverified for + * `status` to target specific types of users. + * + * @param {string} id + * @param {string} status + * @returns {string} + */ public static user(id: string, status: string = ''): string { - if(status === '') { + if (status === '') { return `user:${id}` } return `user:${id}/${status}` } - + + /** + * Grants access to any authenticated or anonymous user. + * + * You can optionally pass verified or unverified for + * `status` to target specific types of users. + * + * @param {string} status + * @returns {string} + */ public static users(status: string = ''): string { - if(status === '') { + if (status === '') { return 'users' } return `users/${status}` } - + + /** + * Grants access to any guest user without a session. + * + * Authenticated users don't have access to this role. + * + * @returns {string} + */ public static guests(): string { return 'guests' } - + + /** + * Grants access to a team by team ID. + * + * You can optionally pass a role for `role` to target + * team members with the specified role. + * + * @param {string} id + * @param {string} role + * @returns {string} + */ public static team(id: string, role: string = ''): string { - if(role === '') { + if (role === '') { return `team:${id}` } return `team:${id}/${role}` } + /** + * Grants access to a specific member of a team. + * + * When the member is removed from the team, they will + * no longer have access. + * + * @param {string} id + * @returns {string} + */ public static member(id: string): string { return `member:${id}` } + + /** + * Grants access to a user with the specified label. + * + * @param {string} name + * @returns {string} + */ + public static label(name: string): string { + return `label:${name}` + } } \ No newline at end of file diff --git a/templates/web/src/services/template.ts.twig b/templates/web/src/services/template.ts.twig index cd7ddb88a..585e5de60 100644 --- a/templates/web/src/services/template.ts.twig +++ b/templates/web/src/services/template.ts.twig @@ -34,8 +34,8 @@ export class {{ service.name | caseUcfirst }} extends Service { {% endif %} {% endfor %} - let path = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel | escapeKeyword }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; - let payload: Payload = {}; + const apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel | escapeKeyword }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; + const payload: Payload = {}; {% for parameter in method.parameters.query %} if (typeof {{ parameter.name | caseCamel | escapeKeyword }} !== 'undefined') { @@ -49,7 +49,7 @@ export class {{ service.name | caseUcfirst }} extends Service { } {% endfor %} - const uri = new URL(this.client.config.endpoint + path); + const uri = new URL(this.client.config.endpoint + apiPath); {% if method.type == 'location' or method.type == 'webAuth' %} {% if method.auth|length > 0 %} {% for node in method.auth %} @@ -85,7 +85,6 @@ export class {{ service.name | caseUcfirst }} extends Service { if (size <= Service.CHUNK_SIZE) { return await this.client.call('{{ method.method | caseLower }}', uri, { - {% for parameter in method.parameters.header %} '{{ parameter.name | caseCamel | escapeKeyword }}': this.client.${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} @@ -94,10 +93,8 @@ export class {{ service.name | caseUcfirst }} extends Service { {% endfor %} }, payload); } - let id = undefined; - let response = undefined; - const headers: { [header: string]: string } = { + const apiHeaders: { [header: string]: string } = { {% for parameter in method.parameters.header %} '{{ parameter.name | caseCamel | escapeKeyword }}': this.client.${{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %} @@ -106,50 +103,43 @@ export class {{ service.name | caseUcfirst }} extends Service { {% endfor %} } - let counter = 0; - const totalCounters = Math.ceil(size / Service.CHUNK_SIZE); + let offset = 0; + let response = undefined; {% for parameter in method.parameters.all %} {% if parameter.isUploadID %} if({{ parameter.name | caseCamel | escapeKeyword }} != 'unique()') { try { - response = await this.client.call('GET', new URL(this.client.config.endpoint + path + '/' + {{ parameter.name }}), headers); - counter = response.chunksUploaded; + response = await this.client.call('GET', new URL(this.client.config.endpoint + apiPath + '/' + {{ parameter.name }}), apiHeaders); + offset = response.chunksUploaded * Service.CHUNK_SIZE; } catch(e) { } } {% endif %} {% endfor %} - for (counter; counter < totalCounters; counter++) { - const start = (counter * Service.CHUNK_SIZE); - const end = Math.min((((counter * Service.CHUNK_SIZE) + Service.CHUNK_SIZE) - 1), size); - - headers['content-range'] = 'bytes ' + start + '-' + end + '/' + size + while (offset < size) { + let end = Math.min(offset + Service.CHUNK_SIZE - 1, size - 1); - if (id) { - headers['x-{{spec.title | caseLower }}-id'] = id; + apiHeaders['content-range'] = 'bytes ' + offset + '-' + end + '/' + size; + if (response && response.$id) { + apiHeaders['x-{{spec.title | caseLower }}-id'] = response.$id; } - const stream = {{ parameter.name | caseCamel | escapeKeyword }}.slice(start, end + 1); - payload['{{ parameter.name }}'] = new File([stream], {{ parameter.name | caseCamel | escapeKeyword }}.name); - - response = await this.client.call('{{ method.method | caseLower }}', uri, headers, payload); - - if (!id) { - id = response['$id']; - } + const chunk = {{ parameter.name | caseCamel | escapeKeyword }}.slice(offset, end + 1); + payload['{{ parameter.name }}'] = new File([chunk], {{ parameter.name | caseCamel | escapeKeyword }}.name); + response = await this.client.call('{{ method.method | caseLower }}', uri, apiHeaders, payload); if (onProgress) { onProgress({ $id: response.$id, - progress: Math.min((counter + 1) * Service.CHUNK_SIZE - 1, size) / size * 100, - sizeUploaded: end, + progress: (offset / size) * 100, + sizeUploaded: offset, chunksTotal: response.chunksTotal, chunksUploaded: response.chunksUploaded }); } + offset += Service.CHUNK_SIZE; } - return response; {% endif %} {% endfor %} diff --git a/tests/Android11Java11Test.php b/tests/Android11Java11Test.php index 3985cd3f2..9aa7a6c1a 100644 --- a/tests/Android11Java11Test.php +++ b/tests/Android11Java11Test.php @@ -17,7 +17,7 @@ class Android11Java11Test extends Base 'chmod +x tests/sdks/android/gradlew', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-30-jdk11 sh -c "./gradlew :library:testReleaseUnitTest -q && cat library/result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-30-jdk11 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, @@ -28,7 +28,7 @@ class Android11Java11Test extends Base ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, - ...Base::COOKIE_RESPONSES, + // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Android11Java8Test.php b/tests/Android11Java8Test.php index a73b040ad..e79656ee8 100644 --- a/tests/Android11Java8Test.php +++ b/tests/Android11Java8Test.php @@ -17,7 +17,7 @@ class Android11Java8Test extends Base 'chmod +x tests/sdks/android/gradlew', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-30-jdk8 sh -c "./gradlew :library:testReleaseUnitTest -q && cat library/result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-30-jdk8 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, @@ -28,7 +28,7 @@ class Android11Java8Test extends Base ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, - ...Base::COOKIE_RESPONSES, + // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Android12Java11Test.php b/tests/Android12Java11Test.php index 23eec2fbb..aead05eab 100644 --- a/tests/Android12Java11Test.php +++ b/tests/Android12Java11Test.php @@ -17,7 +17,7 @@ class Android12Java11Test extends Base 'chmod +x tests/sdks/android/gradlew', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-32-jdk11 sh -c "./gradlew :library:testReleaseUnitTest -q && cat library/result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-32-jdk11 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, @@ -28,7 +28,7 @@ class Android12Java11Test extends Base ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, - ...Base::COOKIE_RESPONSES, + // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Android12Java8Test.php b/tests/Android12Java8Test.php index 8718b25b4..1fe8f5340 100644 --- a/tests/Android12Java8Test.php +++ b/tests/Android12Java8Test.php @@ -17,7 +17,7 @@ class Android12Java8Test extends Base 'chmod +x tests/sdks/android/gradlew', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-32-jdk8 sh -c "./gradlew :library:testReleaseUnitTest -q && cat library/result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-32-jdk8 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, @@ -28,7 +28,7 @@ class Android12Java8Test extends Base ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, - ...Base::COOKIE_RESPONSES, + // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Android5Java11Test.php b/tests/Android5Java11Test.php index 0b29aff87..a0861181a 100644 --- a/tests/Android5Java11Test.php +++ b/tests/Android5Java11Test.php @@ -17,7 +17,7 @@ class Android5Java11Test extends Base 'chmod +x tests/sdks/android/gradlew', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-21-jdk11 sh -c "./gradlew :library:testReleaseUnitTest -q && cat library/result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-21-jdk11 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, @@ -28,7 +28,7 @@ class Android5Java11Test extends Base ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, - ...Base::COOKIE_RESPONSES, + // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/Android5Java8Test.php b/tests/Android5Java8Test.php index 6332f1040..5d80aa266 100644 --- a/tests/Android5Java8Test.php +++ b/tests/Android5Java8Test.php @@ -17,7 +17,7 @@ class Android5Java8Test extends Base 'chmod +x tests/sdks/android/gradlew', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-21-jdk8 sh -c "./gradlew :library:testReleaseUnitTest -q && cat library/result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/android alvrme/alpine-android:android-21-jdk8 sh -c "./gradlew :library:testReleaseUnitTest --stacktrace -q && cat library/result.txt"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, @@ -28,7 +28,7 @@ class Android5Java8Test extends Base ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, - ...Base::COOKIE_RESPONSES, + // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES diff --git a/tests/AppleSwift55Test.php b/tests/AppleSwift55Test.php index b9b2e8842..4cbd655a0 100644 --- a/tests/AppleSwift55Test.php +++ b/tests/AppleSwift55Test.php @@ -16,7 +16,7 @@ class AppleSwift55Test extends Base 'cp tests/languages/apple/Tests.swift tests/sdks/apple/Tests/AppwriteTests/Tests.swift', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/apple swiftarm/swift:5.5.2-focal-multi-arch swift test'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/apple swiftarm/swift:5.5.2-focal-multi-arch swift test'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Base.php b/tests/Base.php index 2df5b08cf..e0855595b 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -69,9 +69,9 @@ abstract class Base extends TestCase 'search("name", ["john"])', 'isNull("name")', 'isNotNull("name")', - 'between("age", [50,100])', - 'between("age", [50.5,100.5])', - 'between("name", ["Anna","Brad"])', + 'between("age", 50, 100)', + 'between("age", 50.5, 100.5)', + 'between("name", "Anna", "Brad")', 'startsWith("name", ["Ann"])', 'endsWith("name", ["nne"])', 'select(["name","age"])', @@ -93,6 +93,7 @@ abstract class Base extends TestCase 'create("member:memberId")', 'update("users/verified")', 'update("user:userid/unverified")', + 'create("label:admin")', ]; protected const ID_HELPER_RESPONSES = [ @@ -105,11 +106,23 @@ abstract class Base extends TestCase protected array $build = []; protected string $command = ''; protected array $expectedOutput = []; + protected string $sdkName; + protected string $sdkPlatform; + protected string $sdkLanguage; + protected string $version; public function setUp(): void { $headers = "x-sdk-name: {$this->sdkName}; x-sdk-platform: {$this->sdkPlatform}; x-sdk-language: {$this->sdkLanguage}; x-sdk-version: {$this->version}"; array_push($this->expectedOutput, $headers); + + // Figure out if mock-server is running + $isMockAPIRunning = (strlen(exec('docker ps | grep mock-server')) > 0); + + if (!$isMockAPIRunning) { + echo "Starting Mock API Server"; + exec('cd ./mock-server && docker-compose up -d --force-recreate'); + } } public function tearDown(): void diff --git a/tests/CLINode14Test.php b/tests/CLINode14Test.php index 9986758d1..6b92eb423 100644 --- a/tests/CLINode14Test.php +++ b/tests/CLINode14Test.php @@ -19,7 +19,7 @@ class CLINode14Test extends Base 'cp tests/languages/cli/test.js tests/sdks/cli/test.js' ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:14-alpine node test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/cli node:14-alpine node test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/CLINode16Test.php b/tests/CLINode16Test.php index c6ed8549c..0dc378ca4 100644 --- a/tests/CLINode16Test.php +++ b/tests/CLINode16Test.php @@ -19,7 +19,7 @@ class CLINode16Test extends Base 'cp tests/languages/cli/test.js tests/sdks/cli/test.js' ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:16-alpine node test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/cli node:16-alpine node test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/DartBetaTest.php b/tests/DartBetaTest.php index f6dec74ee..85a8e8b9e 100644 --- a/tests/DartBetaTest.php +++ b/tests/DartBetaTest.php @@ -16,7 +16,7 @@ class DartBetaTest extends Base 'cp tests/languages/dart/tests.dart tests/sdks/dart/tests/tests.dart', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/dart dart:beta sh -c "dart pub get && dart pub run tests/tests.dart"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/dart dart:beta sh -c "dart pub get && dart pub run tests/tests.dart"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/DartStableTest.php b/tests/DartStableTest.php index 1c586ab28..ac21d95d5 100644 --- a/tests/DartStableTest.php +++ b/tests/DartStableTest.php @@ -16,7 +16,7 @@ class DartStableTest extends Base 'cp tests/languages/dart/tests.dart tests/sdks/dart/tests/tests.dart', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/dart dart:stable sh -c "dart pub get && dart pub run tests/tests.dart"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/dart dart:stable sh -c "dart pub get && dart pub run tests/tests.dart"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Deno1193Test.php b/tests/Deno1193Test.php index 29f5502ca..a16066f85 100644 --- a/tests/Deno1193Test.php +++ b/tests/Deno1193Test.php @@ -13,7 +13,7 @@ class Deno1193Test extends Base protected string $class = 'Appwrite\SDK\Language\Deno'; protected array $build = []; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app denoland/deno:alpine-1.19.3 run --allow-net --allow-read tests/languages/deno/tests.ts'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app denoland/deno:alpine-1.19.3 run --allow-net --allow-read tests/languages/deno/tests.ts'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Deno1303Test.php b/tests/Deno1303Test.php index fb09573b9..ef28031f7 100644 --- a/tests/Deno1303Test.php +++ b/tests/Deno1303Test.php @@ -13,7 +13,7 @@ class Deno1303Test extends Base protected string $class = 'Appwrite\SDK\Language\Deno'; protected array $build = []; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app denoland/deno:alpine-1.30.3 run --allow-net --allow-read tests/languages/deno/tests.ts'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app denoland/deno:alpine-1.30.3 run --allow-net --allow-read tests/languages/deno/tests.ts'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/DotNet31Test.php b/tests/DotNet31Test.php index 9d0b6733a..0852909fb 100644 --- a/tests/DotNet31Test.php +++ b/tests/DotNet31Test.php @@ -17,7 +17,7 @@ class DotNet31Test extends Base 'cp tests/languages/dotnet/Tests31.csproj tests/sdks/dotnet/src/test/Tests.csproj', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/dotnet/src/test/ mcr.microsoft.com/dotnet/sdk:3.1 dotnet test --verbosity normal --framework netcoreapp3.1'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/dotnet/src/test/ mcr.microsoft.com/dotnet/sdk:3.1 dotnet test --verbosity normal --framework netcoreapp3.1'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/DotNet60Test.php b/tests/DotNet60Test.php index ecadcd9d6..d2d74c17d 100644 --- a/tests/DotNet60Test.php +++ b/tests/DotNet60Test.php @@ -17,7 +17,7 @@ class DotNet60Test extends Base 'cp tests/languages/dotnet/Tests60.csproj tests/sdks/dotnet/src/test/Tests.csproj', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/dotnet/src/test/ mcr.microsoft.com/dotnet/sdk:6.0-alpine3.17 dotnet test --verbosity normal --framework net6.0'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/dotnet/src/test/ mcr.microsoft.com/dotnet/sdk:6.0-alpine3.17 dotnet test --verbosity normal --framework net6.0'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/DotNet70Test.php b/tests/DotNet70Test.php index 582213e81..b73a37713 100644 --- a/tests/DotNet70Test.php +++ b/tests/DotNet70Test.php @@ -17,7 +17,7 @@ class DotNet70Test extends Base 'cp tests/languages/dotnet/Tests70.csproj tests/sdks/dotnet/src/test/Tests.csproj', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/dotnet/src/test/ mcr.microsoft.com/dotnet/sdk:7.0-alpine3.17 dotnet test --verbosity normal --framework net7.0'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/dotnet/src/test/ mcr.microsoft.com/dotnet/sdk:7.0-alpine3.17 dotnet test --verbosity normal --framework net7.0'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/FlutterBetaTest.php b/tests/FlutterBetaTest.php index bbb1ff2bf..50e4caaee 100644 --- a/tests/FlutterBetaTest.php +++ b/tests/FlutterBetaTest.php @@ -16,7 +16,7 @@ class FlutterBetaTest extends Base 'cp tests/languages/flutter/tests.dart tests/sdks/flutter/test/appwrite_test.dart', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/flutter --env PUB_CACHE=vendor cirrusci/flutter:beta sh -c "flutter pub get && flutter test test/appwrite_test.dart"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/flutter --env PUB_CACHE=vendor cirrusci/flutter:beta sh -c "flutter pub get && flutter test test/appwrite_test.dart"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/FlutterStableTest.php b/tests/FlutterStableTest.php index 342820ddd..1fba442ea 100644 --- a/tests/FlutterStableTest.php +++ b/tests/FlutterStableTest.php @@ -16,7 +16,7 @@ class FlutterStableTest extends Base 'cp tests/languages/flutter/tests.dart tests/sdks/flutter/test/appwrite_test.dart', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/flutter --env PUB_CACHE=vendor cirrusci/flutter:stable sh -c "flutter pub get && flutter test test/appwrite_test.dart"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/flutter --env PUB_CACHE=vendor cirrusci/flutter:stable sh -c "flutter pub get && flutter test test/appwrite_test.dart"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Go112Test.php b/tests/Go112Test.php index c845e202f..1da698fa2 100644 --- a/tests/Go112Test.php +++ b/tests/Go112Test.php @@ -16,7 +16,7 @@ class Go112Test extends Base 'cp -Rf tests/sdks/go/* tests/tmp/go/src/github.com/repoowner/sdk-for-go/' ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app golang:1.12 sh -c "cd tests/languages/go/ && ./test.sh"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app golang:1.12 sh -c "cd tests/languages/go/ && ./test.sh"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/Go118Test.php b/tests/Go118Test.php index d05acf7ac..4f859f434 100644 --- a/tests/Go118Test.php +++ b/tests/Go118Test.php @@ -16,7 +16,7 @@ class Go118Test extends Base 'cp -Rf tests/sdks/go/* tests/tmp/go/src/github.com/repoowner/sdk-for-go/' ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app golang:1.18 sh -c "cd tests/languages/go/ && ./test.sh"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app golang:1.18 sh -c "cd tests/languages/go/ && ./test.sh"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, diff --git a/tests/KotlinJava11Test.php b/tests/KotlinJava11Test.php index 0522fc7cc..626820ed4 100644 --- a/tests/KotlinJava11Test.php +++ b/tests/KotlinJava11Test.php @@ -17,7 +17,7 @@ class KotlinJava11Test extends Base 'chmod +x tests/sdks/kotlin/gradlew', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:11-jdk-slim sh -c "./gradlew test -q && cat result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:11-jdk-slim sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/KotlinJava17Test.php b/tests/KotlinJava17Test.php index df1110f8b..c77c051ec 100644 --- a/tests/KotlinJava17Test.php +++ b/tests/KotlinJava17Test.php @@ -17,7 +17,7 @@ class KotlinJava17Test extends Base 'chmod +x tests/sdks/kotlin/gradlew', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:17-jdk-slim sh -c "./gradlew test -q && cat result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:17-jdk-slim sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/KotlinJava8Test.php b/tests/KotlinJava8Test.php index c5ded35ff..a5fc8b176 100644 --- a/tests/KotlinJava8Test.php +++ b/tests/KotlinJava8Test.php @@ -17,7 +17,7 @@ class KotlinJava8Test extends Base 'chmod +x tests/sdks/kotlin/gradlew', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:8-jdk-slim sh -c "./gradlew test -q && cat result.txt"'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/kotlin openjdk:8-jdk-slim sh -c "./gradlew test -q && cat result.txt"'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Node12Test.php b/tests/Node12Test.php index d60777322..9c0e6818e 100644 --- a/tests/Node12Test.php +++ b/tests/Node12Test.php @@ -16,7 +16,7 @@ class Node12Test extends Base 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:12-alpine npm install', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app node:12-alpine node tests/languages/node/test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:12-alpine node tests/languages/node/test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Node14Test.php b/tests/Node14Test.php index fdacb50af..4e78c97f6 100644 --- a/tests/Node14Test.php +++ b/tests/Node14Test.php @@ -15,7 +15,7 @@ class Node14Test extends Base 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:14-alpine npm install', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app node:14-alpine node tests/languages/node/test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:14-alpine node tests/languages/node/test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Node16Test.php b/tests/Node16Test.php index c05cd7249..af9d23d16 100644 --- a/tests/Node16Test.php +++ b/tests/Node16Test.php @@ -15,7 +15,7 @@ class Node16Test extends Base 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:16-alpine npm install', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app node:16-alpine node tests/languages/node/test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:16-alpine node tests/languages/node/test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/PHP74Test.php b/tests/PHP74Test.php index bbd296b42..1c93a1daf 100644 --- a/tests/PHP74Test.php +++ b/tests/PHP74Test.php @@ -13,7 +13,7 @@ class PHP74Test extends Base protected string $class = 'Appwrite\SDK\Language\PHP'; protected array $build = []; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app php:7.4-cli-alpine php tests/languages/php/test.php'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app php:7.4-cli-alpine php tests/languages/php/test.php'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/PHP80Test.php b/tests/PHP80Test.php index 32a0a1c76..a141deaba 100644 --- a/tests/PHP80Test.php +++ b/tests/PHP80Test.php @@ -13,7 +13,7 @@ class PHP80Test extends Base protected string $class = 'Appwrite\SDK\Language\PHP'; protected array $build = []; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app php:8.0-cli-alpine php tests/languages/php/test.php'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app php:8.0-cli-alpine php tests/languages/php/test.php'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Python310Test.php b/tests/Python310Test.php index a8c5f2cef..6b6b2c5a2 100644 --- a/tests/Python310Test.php +++ b/tests/Python310Test.php @@ -14,10 +14,10 @@ class Python310Test extends Base protected array $build = [ 'cp tests/languages/python/tests.py tests/sdks/python/test.py', 'echo "" > tests/sdks/python/__init__.py', - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.10 pip install -r tests/sdks/python/requirements.txt --upgrade', + 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.10-alpine pip install -r tests/sdks/python/requirements.txt --upgrade', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.10-alpine python tests/sdks/python/test.py'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.10-alpine python tests/sdks/python/test.py'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Python38Test.php b/tests/Python38Test.php index 0fc836224..1cfe30cc3 100644 --- a/tests/Python38Test.php +++ b/tests/Python38Test.php @@ -14,10 +14,10 @@ class Python38Test extends Base protected array $build = [ 'cp tests/languages/python/tests.py tests/sdks/python/test.py', 'echo "" > tests/sdks/python/__init__.py', - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.8 pip install -r tests/sdks/python/requirements.txt --upgrade', + 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.8-alpine pip install -r tests/sdks/python/requirements.txt --upgrade', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.8-alpine python tests/sdks/python/test.py'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.8-alpine python tests/sdks/python/test.py'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Python39Test.php b/tests/Python39Test.php index 5dbfe4fcf..008cca5df 100644 --- a/tests/Python39Test.php +++ b/tests/Python39Test.php @@ -14,10 +14,10 @@ class Python39Test extends Base protected array $build = [ 'cp tests/languages/python/tests.py tests/sdks/python/test.py', 'echo "" > tests/sdks/python/__init__.py', - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.9 pip install -r tests/sdks/python/requirements.txt --upgrade', + 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor python:3.9-alpine pip install -r tests/sdks/python/requirements.txt --upgrade', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.9-alpine python tests/sdks/python/test.py'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app --env PIP_TARGET=tests/sdks/python/vendor --env PYTHONPATH=tests/sdks/python/vendor python:3.9-alpine python tests/sdks/python/test.py'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Ruby27Test.php b/tests/Ruby27Test.php index 2d5af6d0d..d17b42966 100644 --- a/tests/Ruby27Test.php +++ b/tests/Ruby27Test.php @@ -15,7 +15,7 @@ class Ruby27Test extends Base 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/ruby --env GEM_HOME=/app/vendor ruby:2.7-alpine sh -c "apk add git build-base && bundle install"', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app --env GEM_HOME=vendor ruby:2.7-alpine ruby tests/languages/ruby/tests.rb'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app --env GEM_HOME=vendor ruby:2.7-alpine ruby tests/languages/ruby/tests.rb'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Ruby30Test.php b/tests/Ruby30Test.php index 1d2d4c102..8a135c82c 100644 --- a/tests/Ruby30Test.php +++ b/tests/Ruby30Test.php @@ -15,7 +15,7 @@ class Ruby30Test extends Base 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/ruby --env GEM_HOME=/app/vendor ruby:3.0-alpine sh -c "apk add git build-base && bundle install"', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app --env GEM_HOME=vendor ruby:3.0-alpine ruby tests/languages/ruby/tests.rb'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app --env GEM_HOME=vendor ruby:3.0-alpine ruby tests/languages/ruby/tests.rb'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Ruby31Test.php b/tests/Ruby31Test.php index 97039da86..0cf7a122b 100644 --- a/tests/Ruby31Test.php +++ b/tests/Ruby31Test.php @@ -15,7 +15,7 @@ class Ruby31Test extends Base 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/ruby --env GEM_HOME=/app/vendor ruby:3.1-alpine sh -c "apk add git build-base && bundle install"', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app --env GEM_HOME=vendor ruby:3.1-alpine ruby tests/languages/ruby/tests.rb'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app --env GEM_HOME=vendor ruby:3.1-alpine ruby tests/languages/ruby/tests.rb'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/Swift55Test.php b/tests/Swift55Test.php index 568de657a..c8fd09fcc 100644 --- a/tests/Swift55Test.php +++ b/tests/Swift55Test.php @@ -16,7 +16,7 @@ class Swift55Test extends Base 'cp tests/languages/swift/Tests.swift tests/sdks/swift/Tests/AppwriteTests/Tests.swift', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/swift swiftarm/swift:5.5.2-focal-multi-arch swift test'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/swift swiftarm/swift:5.5.2-focal-multi-arch swift test'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/WebChromiumTest.php b/tests/WebChromiumTest.php index b1d463e23..967957763 100644 --- a/tests/WebChromiumTest.php +++ b/tests/WebChromiumTest.php @@ -19,7 +19,7 @@ class WebChromiumTest extends Base 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.15.0-focal npm run build', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -e BROWSER=chromium -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.15.0-focal node tests.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -e BROWSER=chromium -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.15.0-focal node tests.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/WebNodeTest.php b/tests/WebNodeTest.php index b6154aa1a..9aa26a790 100644 --- a/tests/WebNodeTest.php +++ b/tests/WebNodeTest.php @@ -19,7 +19,7 @@ class WebNodeTest extends Base 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.15.0-focal npm run build', ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web node:18-alpine node node.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/web node:18-alpine node node.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index 0df42c522..b8b608f5a 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -152,11 +152,11 @@ class ServiceTest { delay(5000) writeToFile(realtimeResponse) - mock = general.setCookie() - writeToFile(mock.result) + // mock = general.setCookie() + // writeToFile(mock.result) - mock = general.getCookie() - writeToFile(mock.result) + // mock = general.getCookie() + // writeToFile(mock.result) general.empty() @@ -192,6 +192,7 @@ class ServiceTest { writeToFile(Permission.create(Role.member("memberId"))) writeToFile(Permission.update(Role.users("verified"))) writeToFile(Permission.update(Role.user(ID.custom("userid"), "unverified"))) + writeToFile(Permission.create(Role.label("admin"))) // ID helper tests writeToFile(ID.unique()) diff --git a/tests/languages/apple/Tests.swift b/tests/languages/apple/Tests.swift index 64fa928a7..06cd5144f 100644 --- a/tests/languages/apple/Tests.swift +++ b/tests/languages/apple/Tests.swift @@ -175,6 +175,7 @@ class Tests: XCTestCase { print(Permission.create(Role.member("memberId"))) print(Permission.update(Role.users("verified"))) print(Permission.update(Role.user(ID.custom("userid"), "unverified"))) + print(Permission.create(Role.label("admin"))) // ID helper tests print(ID.unique()) diff --git a/tests/languages/cli/test.js b/tests/languages/cli/test.js index 6c814738e..782fe357f 100644 --- a/tests/languages/cli/test.js +++ b/tests/languages/cli/test.js @@ -1,6 +1,6 @@ const { exec, execSync } = require('child_process'); -execSync("node index client --endpoint 'https://stage.appwrite.io/v1' --projectId console --key=35y3h5h345 --selfSigned true", { stdio: 'inherit' }); +execSync("node index client --endpoint 'https://qa.appwrite.org/v1' --projectId console --key=35y3h5h345 --selfSigned true", { stdio: 'inherit' }); var output; console.log('\nTest Started'); diff --git a/tests/languages/dart/tests.dart b/tests/languages/dart/tests.dart index a77117dfc..94c474ab2 100644 --- a/tests/languages/dart/tests.dart +++ b/tests/languages/dart/tests.dart @@ -120,6 +120,7 @@ void main() async { print(Permission.create(Role.member('memberId'))); print(Permission.update(Role.users('verified'))); print(Permission.update(Role.user(ID.custom('userid'), 'unverified'))); + print(Permission.create(Role.label('admin'))); // ID helper tests print(ID.unique()); diff --git a/tests/languages/deno/tests.ts b/tests/languages/deno/tests.ts index ae9cfe37e..f0bb47138 100644 --- a/tests/languages/deno/tests.ts +++ b/tests/languages/deno/tests.ts @@ -129,6 +129,7 @@ async function start() { console.log(Permission.create(Role.member('memberId'))); console.log(Permission.update(Role.users('verified'))); console.log(Permission.update(Role.user(ID.custom('userid'), 'unverified'))); + console.log(Permission.create(Role.label('admin'))); // ID helper tests console.log(ID.unique()); diff --git a/tests/languages/dotnet/Tests.cs b/tests/languages/dotnet/Tests.cs index cb6b765d6..c01946a74 100644 --- a/tests/languages/dotnet/Tests.cs +++ b/tests/languages/dotnet/Tests.cs @@ -136,8 +136,9 @@ public async Task Test1() TestContext.WriteLine(Permission.Delete(Role.Team("teamId", "owner"))); TestContext.WriteLine(Permission.Delete(Role.Team("teamId"))); TestContext.WriteLine(Permission.Create(Role.Member("memberId"))); - TestContext.WriteLine(Permission.Update(Role.Users("verified")));; - TestContext.WriteLine(Permission.Update(Role.User(ID.Custom("userid"), "unverified")));; + TestContext.WriteLine(Permission.Update(Role.Users("verified"))); + TestContext.WriteLine(Permission.Update(Role.User(ID.Custom("userid"), "unverified"))); + TestContext.WriteLine(Permission.Create(Role.Label("admin"))); // ID helper tests TestContext.WriteLine(ID.Unique()); diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index 8b13681e4..b6fb89e3b 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -153,6 +153,7 @@ void main() async { print(Permission.create(Role.member('memberId'))); print(Permission.update(Role.users('verified'))); print(Permission.update(Role.user(ID.custom('userid'), 'unverified'))); + print(Permission.create(Role.label('admin'))); // ID helper tests print(ID.unique()); diff --git a/tests/languages/kotlin/Tests.kt b/tests/languages/kotlin/Tests.kt index 0edf3e896..33b4ae06a 100644 --- a/tests/languages/kotlin/Tests.kt +++ b/tests/languages/kotlin/Tests.kt @@ -152,6 +152,7 @@ class ServiceTest { writeToFile(Permission.create(Role.member("memberId"))) writeToFile(Permission.update(Role.users("verified"))); writeToFile(Permission.update(Role.user(ID.custom("userid"), "unverified"))); + writeToFile(Permission.create(Role.label("admin"))); // ID helper tests writeToFile(ID.unique()) diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index 03da67da1..259e7c0ea 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -117,6 +117,7 @@ async function start() { console.log(Permission.create(Role.member('memberId'))); console.log(Permission.update(Role.users('verified'))); console.log(Permission.update(Role.user(ID.custom('userid'), 'unverified'))); + console.log(Permission.create(Role.label('admin'))); // ID helper tests console.log(ID.unique()); diff --git a/tests/languages/php/test.php b/tests/languages/php/test.php index 2bc98da87..4f74f4e60 100644 --- a/tests/languages/php/test.php +++ b/tests/languages/php/test.php @@ -135,6 +135,7 @@ echo Permission::create(Role::member('memberId')) . "\n"; echo Permission::update(Role::users('verified')) . "\n"; echo Permission::update(Role::user(ID::custom('userid'), 'unverified')) . "\n"; +echo Permission::create(Role::label('admin')) . "\n"; // ID helper tests echo ID::unique() . "\n"; diff --git a/tests/languages/python/tests.py b/tests/languages/python/tests.py index 9c262b29c..99f70c301 100644 --- a/tests/languages/python/tests.py +++ b/tests/languages/python/tests.py @@ -124,6 +124,7 @@ print(Permission.create(Role.member('memberId'))) print(Permission.update(Role.users('verified'))) print(Permission.update(Role.user(ID.custom('userid'), 'unverified'))) +print(Permission.create(Role.label('admin'))) # ID helper tests print(ID.unique()) diff --git a/tests/languages/ruby/tests.rb b/tests/languages/ruby/tests.rb index dc14cafe4..4a76966b0 100644 --- a/tests/languages/ruby/tests.rb +++ b/tests/languages/ruby/tests.rb @@ -3,6 +3,7 @@ include Appwrite client = Client.new +client.set_self_signed(true) client.add_header('Origin', 'http://localhost') foo = Foo.new(client) @@ -133,6 +134,7 @@ puts Permission.create(Role.member('memberId')) puts Permission.update(Role.users('verified')) puts Permission.update(Role.user(ID.custom('userid'), 'unverified')) +puts Permission.create(Role.label('admin')) # ID helper tests puts ID.unique() diff --git a/tests/languages/swift/Tests.swift b/tests/languages/swift/Tests.swift index ab8d3847b..948de3601 100644 --- a/tests/languages/swift/Tests.swift +++ b/tests/languages/swift/Tests.swift @@ -156,6 +156,7 @@ class Tests: XCTestCase { print(Permission.create(Role.member("memberId"))) print(Permission.update(Role.users("verified"))) print(Permission.update(Role.user(ID.custom("userid"), "unverified"))) + print(Permission.create(Role.label("admin"))) // ID helper tests print(ID.unique()) diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index b7396a113..17387180f 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -145,6 +145,7 @@ console.log(Permission.create(Role.member('memberId'))); console.log(Permission.update(Role.users('verified'))); console.log(Permission.update(Role.user(ID.custom('userid'), 'unverified'))); + console.log(Permission.create(Role.label('admin'))); // ID helper tests diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index 9a95c0a70..0ec235512 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -102,6 +102,7 @@ async function start() { console.log(Permission.create(Role.member('memberId'))); console.log(Permission.update(Role.users('verified'))); console.log(Permission.update(Role.user(ID.custom('userid'), 'unverified'))); + console.log(Permission.create(Role.label('admin'))); // ID helper tests diff --git a/tests/resources/spec.json b/tests/resources/spec.json index fae575b37..772602681 100644 --- a/tests/resources/spec.json +++ b/tests/resources/spec.json @@ -15,9 +15,9 @@ "url": "https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE" } }, - "host": "stage.appwrite.io", + "host": "mockapi", "basePath": "/v1", - "schemes": ["https"], + "schemes": ["http"], "consumes": ["application/json", "multipart/form-data"], "produces": ["application/json"], "securityDefinitions": {