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/specs.php b/app/tasks/specs.php new file mode 100644 index 000000000..ce3c63ab7 --- /dev/null +++ b/app/tasks/specs.php @@ -0,0 +1,271 @@ +task('specs') + ->param('version', 'latest', new Text(8), 'Spec version', true) + ->param('mode', 'normal', new WhiteList(['normal', 'tests']), '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()); + $tests = ($mode === 'tests'); + + App::setResource('request', function() { + return new Request; + }); + + App::setResource('response', function() use ($response) { + return $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) && !$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': + $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($tests) { + $path = __DIR__.'/../config/specs/'.$format.'-tests-'.$platform.'.json'; + + if(!file_put_contents($path, json_encode($specs->parse()))) { + throw new Exception('Failed to save tests spec file: '.$path); + } + + Console::success('Saved tests 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..fb4c76021 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 $key => $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..c0b8fec98 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -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 $key => $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..a5b9e701b 100644 --- a/tests/e2e/General/HTTPTest.php +++ b/tests/e2e/General/HTTPTest.php @@ -128,26 +128,34 @@ 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); + /** + * Test for SUCCESS + */ + $response = $client->call(Client::METHOD_POST, '/validator/debug', [ + 'content-type' => 'application/json', + ], json_decode(file_get_contents($directory.$file), true)); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertTrue(empty($response['body'])); + $response['body'] = json_decode($response['body'], true); - unlink(realpath(__DIR__ . '/../../resources/open-api3.json')); + $this->assertEquals(200, $response['headers']['status-code']); + + if(!empty($response['body'])){ + var_dump($directory.$file); + var_dump($response['body']); + } + $this->assertTrue(empty($response['body'])); + } } public function testResponseHeader() {