diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd28fab48..1a453a3b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -288,7 +288,7 @@ The Runtimes for all supported cloud functions (multicore builds) can be found a For generating a new console SDK follow the next steps: -1. Update the console spec file located at `app/config/specs/0.10.x.console.json` from the dynamic version located at `https://localhost/specs/swagger2?platform=console` +1. Update the console spec file located at `app/config/specs/swagger2-0.12.x.console.json` from the dynamic version located at `https://localhost/specs/swagger2?platform=console` 2. Generate a new SDK using the command `php app/cli.php sdks` 3. Change your working dir using `cd app/sdks/console-web` 4. Build the new SDK `npm run build` diff --git a/Dockerfile b/Dockerfile index fe515f7b5..e6f133420 100755 --- a/Dockerfile +++ b/Dockerfile @@ -259,6 +259,7 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/realtime && \ chmod +x /usr/local/bin/schedule && \ chmod +x /usr/local/bin/sdks && \ + chmod +x /usr/local/bin/specs && \ chmod +x /usr/local/bin/ssl && \ chmod +x /usr/local/bin/test && \ chmod +x /usr/local/bin/vars && \ diff --git a/app/cli.php b/app/cli.php index 99a77d638..d2c6496dc 100644 --- a/app/cli.php +++ b/app/cli.php @@ -1,6 +1,6 @@ setParam('body', $page); }); -App::get('/specs/:format') - ->groups(['web', 'home']) - ->label('scope', 'public') - ->label('docs', false) - ->label('origin', '*') - ->param('format', 'swagger2', new WhiteList(['swagger2', 'open-api3'], true), 'Spec format.', true) - ->param('platform', APP_PLATFORM_CLIENT, new WhiteList([APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER, APP_PLATFORM_CONSOLE], true), 'Choose target platform.', true) - ->param('tests', 0, function () {return new Range(0, 1);}, 'Include only test services.', true) - ->inject('utopia') - ->inject('request') - ->inject('response') - ->action(function ($format, $platform, $tests, $utopia, $request, $response) { - /** @var Utopia\App $utopia */ - /** @var Utopia\Swoole\Request $request */ - /** @var Appwrite\Utopia\Response $response */ - - $platforms = [ - 'client' => APP_PLATFORM_CLIENT, - 'server' => APP_PLATFORM_SERVER, - 'console' => APP_PLATFORM_CONSOLE, - ]; - - $authCounts = [ - 'client' => 1, - 'server' => 2, - 'console' => 1, - ]; - - $routes = []; - $models = []; - $services = []; - - $keys = [ - APP_PLATFORM_CLIENT => [ - 'Project' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Project', - 'description' => 'Your project ID', - 'in' => 'header', - ], - 'JWT' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-JWT', - 'description' => 'Your secret JSON Web Token', - 'in' => 'header', - ], - 'Locale' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Locale', - 'description' => '', - 'in' => 'header', - ], - ], - APP_PLATFORM_SERVER => [ - 'Project' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Project', - 'description' => 'Your project ID', - 'in' => 'header', - ], - 'Key' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Key', - 'description' => 'Your secret API key', - 'in' => 'header', - ], - 'JWT' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-JWT', - 'description' => 'Your secret JSON Web Token', - 'in' => 'header', - ], - 'Locale' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Locale', - 'description' => '', - 'in' => 'header', - ], - ], - APP_PLATFORM_CONSOLE => [ - 'Project' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Project', - 'description' => 'Your project ID', - 'in' => 'header', - ], - 'Key' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Key', - 'description' => 'Your secret API key', - 'in' => 'header', - ], - 'JWT' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-JWT', - 'description' => 'Your secret JSON Web Token', - 'in' => 'header', - ], - 'Locale' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Locale', - 'description' => '', - 'in' => 'header', - ], - 'Mode' => [ - 'type' => 'apiKey', - 'name' => 'X-Appwrite-Mode', - 'description' => '', - 'in' => 'header', - ], - ], - ]; - - foreach ($utopia->getRoutes() as $key => $method) { - foreach ($method as $route) { /** @var \Utopia\Route $route */ - $routeSecurity = $route->getLabel('sdk.auth', []); - $sdkPlatofrms = []; - - foreach ($routeSecurity as $value) { - switch ($value) { - case APP_AUTH_TYPE_SESSION: - $sdkPlatofrms[] = APP_PLATFORM_CLIENT; - break; - case APP_AUTH_TYPE_KEY: - $sdkPlatofrms[] = APP_PLATFORM_SERVER; - break; - case APP_AUTH_TYPE_JWT: - $sdkPlatofrms[] = APP_PLATFORM_SERVER; - break; - case APP_AUTH_TYPE_ADMIN: - $sdkPlatofrms[] = APP_PLATFORM_CONSOLE; - break; - } - } - - if(empty($routeSecurity)) { - $sdkPlatofrms[] = APP_PLATFORM_CLIENT; - } - - if (!$route->getLabel('docs', true)) { - continue; - } - - if ($route->getLabel('sdk.mock', false) && !$tests) { - continue; - } - - if (!$route->getLabel('sdk.mock', false) && $tests) { - continue; - } - - if (empty($route->getLabel('sdk.namespace', null))) { - continue; - } - - if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $sdkPlatofrms)) { - continue; - } - - $routes[] = $route; - $modelLabel = $route->getLabel('sdk.response.model', 'none'); - $model = \is_array($modelLabel) ? \array_map(function($m) use($response) { - return $response->getModel($m); - }, $modelLabel) : $response->getModel($modelLabel); - } - } - - foreach (Config::getParam('services', []) as $service) { - if(!isset($service['docs']) // Skip service if not part of the public API - || !isset($service['sdk']) - || !$service['docs'] - || !$service['sdk']) { - continue; - } - - $services[] = [ - 'name' => $service['key'] ?? '', - 'description' => $service['subtitle'] ?? '', - ]; - } - - $models = $response->getModels(); - - foreach ($models as $key => $value) { - if($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) { - unset($models[$key]); - } - } - - switch ($format) { - case 'swagger2': - $format = new Swagger2($utopia, $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0); - break; - - case 'open-api3': - $format = new OpenAPI3($utopia, $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0); - break; - - default: - throw new Exception('Format not found', 404); - break; - } - - $specs = new Specification($format); - - $format - ->setParam('name', APP_NAME) - ->setParam('description', 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)') - ->setParam('endpoint', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/v1') - ->setParam('version', APP_VERSION_STABLE) - ->setParam('terms', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/policy/terms') - ->setParam('support.email', App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM)) - ->setParam('support.url', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/support') - ->setParam('contact.name', APP_NAME.' Team') - ->setParam('contact.email', App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM)) - ->setParam('contact.url', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/support') - ->setParam('license.name', 'BSD-3-Clause') - ->setParam('license.url', 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE') - ->setParam('docs.description', 'Full API docs, specs and tutorials') - ->setParam('docs.url', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/docs') - ; - - $response - ->json($specs->parse()); - }); - App::get('/versions') ->desc('Get Version') ->groups(['web', 'home']) diff --git a/app/tasks/maintenance.php b/app/tasks/maintenance.php index e2fa90fcd..a6f37ff71 100644 --- a/app/tasks/maintenance.php +++ b/app/tasks/maintenance.php @@ -2,8 +2,6 @@ global $cli; -require_once __DIR__.'/../init.php'; - use Appwrite\Event\Event; use Utopia\App; use Utopia\CLI\Console; diff --git a/app/tasks/sdks.php b/app/tasks/sdks.php index 5fcf9efe6..acf099642 100644 --- a/app/tasks/sdks.php +++ b/app/tasks/sdks.php @@ -47,7 +47,7 @@ $cli Console::info('Fetching API Spec for '.$language['name'].' for '.$platform['name'] . ' (version: '.$version.')'); - $spec = file_get_contents(__DIR__.'/../config/specs/'.$version.'.'.$language['family'].'.json'); + $spec = file_get_contents(__DIR__.'/../config/specs/swagger2-'.$version.'.'.$language['family'].'.json'); $cover = 'https://appwrite.io/images/github.png'; $result = \realpath(__DIR__.'/..').'/sdks/'.$key.'-'.$language['key']; diff --git a/app/tasks/specs.php b/app/tasks/specs.php new file mode 100644 index 000000000..dc073177c --- /dev/null +++ b/app/tasks/specs.php @@ -0,0 +1,266 @@ +task('specs') + ->param('version', 'latest', new Text(8), 'Spec version', true) + ->param('mode', 'normal', new WhiteList(['normal', 'mocks']), 'Spec Mode', true) + ->action(function ($version, $mode) use ($register) { + $db = $register->get('db'); + $redis = $register->get('cache'); + $appRoutes = App::getRoutes(); + $response = new Response(new HttpResponse()); + $mocks = ($mode === 'mocks'); + + App::setResource('request', fn() => new Request); + App::setResource('response', fn() => $response); + + App::setResource('db', fn() => $db); + App::setResource('cache', fn() => $redis); + + $platforms = [ + 'client' => APP_PLATFORM_CLIENT, + 'server' => APP_PLATFORM_SERVER, + 'console' => APP_PLATFORM_CONSOLE, + ]; + + $authCounts = [ + 'client' => 1, + 'server' => 2, + 'console' => 1, + ]; + + $keys = [ + APP_PLATFORM_CLIENT => [ + 'Project' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Project', + 'description' => 'Your project ID', + 'in' => 'header', + ], + 'JWT' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-JWT', + 'description' => 'Your secret JSON Web Token', + 'in' => 'header', + ], + 'Locale' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Locale', + 'description' => '', + 'in' => 'header', + ], + ], + APP_PLATFORM_SERVER => [ + 'Project' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Project', + 'description' => 'Your project ID', + 'in' => 'header', + ], + 'Key' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Key', + 'description' => 'Your secret API key', + 'in' => 'header', + ], + 'JWT' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-JWT', + 'description' => 'Your secret JSON Web Token', + 'in' => 'header', + ], + 'Locale' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Locale', + 'description' => '', + 'in' => 'header', + ], + ], + APP_PLATFORM_CONSOLE => [ + 'Project' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Project', + 'description' => 'Your project ID', + 'in' => 'header', + ], + 'Key' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Key', + 'description' => 'Your secret API key', + 'in' => 'header', + ], + 'JWT' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-JWT', + 'description' => 'Your secret JSON Web Token', + 'in' => 'header', + ], + 'Locale' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Locale', + 'description' => '', + 'in' => 'header', + ], + 'Mode' => [ + 'type' => 'apiKey', + 'name' => 'X-Appwrite-Mode', + 'description' => '', + 'in' => 'header', + ], + ], + ]; + + foreach (['swagger2', 'open-api3'] as $format) { + foreach ($platforms as $platform) { + + $routes = []; + $models = []; + $services = []; + + foreach ($appRoutes as $key => $method) { + foreach ($method as $route) { /** @var \Utopia\Route $route */ + $routeSecurity = $route->getLabel('sdk.auth', []); + $sdkPlatofrms = []; + + foreach ($routeSecurity as $value) { + switch ($value) { + case APP_AUTH_TYPE_SESSION: + $sdkPlatofrms[] = APP_PLATFORM_CLIENT; + break; + case APP_AUTH_TYPE_KEY: + $sdkPlatofrms[] = APP_PLATFORM_SERVER; + break; + case APP_AUTH_TYPE_JWT: + $sdkPlatofrms[] = APP_PLATFORM_SERVER; + break; + case APP_AUTH_TYPE_ADMIN: + $sdkPlatofrms[] = APP_PLATFORM_CONSOLE; + break; + } + } + + if(empty($routeSecurity)) { + $sdkPlatofrms[] = APP_PLATFORM_CLIENT; + } + + if (!$route->getLabel('docs', true)) { + continue; + } + + if ($route->getLabel('sdk.mock', false) && !$mocks) { + continue; + } + + if (!$route->getLabel('sdk.mock', false) && $mocks) { + continue; + } + + if (empty($route->getLabel('sdk.namespace', null))) { + continue; + } + + if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $sdkPlatofrms)) { + continue; + } + + $routes[] = $route; + $modelLabel = $route->getLabel('sdk.response.model', 'none'); + \is_array($modelLabel) ? \array_map(function($m) use($response) { + return $response->getModel($m); + }, $modelLabel) : $response->getModel($modelLabel); + } + } + + foreach (Config::getParam('services', []) as $service) { + if(!isset($service['docs']) // Skip service if not part of the public API + || !isset($service['sdk']) + || !$service['docs'] + || !$service['sdk']) { + continue; + } + + $services[] = [ + 'name' => $service['key'] ?? '', + 'description' => $service['subtitle'] ?? '', + ]; + } + + $models = $response->getModels(); + + foreach ($models as $key => $value) { + if($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) { + unset($models[$key]); + } + } + + switch ($format) { + case 'swagger2': + $formatInstance = new Swagger2(new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0); + break; + + case 'open-api3': + $formatInstance = new OpenAPI3(new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0); + break; + + default: + throw new Exception('Format not found: '.$format); + break; + } + + $specs = new Specification($formatInstance); + $endpoint = App::getEnv('_APP_HOME', '[HOSTNAME]'); + $email = App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM); + + $formatInstance + ->setParam('name', APP_NAME) + ->setParam('description', 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)') + ->setParam('endpoint', 'https://HOSTNAME/v1') + ->setParam('version', APP_VERSION_STABLE) + ->setParam('terms', $endpoint.'/policy/terms') + ->setParam('support.email', $email) + ->setParam('support.url', $endpoint.'/support') + ->setParam('contact.name', APP_NAME.' Team') + ->setParam('contact.email', $email) + ->setParam('contact.url', $endpoint.'/support') + ->setParam('license.name', 'BSD-3-Clause') + ->setParam('license.url', 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE') + ->setParam('docs.description', 'Full API docs, specs and tutorials') + ->setParam('docs.url', $endpoint.'/docs') + ; + + if($mocks) { + $path = __DIR__.'/../config/specs/'.$format.'-mocks-'.$platform.'.json'; + + if(!file_put_contents($path, json_encode($specs->parse()))) { + throw new Exception('Failed to save mocks spec file: '.$path); + } + + Console::success('Saved mocks spec file: ' . realpath($path)); + + continue; + } + + $path = __DIR__.'/../config/specs/'.$format.'-'.$version.'-'.$platform.'.json'; + + if(!file_put_contents($path, json_encode($specs->parse()))) { + throw new Exception('Failed to save spec file: '.$path); + } + + Console::success('Saved spec file: ' . realpath($path)); + } + } + }); \ No newline at end of file diff --git a/app/tasks/usage.php b/app/tasks/usage.php index 690da5e2f..e341dad3d 100644 --- a/app/tasks/usage.php +++ b/app/tasks/usage.php @@ -2,8 +2,6 @@ global $cli, $register; -require_once __DIR__ . '/../init.php'; - use Utopia\App; use Utopia\Cache\Adapter\Redis; use Utopia\Cache\Cache; diff --git a/bin/specs b/bin/specs new file mode 100644 index 000000000..e77d1487d --- /dev/null +++ b/bin/specs @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php specs $@ \ No newline at end of file diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index 5b4282b2d..0172e6b53 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -396,7 +396,14 @@ class OpenAPI3 extends Format foreach ($this->models as $model) { foreach ($model->getRules() as $rule) { if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) { - $usedModels[] = $rule['type']; + if(\is_array($rule['type'])) { + foreach ($rule['type'] as $value) { + $usedModels[] = $value; + } + } + else { + $usedModels[] = $rule['type']; + } } } } diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index 771761b18..a9c28feda 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -198,7 +198,7 @@ class Swagger2 extends Format $temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [ 'description' => $modelDescription, 'schema' => [ - 'oneOf' => \array_map(function($m) { + 'x-oneOf' => \array_map(function($m) { return ['$ref' => '#/definitions/'.$m->getType()]; }, $model) ], @@ -393,7 +393,14 @@ class Swagger2 extends Format foreach ($this->models as $model) { foreach ($model->getRules() as $rule) { if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) { - $usedModels[] = $rule['type']; + if(\is_array($rule['type'])) { + foreach ($rule['type'] as $value) { + $usedModels[] = $value; + } + } + else { + $usedModels[] = $rule['type']; + } } } } @@ -460,13 +467,13 @@ class Swagger2 extends Format if(\is_array($rule['type'])) { if($rule['array']) { $items = [ - 'anyOf' => \array_map(function($type) { + 'x-anyOf' => \array_map(function($type) { return ['$ref' => '#/definitions/'.$type]; }, $rule['type']) ]; } else { $items = [ - 'oneOf' => \array_map(function($type) { + 'x-oneOf' => \array_map(function($type) { return ['$ref' => '#/definitions/'.$type]; }, $rule['type']) ]; @@ -514,7 +521,7 @@ class Swagger2 extends Format } } if (!in_array($name, $required)) { - $output['definitions'][$model->getType()]['properties'][$name]['nullable'] = true; + $output['definitions'][$model->getType()]['properties'][$name]['x-nullable'] = true; } } } diff --git a/tests/e2e/General/HTTPTest.php b/tests/e2e/General/HTTPTest.php index 4f9a8dc95..9129d7b4b 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -128,26 +128,36 @@ class HTTPTest extends Scope 'content-type' => 'application/json', ], []); - if(!file_put_contents(__DIR__ . '/../../resources/open-api3.json', json_encode($response['body']))) { - throw new Exception('Failed to save spec file'); - } + $directory = __DIR__ . '/../../../app/config/specs/'; + $files = scandir($directory); $client = new Client(); $client->setEndpoint('https://validator.swagger.io'); - /** - * Test for SUCCESS - */ - $response = $client->call(Client::METHOD_POST, '/validator/debug', [ - 'content-type' => 'application/json', - ], json_decode(file_get_contents(realpath(__DIR__ . '/../../resources/open-api3.json')), true)); + foreach($files as $file) { + if(in_array($file, ['.', '..'])) { + continue; + } - $response['body'] = json_decode($response['body'], true); + if( + (strpos($file, 'latest') === false) && + (strpos($file, '0.12.x') === false) + ) { + continue; + } - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertTrue(empty($response['body'])); + /** + * Test for SUCCESS + */ + $response = $client->call(Client::METHOD_POST, '/validator/debug', [ + 'content-type' => 'application/json', + ], json_decode(file_get_contents($directory.$file), true)); - unlink(realpath(__DIR__ . '/../../resources/open-api3.json')); + $response['body'] = json_decode($response['body'], true); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertTrue(empty($response['body'])); + } } public function testResponseHeader() {