diff --git a/public/index.php b/public/index.php index 041adc6..00ca34c 100644 --- a/public/index.php +++ b/public/index.php @@ -95,6 +95,36 @@ 'byIdRelationships', Phramework::METHOD_ANY ], + [ + 'user_data_template/', //URI + NS . 'UserDataTemplateController', //Class + 'GET', //Class method + Phramework::METHOD_GET, //HTTP Method + ], + [ + 'user_data_template/{id}', //URI + NS . 'UserDataTemplateController', //Class + 'GETById', //Class method + Phramework::METHOD_GET, //HTTP Method + ], + [ + 'user_data/', //URI + NS . 'UserDataController', //Class + 'GET', //Class method + Phramework::METHOD_GET, //HTTP Method + ], + [ + 'user_data/{id}', //URI + NS . 'UserDataController', //Class + 'GETById', //Class method + Phramework::METHOD_GET, //HTTP Method + ], + [ + 'user_data/', //URI + NS . 'UserDataController', //Class + 'POST', //Class method + Phramework::METHOD_POST, //HTTP Method + ], ]); //Initialize API with settings and routing @@ -109,6 +139,22 @@ Phramework::setViewer( \Phramework\JSONAPI\Viewers\JSONAPI::class ); + //Set authentication class + \Phramework\Authentication\Manager::register( + \Phramework\Authentication\BasicAuthentication\BasicAuthentication::class + ); + + //Set method to fetch user object, including password attribute + \Phramework\Authentication\Manager::setUserGetByEmailMethod( + [ + \Phramework\Examples\JSONAPI\Models\Administrator\Authentication::class, + 'getByEmailWithPassword' + ] + ); + + \Phramework\Authentication\Manager::setAttributes( + ['email'] + ); unset($settings); diff --git a/src/Controllers/UserDataController.php b/src/Controllers/UserDataController.php new file mode 100644 index 0000000..fd98058 --- /dev/null +++ b/src/Controllers/UserDataController.php @@ -0,0 +1,150 @@ + + */ +class UserDataController extends \Phramework\Examples\JSONAPI\Controller +{ + /** + * Get collection + * @param \stdClass $params Request parameters + * @param string $method Request method + * @param array $headers Request headers + */ + public static function GET( \stdClass $params, string $method, array $headers) + { + $user = Request::checkPermission(); + + static::handleGET( + $params, + UserData::class, + [$user->id], + [] + ); + } + + /** + * Get a resource + * @param \stdClass $params Request parameters + * @param string $method Request method + * @param array $headers Request headers + * @param string $id Resource id + */ + public static function GETById(\stdClass $params, string $method, array $headers, string $id) + { + $user = Request::checkPermission(); + + static::handleGETById( + $params, + $id, + UserData::class, + [$user->id], + [] + ); + } + + /** + * Post new resource + * @param \stdClass $params Request parameters + * @param string $method Request method + * @param array $headers Request headers + */ + public static function POST(\stdClass $params, string $method, array $headers) + { + $user = Request::checkPermission(); + + static::handlePOST( + $params, + $method, + $headers, + UserData::class, + [], + [], + [ + /** + * A validation callback to inject user id + */ + function ( + \stdClass $requestAttributes, + \stdClass $requestRelationships, + \stdClass $attributes, + \stdClass $parsedRelationshipAttributes + ) use ($user) { + $attributes->user_id = $user->id; // inject user id from authorization + $template = UserDataTemplate::getById($requestRelationships->template->data->id); // get template in order to create validator + $validator = new ObjectValidator(); + $templateJson = json_decode($template->attributes->value); + $validator = $validator->createFromObject($templateJson); // create validator + $attributes->value = $validator->parse($attributes->value); // validate data + } + ], + /** + * Override view, by setting a view callback + * to response with 204 and a Location header + */ + function (array $ids) { + //Prepare response with 201 Created status code and Location header + \Phramework\Models\Response::created( + UserData::getSelfLink($ids[0]) + ); + \Phramework\JSONAPI\Viewers\JSONAPI::header(); + //Will overwrite 201 with 204 status code + \Phramework\Models\Response::noContent(); + } + ); + } + + /** + * Manage resource's relationships + * `/article/{id}/relationships/{relationship}/` handler + * @param \stdClass $params Request parameters + * @param string $method Request method + * @param array $headers Request headers + * @param string $id Resource id + * @param string $relationship Relationship + */ + public static function byIdRelationships( + \stdClass $params, + string $method, + array $headers, + string $id, + string $relationship + ) { + static::handleByIdRelationships( + $params, + $method, + $headers, + $id, + $relationship, + UserData::class, + [Phramework::METHOD_GET], + [], + [] + ); + } +} diff --git a/src/Controllers/UserDataTemplateController.php b/src/Controllers/UserDataTemplateController.php new file mode 100644 index 0000000..37ea79e --- /dev/null +++ b/src/Controllers/UserDataTemplateController.php @@ -0,0 +1,65 @@ + + */ +class UserDataTemplateController extends \Phramework\Examples\JSONAPI\Controller +{ + /** + * Get collection + * @param \stdClass $params Request parameters + * @param string $method Request method + * @param array $headers Request headers + */ + public static function GET( \stdClass $params, string $method, array $headers) + { + static::handleGET( + $params, + UserDataTemplate::class, + [], + [] + ); + } + + /** + * Get a resource + * @param \stdClass $params Request parameters + * @param string $method Request method + * @param array $headers Request headers + * @param string $id Resource id + */ + public static function GETById( \stdClass $params, string $method, array $headers, string $id) + { + static::handleGETById( + $params, + $id, + UserDataTemplate::class, + [], + [] + ); + } + + +} diff --git a/src/Models/UserData.php b/src/Models/UserData.php new file mode 100644 index 0000000..ec711c7 --- /dev/null +++ b/src/Models/UserData.php @@ -0,0 +1,182 @@ + + */ +class UserData extends \Phramework\Examples\JSONAPI\Model +{ + protected static $type = 'user_data'; + protected static $endpoint = 'user_data'; + protected static $table = 'user_data'; + + /** + * @param Page $page + * @param Filter $filter + * @param Sort $sort + * @param Fields $fields + * @param mixed ...$additionalParameters + * @return Resource[] + */ + public static function get( + Page $page = null, + Filter $filter = null, + Sort $sort = null, + Fields $fields = null, + ...$additionalParameters + ) { + $query = static::handleGet( + 'SELECT + {{fields}} + FROM "user_data" + WHERE + "user_id" = ? + {{filter}} + {{sort}} + {{page}}', + $page, + $filter, + $sort, + $fields, + true + ); + + $records = Database::executeAndFetchAll( + $query, + [ $additionalParameters[0] ] + ); + + array_walk( + $records, + [static::class, 'prepareRecord'] + ); + + return static::collection($records, $fields); + } + + /** + * Defines model's validator for POST requests + * also may be used for PATCH requests and to validate filter directive values + * @return ValidationModel + */ + public static function getValidationModel() + { + return new ValidationModel( + new ObjectValidator( //attributes + (object) [ //properties + 'value' => new ObjectValidator((object)[], [], true), + ], + ['value'], //required attributes, + false //additional properties + ), + new ObjectValidator( //relationships + (object) [ + 'user' => User::getIdValidator(), + 'template'=> UserDataTemplate::getIdValidator(), + ], + ['template'], //required relationships, + false //additional properties + ) + ); + } + + /** + * @return string[] + */ + public static function getFields() + { + return ['user_id', 'user_data_template_id', 'value']; + } + + /** + * Override post behaviour, + * to encode json value to string + * @param \stdClass|array $attributes + * @return string + */ + public static function post( + $attributes, + $return = \Phramework\Database\Operations\Create::RETURN_ID + ) { + /* + * Work with object + */ + + if (is_array($attributes)) { + $attributes = (object) $attributes; + } + + if (isset($attributes->value)) { + $attributes->value = json_encode($attributes->value); + } + + return parent::post($attributes, $return); + } + + /** + * @return \stdClass + */ + public static function getRelationships() + { + return (object) [ + 'user' => new Relationship( + User::class, + Relationship::TYPE_TO_ONE, + 'user_id', //source data attribute + null, //source data callback + Relationship::FLAG_DEFAULT | Relationship::FLAG_DATA + ), + 'template' => new Relationship( + UserDataTemplate::class, + Relationship::TYPE_TO_ONE, + 'user_data_template_id', //source data attribute + null, + Relationship::FLAG_DEFAULT | Relationship::FLAG_DATA + ) + ]; + } + + /** + * Prepare records + * @param array $record Database record row + * @return array|null on failure + */ + protected static function prepareRecord(array &$record) + { + if (isset($record['value'])) { + $record['value'] = json_decode($record['value']); + } + } +} diff --git a/src/Models/UserDataTemplate.php b/src/Models/UserDataTemplate.php new file mode 100644 index 0000000..49fc5e7 --- /dev/null +++ b/src/Models/UserDataTemplate.php @@ -0,0 +1,94 @@ + + */ +class UserDataTemplate extends \Phramework\Examples\JSONAPI\Model +{ + protected static $type = 'user_data_template'; + protected static $endpoint = 'user_data_template'; + protected static $table = 'user_data_template'; + + /** + * @param Page $page + * @param Filter $filter + * @param Sort $sort + * @param Fields $fields + * @param mixed ...$additionalParameters + * @return Resource[] + */ + public static function get( + Page $page = null, + Filter $filter = null, + Sort $sort = null, + Fields $fields = null, + ...$additionalParameters + ) { + $query = static::handleGet( + 'SELECT + {{fields}} + FROM "user_data_template" + {{filter}} + {{sort}} + {{page}}', + $page, + $filter, + $sort, + $fields, + false + ); + + $records = Database::executeAndFetchAll( + $query, + [ ] + ); + + array_walk( + $records, + [static::class, 'prepareRecord'] + ); + + return static::collection($records, $fields); + } + + /** + * @return string[] + */ + public static function getFields() + { + return ['value']; + } + +} diff --git a/tests/testphase/userData/get-byid-not-found.json b/tests/testphase/userData/get-byid-not-found.json new file mode 100644 index 0000000..aba456c --- /dev/null +++ b/tests/testphase/userData/get-byid-not-found.json @@ -0,0 +1,22 @@ +{ + "meta": { + "order": 122 + }, + "request": { + "url": "user_data/333", + "method": "GET", + "headers": [ + "{{{headerRequestAccept}}}", + "{{{headerRequestContentType}}}", + "{{{headerRequestAuthorization}}}" + ] + }, + "response": { + "statusCode": 404, + "headers": { + "Content-Type": "{{{headerResponseContentType}}}" + }, + "ruleObjects": [ + ] + } +} \ No newline at end of file diff --git a/tests/testphase/userData/get-byid.json b/tests/testphase/userData/get-byid.json new file mode 100644 index 0000000..4fb1363 --- /dev/null +++ b/tests/testphase/userData/get-byid.json @@ -0,0 +1,23 @@ +{ + "meta": { + "order": 121 + }, + "request": { + "url": "user_data/{{userDataId1}}", + "method": "GET", + "headers": [ + "{{{headerRequestAccept}}}", + "{{{headerRequestContentType}}}", + "{{{headerRequestAuthorization}}}" + ] + }, + "response": { + "statusCode": 200, + "headers": { + "Content-Type": "{{{headerResponseContentType}}}" + }, + "ruleObjects": [ + "{{{responseBodyJsonapiResource}}}" + ] + } +} \ No newline at end of file diff --git a/tests/testphase/userData/get.json b/tests/testphase/userData/get.json new file mode 100644 index 0000000..70ad7fb --- /dev/null +++ b/tests/testphase/userData/get.json @@ -0,0 +1,26 @@ +{ + "meta": { + "order": 120 + }, + "request": { + "url": "user_data/", + "method": "GET", + "headers": [ + "{{{headerRequestAccept}}}", + "{{{headerRequestContentType}}}", + "{{{headerRequestAuthorization}}}" + ] + }, + "response": { + "statusCode": 200, + "headers": { + "Content-Type": "{{{headerResponseContentType}}}" + }, + "ruleObjects": [ + "{{{responseBodyJsonapiCollection}}}" + ], + "export": { + "userDataId1": "data.id" + } + } +} \ No newline at end of file diff --git a/tests/testphase/userData/post-failure-missing-template_id.json b/tests/testphase/userData/post-failure-missing-template_id.json new file mode 100644 index 0000000..ce0d53f --- /dev/null +++ b/tests/testphase/userData/post-failure-missing-template_id.json @@ -0,0 +1,26 @@ +{ + "meta": { + "order": 124, + "description": "Attempt to create a new user data, expecting exception since body is without a \"user data template \" relationship" + }, + "request": { + "url": "user_data", + "method": "POST", + "headers": [ + "{{{headerRequestAccept}}}", + "{{{headerRequestContentType}}}", + "{{{headerRequestAuthorization}}}" + ], + "body": { + "data": { + "type": "user_data", + "attributes": { + "value": {"type":"object","properties":{"platform":{"type":"enum","enum":["www","ios","android"],"title":"Your platform","description":"Tell us your platform","meta":{"display":"radio","enum-titles":{"www":"Web browser","android":"Android"}}}}} + } + } + } + }, + "response": { + "statusCode": 422 + } +} \ No newline at end of file diff --git a/tests/testphase/userData/post-failure-template-not-found.json b/tests/testphase/userData/post-failure-template-not-found.json new file mode 100644 index 0000000..cabf7ee --- /dev/null +++ b/tests/testphase/userData/post-failure-template-not-found.json @@ -0,0 +1,34 @@ +{ + "meta": { + "order": 125, + "description": "Attempt to create a new user data, expecting exception since using non-existing user_data_template" + }, + "request": { + "url": "user_data", + "method": "POST", + "headers": [ + "{{{headerRequestAccept}}}", + "{{{headerRequestContentType}}}", + "{{{headerRequestAuthorization}}}" + ], + "body": { + "data": { + "type": "user_data", + "attributes": { + "value": {"type":"object","properties":{"platform":{"type":"enum","enum":["www","ios","android"],"title":"Your platform","description":"Tell us your platform","meta":{"display":"radio","enum-titles":{"www":"Web browser","android":"Android"}}}}} + }, + "relationships": { + "template": { + "data": { + "id": "1234567890", + "type": "user_data_template" + } + } + } + } + } + }, + "response": { + "statusCode": 404 + } +} \ No newline at end of file diff --git a/tests/testphase/userData/post.json b/tests/testphase/userData/post.json new file mode 100644 index 0000000..ae7dfbc --- /dev/null +++ b/tests/testphase/userData/post.json @@ -0,0 +1,34 @@ +{ + "meta": { + "order": 123, + "description": "Attempt to create a new user data successfully" + }, + "request": { + "url": "user_data", + "method": "POST", + "headers": [ + "{{{headerRequestAccept}}}", + "{{{headerRequestContentType}}}", + "{{{headerRequestAuthorization}}}" + ], + "body": { + "data": { + "type": "user_data", + "attributes": { + "value": {"type":"object","properties":{"platform":{"type":"enum","enum":["www","ios","android"],"title":"Your platform","description":"Tell us your platform","meta":{"display":"radio","enum-titles":{"www":"Web browser","android":"Android"}}}}} + }, + "relationships": { + "template": { + "data": { + "id": "{{templateId1}}", + "type": "user_data_template" + } + } + } + } + } + }, + "response": { + "statusCode": 204 + } +} \ No newline at end of file diff --git a/tests/testphase/userDataTemplate/get-byid.json b/tests/testphase/userDataTemplate/get-byid.json new file mode 100644 index 0000000..0d7380f --- /dev/null +++ b/tests/testphase/userDataTemplate/get-byid.json @@ -0,0 +1,22 @@ +{ + "meta": { + "order": 101 + }, + "request": { + "url": "user_data_template/{{templateId1}}", + "method": "GET", + "headers": [ + "{{{headerRequestAccept}}}", + "{{{headerRequestContentType}}}" + ] + }, + "response": { + "statusCode": 200, + "headers": { + "Content-Type": "{{{headerResponseContentType}}}" + }, + "ruleObjects": [ + "{{{responseBodyJsonapiResource}}}" + ] + } +} \ No newline at end of file diff --git a/tests/testphase/userDataTemplate/get.json b/tests/testphase/userDataTemplate/get.json new file mode 100644 index 0000000..4e5526a --- /dev/null +++ b/tests/testphase/userDataTemplate/get.json @@ -0,0 +1,25 @@ +{ + "meta": { + "order": 100 + }, + "request": { + "url": "user_data_template/", + "method": "GET", + "headers": [ + "{{{headerRequestAccept}}}", + "{{{headerRequestContentType}}}" + ] + }, + "response": { + "statusCode": 200, + "headers": { + "Content-Type": "{{{headerResponseContentType}}}" + }, + "ruleObjects": [ + "{{{responseBodyJsonapiCollection}}}" + ], + "export": { + "templateId1": "data.id" + } + } +} \ No newline at end of file diff --git a/tools/database.php b/tools/database.php index 9a31690..f3028c5 100644 --- a/tools/database.php +++ b/tools/database.php @@ -62,6 +62,22 @@ )' ); +$adapter->execute( + 'CREATE TABLE `user_data_template`( + `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + `value` TEXT + )' +); + +$adapter->execute( + 'CREATE TABLE `user_data`( + `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + `user_id` INTEGER, + `user_data_template_id` INTEGER, + `value` TEXT + )' +); + /* * Define lists of records to be inserted */ @@ -110,7 +126,37 @@ [1, 2, 1], [2, 1, 1] ]; +$user_data_templates = [ + [ + 1, + '{"type":"object","properties":{"platform":{"type":"enum","enum":["www","ios","android"],"title":"Your platform","description":"Tell us your platform","meta":{"display":"radio","enum-titles":{"www":"Web browser","android":"Android"}}}}}', + ], + [ + 2, + '{"type":"object","properties":{"platform":{"type":"enum","enum":["www","ios","android"],"title":"Your platform","description":"Tell us your platform","meta":{"display":"radio","enum-titles":{"www":"Web browser","android":"Android"}}}}}', + ], +]; +$user_data_entries = [ + [ + 1, + 1, + 1, + '{"platform":"www"}', + ], + [ + 2, + 1, + 2, + '{"platform":"ios"}', + ], + [ + 3, + 2, + 2, + '{"platform":"ios"}', + ], +]; /* * Insert user records @@ -160,6 +206,30 @@ ); } +/* + * Insert user_data_template records + */ +foreach ($user_data_templates as $user_data_template) { + $adapter->execute( + 'INSERT INTO `user_data_template` + (`id`, `value`) + VALUES (?, ?)', + $user_data_template + ); +} + +/* + * Insert user_data_template records + */ +foreach ($user_data_entries as $user_data) { + $adapter->execute( + 'INSERT INTO `user_data` + (`id`, `user_id`, `user_data_template_id`, `value`) + VALUES (?, ?, ?, ?)', + $user_data + ); +} + /* * Close connection */