diff --git a/CHANGES.md b/CHANGES.md index 233301df9e..b478ebc940 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,11 @@ - Upgraded Redis Resque queue library to version 1.3.6 - Added container names to docker-compose.yml (@drandell) - Upgraded ClamAV container image to version 1.0.9 +- Optimised function execution by using fully-qualified function calls +- New and consistent response format for all API object + new response examples in the docs + - Removed user roles attribute from user object (can be fetched from /v1/teams/memberships) ** + - Removed type attribute from session object response (used only internally) + - ** - might be changed before merging to master ## Bug Fixes @@ -31,6 +36,8 @@ - Fixed update form labels and tooltips for Flutter Android apps - Fixed missing custom scopes param for OAuth2 session create API route - Fixed wrong JSON validation when creating and updating database documnets +- Fixed bug where max file size was limited to max of 10MB +- Fixed bug preventing the deletion of the project logo ## Security diff --git a/Dockerfile b/Dockerfile index c4e7e72c82..068e53d6ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,7 +94,7 @@ RUN \ add-apt-repository ppa:certbot/certbot && \ apt-get update && \ apt-get install -y --no-install-recommends --no-install-suggests php$PHP_VERSION php$PHP_VERSION-fpm \ - php$PHP_VERSION-mysqlnd php$PHP_VERSION-curl php$PHP_VERSION-imagick php$PHP_VERSION-mbstring php$PHP_VERSION-dom webp certbot && \ + php$PHP_VERSION-mysqlnd php$PHP_VERSION-curl php$PHP_VERSION-imagick php$PHP_VERSION-mbstring php$PHP_VERSION-dom php$PHP_VERSION-yaml webp certbot && \ # Nginx wget http://nginx.org/download/nginx-1.19.0.tar.gz && \ tar -xzvf nginx-1.19.0.tar.gz && rm nginx-1.19.0.tar.gz && \ @@ -130,13 +130,12 @@ RUN \ # Set Upload Limit (default to 100MB) RUN echo "upload_max_filesize = ${_APP_STORAGE_LIMIT}" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini RUN echo "post_max_size = ${_APP_STORAGE_LIMIT}" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini -RUN echo "env[TESTME] = your-secret-key" >> /etc/php/$PHP_VERSION/fpm/conf.d/appwrite.ini # Add logs file RUN echo "" >> /var/log/appwrite.log # Nginx Configuration (with self-signed ssl certificates) -COPY ./docker/nginx.conf /etc/nginx/nginx.conf +COPY ./docker/nginx.conf.template /etc/nginx/nginx.conf.template COPY ./docker/ssl/cert.pem /etc/nginx/ssl/cert.pem COPY ./docker/ssl/key.pem /etc/nginx/ssl/key.pem diff --git a/app/app.php b/app/app.php index 64a4967568..f6d81fcb6f 100644 --- a/app/app.php +++ b/app/app.php @@ -17,6 +17,7 @@ use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; use Appwrite\Network\Validator\Origin; +use Appwrite\Utopia\Response; /* * Configuration files @@ -270,7 +271,6 @@ $utopia->options(function () use ($request, $response) { }); $utopia->error(function ($error /* @var $error Exception */) use ($request, $response, $utopia, $project) { - $env = Config::getParam('env'); $version = Config::getParam('version'); switch ($error->getCode()) { @@ -292,7 +292,7 @@ $utopia->error(function ($error /* @var $error Exception */) use ($request, $res $_SERVER = []; // Reset before reporting to error log to avoid keys being compromised - $output = ((App::MODE_TYPE_DEVELOPMENT == $env)) ? [ + $output = ($utopia->isDevelopment()) ? [ 'message' => $error->getMessage(), 'code' => $error->getCode(), 'file' => $error->getFile(), @@ -337,9 +337,8 @@ $utopia->error(function ($error /* @var $error Exception */) use ($request, $res $response->send($layout->render()); } - $response - ->json($output) - ; + $response->dynamic(new Document($output), + $utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_LOCALE); }); $utopia->get('/manifest.json') diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index a7c1c2c534..a830827796 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -4,7 +4,6 @@ global $utopia, $register, $request, $response, $user, $audit, $webhook, $mail, $project, $projectDB, $clients; use Utopia\Exception; -use Utopia\Response; use Utopia\Config\Config; use Utopia\Validator\Assoc; use Utopia\Validator\Text; @@ -25,6 +24,7 @@ use Appwrite\Database\Validator\Authorization; use Appwrite\Template\Template; use Appwrite\OpenSSL\OpenSSL; use Appwrite\URL\URL as URLParser; +use Appwrite\Utopia\Response; use DeviceDetector\DeviceDetector; use GeoIp2\Database\Reader; use Utopia\Validator\ArrayList; @@ -45,7 +45,6 @@ $utopia->init(function() use (&$oauth2Keys) { $oauth2Keys[] = 'oauth2'.\ucfirst($key); $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } - }); $utopia->post('/v1/account') @@ -134,17 +133,8 @@ $utopia->post('/v1/account') ->setParam('resource', 'users/'.$user->getId()) ; - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user->setAttribute('roles', Authorization::getRoles()), Response::MODEL_USER); } ); @@ -233,8 +223,9 @@ $utopia->post('/v1/account/sessions') ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($profile->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, null) ->addCookie(Auth::$cookieName, Auth::encodeSession($profile->getId(), $secret), $expiry, '/', COOKIE_DOMAIN, ('https' == $protocol), true, COOKIE_SAMESITE) ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json($session->getArrayCopy(['$id', 'type', 'expire'])) ; + + $response->dynamic($session, Response::MODEL_SESSION); } ); @@ -537,16 +528,7 @@ $utopia->get('/v1/account') ->label('sdk.response', ['200' => 'user']) ->action( function () use ($response, &$user, $oauth2Keys) { - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'emailVerification', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + $response->dynamic($user->setAttribute('roles', Authorization::getRoles()), Response::MODEL_USER); } ); @@ -727,15 +709,7 @@ $utopia->patch('/v1/account/name') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + $response->dynamic($user->setAttribute('roles', Authorization::getRoles()), Response::MODEL_USER); } ); @@ -769,15 +743,7 @@ $utopia->patch('/v1/account/password') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + $response->dynamic($user->setAttribute('roles', Authorization::getRoles()), Response::MODEL_USER); } ); @@ -827,15 +793,7 @@ $utopia->patch('/v1/account/email') ->setParam('resource', 'users/'.$user->getId()) ; - $response->json(\array_merge($user->getArrayCopy(\array_merge( - [ - '$id', - 'email', - 'registration', - 'name', - ], - $oauth2Keys - )), ['roles' => Authorization::getRoles()])); + $response->dynamic($user->setAttribute('roles', Authorization::getRoles()), Response::MODEL_USER); } ); diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index d8ecedab84..c7f2c63241 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -115,25 +115,6 @@ $utopia->get('/v1/database/collections') ->param('orderType', 'ASC', function () { return new WhiteList(['ASC', 'DESC']); }, 'Order result by ASC or DESC order.', true) ->action( function ($search, $limit, $offset, $orderType) use ($response, $projectDB) { - /*$vl = new Structure($projectDB); - - var_dump($vl->isValid(new Document([ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - '$permissions' => [ - 'read' => ['*'], - 'write' => ['*'], - ], - 'label' => 'Platforms', - 'key' => 'platforms', - 'type' => 'document', - 'default' => [], - 'required' => false, - 'array' => true, - 'options' => [Database::SYSTEM_COLLECTION_PLATFORMS], - ]))); - - var_dump($vl->getDescription());*/ - $results = $projectDB->getCollection([ 'limit' => $limit, 'offset' => $offset, @@ -357,7 +338,7 @@ $utopia->post('/v1/database/collections/:collectionId/documents') ->param('write', [], function () { return new ArrayList(new Text(64)); }, 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->param('parentDocument', '', function () { return new UID(); }, 'Parent document unique ID. Use when you want your new document to be a child of a parent document.', true) ->param('parentProperty', '', function () { return new Key(); }, 'Parent document property name. Use when you want your new document to be a child of a parent document.', true) - ->param('parentPropertyType', Document::SET_TYPE_ASSIGN, function () { return new WhiteList([Document::SET_TYPE_ASSIGN, Document::SET_TYPE_APPEND, Document::SET_TYPE_PREPEND]); }, 'Parent document property connection type. You can set this value to **assign**, **append** or **prepend**, default value is assign. Use when you want your new document to be a child of a parent document.', true) + ->param('parentPropertyType', Document::SET_TYPE_ASSIGN, function () { return new WhiteList([Document::SET_TYPE_ASSIGN, Document::SET_TYPE_APPEND, Document::SET_TYPE_PREPEND]); }, 'Parent document property connection type. You can set this value to **assign**, **append** or **prepend**, default value is assign. **append** or **prepend** should be set when the parent property is array. Use when you want your new document to be a child of a parent document.', true) ->action( function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType) use ($response, $projectDB, $webhook, $audit) { $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array diff --git a/app/controllers/api/locale.php b/app/controllers/api/locale.php index 30a1211e91..c778d9fd4c 100644 --- a/app/controllers/api/locale.php +++ b/app/controllers/api/locale.php @@ -2,6 +2,8 @@ global $utopia, $register, $request, $response, $projectDB, $project, $user, $audit; +use Appwrite\Database\Document; +use Appwrite\Utopia\Response; use Utopia\App; use Utopia\Locale\Locale; use GeoIp2\Database\Reader; @@ -43,7 +45,7 @@ $utopia->get('/v1/locale') $output['continentCode'] = $record->continent->code; $output['eu'] = (\in_array($record->country->isoCode, $eu)) ? true : false; - foreach ($currencies as $code => $element) { + foreach ($currencies as $element) { if (isset($element['locations']) && isset($element['code']) && \in_array($record->country->isoCode, $element['locations'])) { $currency = $element['code']; } @@ -62,7 +64,9 @@ $utopia->get('/v1/locale') $response ->addHeader('Cache-Control', 'public, max-age='.$time) ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time).' GMT') // 45 days cache - ->json($output); + ; + + $response->dynamic(new Document($output), Response::MODEL_LOCALE); } ); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index d032cef947..c607f6b68c 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -3,7 +3,6 @@ global $utopia, $response, $projectDB; use Utopia\Exception; -use Utopia\Response; use Utopia\Validator\Assoc; use Utopia\Validator\WhiteList; use Utopia\Validator\Email; @@ -18,6 +17,7 @@ use Appwrite\Auth\Validator\Password; use Appwrite\Database\Database; use Appwrite\Database\Exception\Duplicate; use Appwrite\Database\Validator\UID; +use Appwrite\Utopia\Response; use DeviceDetector\DeviceDetector; use GeoIp2\Database\Reader; @@ -79,16 +79,8 @@ $utopia->post('/v1/users') $oauth2Keys[] = 'oauth2'.\ucfirst($key).'AccessToken'; } - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->json(\array_merge($user->getArrayCopy(\array_merge([ - '$id', - 'status', - 'email', - 'registration', - 'emailVerification', - 'name', - ], $oauth2Keys)), ['roles' => []])); + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic($user, Response::MODEL_USER); } ); diff --git a/app/init.php b/app/init.php index c58895859b..c6f531e083 100644 --- a/app/init.php +++ b/app/init.php @@ -13,7 +13,6 @@ if (\file_exists(__DIR__.'/../vendor/autoload.php')) { use Utopia\App; use Utopia\Request; -use Utopia\Response; use Utopia\Config\Config; use Utopia\Locale\Locale; use Utopia\Registry\Registry; @@ -23,6 +22,7 @@ use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Database\Adapter\MySQL as MySQLAdapter; use Appwrite\Database\Adapter\Redis as RedisAdapter; +use Appwrite\Utopia\Response; use PHPMailer\PHPMailer\PHPMailer; const APP_NAME = 'Appwrite'; diff --git a/bin/start b/bin/start index 931aa101f6..4b0616978e 100755 --- a/bin/start +++ b/bin/start @@ -4,6 +4,8 @@ export PHP_VERSION=$PHP_VERSION chown -Rf www-data.www-data /usr/share/nginx/html/ +sed 's/%_APP_STORAGE_LIMIT%/'$_APP_STORAGE_LIMIT'/g' /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf + # Function to update the fpm configuration to make the service environment variables available function setEnvironmentVariable() { if [ -z "$2" ]; then diff --git a/composer.json b/composer.json index d8eeada93c..547cd84be5 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "ext-imagick": "*", "ext-mbstring": "*", "ext-json": "*", + "ext-yaml": "*", "ext-dom": "*", "ext-redis": "*", "ext-pdo": "*", diff --git a/docker-compose.yml b/docker-compose.yml index 095c896739..35ce7c28e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,7 +54,6 @@ services: - ./docs:/usr/share/nginx/html/docs - ./public:/usr/share/nginx/html/public - ./src:/usr/share/nginx/html/src - - ./docker/nginx.conf:/etc/nginx/nginx.conf depends_on: - mariadb - redis diff --git a/docker/nginx.conf b/docker/nginx.conf.template similarity index 98% rename from docker/nginx.conf rename to docker/nginx.conf.template index 7b62ec9a5b..f0f748d943 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf.template @@ -16,7 +16,7 @@ http { tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; - client_max_body_size 10M; + client_max_body_size %_APP_STORAGE_LIMIT%; # server_names_hash_bucket_size 64; # server_name_in_redirect off; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php new file mode 100644 index 0000000000..a0dff6e767 --- /dev/null +++ b/src/Appwrite/Utopia/Response.php @@ -0,0 +1,143 @@ +setModel(new Error()) + ->setModel(new ErrorDev()) + ->setModel(new User()) + ->setModel(new Locale()) + ; + } + + /** + * HTTP content types + */ + const CONTENT_TYPE_YAML = 'application/x-yaml'; + + /** + * List of defined output objects + */ + protected $models = []; + + /** + * Set Model Object + * + * @return self + */ + public function setModel(Model $instance): self + { + $this->models[$instance->getType()] = $instance; + + return $this; + } + + /** + * Get Model Object + * + * @return Model + */ + public function getModel(string $key): Model + { + if(!isset($this->models[$key])) { + throw new Exception('Undefined model: '.$key); + } + + return $this->models[$key]; + } + + /** + * Validate response objects and outputs + * the response according to given format type + */ + public function dynamic(Document $document, string $model) + { + $data = $document->getArrayCopy(); + $model = $this->getModel($model); + $output = []; + + foreach($model->getRules() as $key => $rule) { + if(!isset($data[$key])) { + if(!is_null($rule['default'])) { + $data[$key] = $rule['default']; + } + else { + throw new Exception('Missing response key: '.$key); + } + } + + if($rule['array'] && !is_array($data[$key])) { + throw new Exception($key.' must be an array of '.$rule['type'].' types'); + } + + $output[$key] = $data[$key]; + } + + return $this->json($output); + //return $this->yaml($output); + } + + /** + * YAML + * + * This helper is for sending YAML HTTP response. + * It sets relevant content type header ('application/x-yaml') and convert a PHP array ($data) to valid YAML using native yaml_parse + * + * @see https://en.wikipedia.org/wiki/YAML + * + * @param array $data + */ + public function yaml(array $data) + { + if(!extension_loaded('yaml')) { + throw new Exception('Missing yaml extension. Learn more at: https://www.php.net/manual/en/book.yaml.php'); + } + + $this + ->setContentType(Response::CONTENT_TYPE_YAML) + ->send(yaml_emit($data, YAML_UTF8_ENCODING)) + ; + } +} diff --git a/src/Appwrite/Utopia/Response/Model.php b/src/Appwrite/Utopia/Response/Model.php new file mode 100644 index 0000000000..b50224935b --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model.php @@ -0,0 +1,48 @@ +rules; + } + + /** + * Add a New Rule + */ + protected function addRule(string $key, array $options): self + { + $this->rules[$key] = array_merge([ + 'type' => '', + 'description' => '', + 'default' => null, + 'example' => '', + 'array' => false, + ], $options); + + return $this; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/Error.php b/src/Appwrite/Utopia/Response/Model/Error.php new file mode 100644 index 0000000000..95a4764d4a --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Error.php @@ -0,0 +1,50 @@ +addRule('message', [ + 'type' => 'string', + 'description' => 'Error message.', + 'example' => 'Not found', + ]) + ->addRule('code', [ + 'type' => 'string', + 'description' => 'Error code.', + 'example' => '404', + ]) + ->addRule('version', [ + 'type' => 'string', + 'description' => 'Server version number.', + 'example' => APP_VERSION_STABLE, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Error'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_ERROR; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/ErrorDev.php b/src/Appwrite/Utopia/Response/Model/ErrorDev.php new file mode 100644 index 0000000000..7069f2b11b --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ErrorDev.php @@ -0,0 +1,43 @@ +addRule('file', [ + 'type' => 'string', + 'description' => 'File path.', + 'example' => '/usr/share/nginx/html/vendor/utopia-php/framework/src/App.php', + ]) + ->addRule('line', [ + 'type' => 'integer', + 'description' => 'Line number.', + 'example' => 209, + ]) + // ->addRule('trace', [ + // 'type' => 'string', + // 'description' => 'Error trace.', + // 'example' => [ + // '' + // ], + // ]) + ; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_ERROR_DEV; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/File.php b/src/Appwrite/Utopia/Response/Model/File.php new file mode 100644 index 0000000000..875c6f4bd7 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/File.php @@ -0,0 +1,34 @@ +addRule('ip', [ + 'type' => 'string', + 'description' => 'User IP address.', + 'example' => '127.0.0.1', + ]) + ->addRule('countryCode', [ + 'type' => 'string', + 'description' => 'Country code in [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) two-character format', + 'example' => 'US', + ]) + ->addRule('country', [ + 'type' => 'string', + 'description' => 'Country name. This field support localization.', + 'example' => 'United States', + ]) + ->addRule('continentCode', [ + 'type' => 'string', + 'description' => 'Continent code. A two character continent code "AF" for Africa, "AN" for Antarctica, "AS" for Asia, "EU" for Europe, "NA" for North America, "OC" for Oceania, and "SA" for South America.', + 'example' => 'NA', + ]) + ->addRule('continent', [ + 'type' => 'string', + 'description' => 'Continent name. This field support localization.', + 'example' => 'North America', + ]) + ->addRule('eu', [ + 'type' => 'Boolean', + 'description' => 'True if country is part of the Europian Union.', + 'default' => false, + 'example' => false, + ]) + ->addRule('currency', [ + 'type' => 'string', + 'description' => 'ISO 4217 Email verification status.', + 'description' => 'Currency code in [ISO 4217-1](http://en.wikipedia.org/wiki/ISO_4217) three-character format', + 'example' => 'USD', + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Locale'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_LOCALE; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/Log.php b/src/Appwrite/Utopia/Response/Model/Log.php new file mode 100644 index 0000000000..93e7e88efd --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Log.php @@ -0,0 +1,33 @@ +addRule('$id', [ + 'type' => 'string', + 'description' => 'User ID.', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('name', [ + 'type' => 'string', + 'description' => 'User name.', + 'default' => '', + 'example' => 'John Doe', + ]) + ->addRule('registration', [ + 'type' => 'integer', + 'description' => 'User registration date in unix timestamp.', + 'default' => false, + 'example' => true, + ]) + ->addRule('status', [ + 'type' => 'integer', + 'description' => 'User status. 0 for Unavtivated, 1 for active and 2 is blocked.', + 'default' => false, + 'example' => true, + ]) + ->addRule('email', [ + 'type' => 'string', + 'description' => 'User email address.', + 'default' => '', + 'example' => 'john@appwrite.io', + ]) + ->addRule('emailVerification', [ + 'type' => 'boolean', + 'description' => 'Email verification status.', + 'default' => false, + 'example' => true, + ]) + ->addRule('prefs', [ + 'type' => 'json', + 'description' => 'User preferences as a key-value object', + 'default' => new \stdClass, + 'example' => ['theme' => 'dark', 'timezone' => 'UTC'], + ]) + ->addRule('roles', [ + 'type' => 'string', + 'description' => 'User list of roles', + 'default' => [], + 'example' => [], + 'array' => true, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'User'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_USER; + } +} \ No newline at end of file