mirror of
https://github.com/silverstripe/silverstripe-restfulserver
synced 2024-10-22 14:05:58 +02:00
Merge pull request #53 from silverstripe-terraformers/feature/api-field-mapping
Add field mapping config
This commit is contained in:
commit
4581fbf479
38
README.md
38
README.md
@ -59,6 +59,44 @@ class Article extends DataObject {
|
||||
}
|
||||
```
|
||||
|
||||
Example DataObject field mapping, allows aliasing fields so that public requests and responses display different field names:
|
||||
|
||||
```php
|
||||
namespace Vendor\Project;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Article extends DataObject {
|
||||
|
||||
private static $db = [
|
||||
'Title'=>'Text',
|
||||
'Published'=>'Boolean'
|
||||
];
|
||||
|
||||
private static $api_access = [
|
||||
'view' => ['Title', 'Content'],
|
||||
];
|
||||
|
||||
private static $api_field_mapping = [
|
||||
'customTitle' => 'Title',
|
||||
];
|
||||
}
|
||||
```
|
||||
Given a dataobject with values:
|
||||
```yml
|
||||
ID: 12
|
||||
Title: Title Value
|
||||
Content: Content value
|
||||
```
|
||||
which when requesting with the url `/api/v1/Vendor-Project-Article/12?fields=customTitle,Content` and `Accept: application/json` the response will look like:
|
||||
```Javascript
|
||||
{
|
||||
"customTitle": "Title Value",
|
||||
"Content": "Content value"
|
||||
}
|
||||
```
|
||||
Similarly, `PUT` or `POST` requests will have fields transformed from the alias name to the DB field name.
|
||||
|
||||
## Supported operations
|
||||
|
||||
- `GET /api/v1/(ClassName)/(ID)` - gets a database record
|
||||
|
@ -373,4 +373,87 @@ abstract class DataFormatter
|
||||
{
|
||||
user_error('DataFormatter::convertStringToArray not implemented on subclass', E_USER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of aliased field names to their Dataobject field name
|
||||
*
|
||||
* @param string $className
|
||||
* @param string[] $fields
|
||||
* @return string[]
|
||||
*/
|
||||
public function getRealFields($className, $fields)
|
||||
{
|
||||
$apiMapping = $this->getApiMapping($className);
|
||||
if (is_array($apiMapping) && is_array($fields)) {
|
||||
$mappedFields = [];
|
||||
foreach ($fields as $field) {
|
||||
$mappedFields[] = $this->getMappedKey($apiMapping, $field);
|
||||
}
|
||||
return $mappedFields;
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DataObject field name from its alias
|
||||
*
|
||||
* @param string $className
|
||||
* @param string $field
|
||||
* @return string
|
||||
*/
|
||||
public function getRealFieldName($className, $field)
|
||||
{
|
||||
$apiMapping = $this->getApiMapping($className);
|
||||
return $this->getMappedKey($apiMapping, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a DataObject Field's Alias
|
||||
* defaults to the fieldname
|
||||
*
|
||||
* @param string $className
|
||||
* @param string $field
|
||||
* @return string
|
||||
*/
|
||||
public function getFieldAlias($className, $field)
|
||||
{
|
||||
$apiMapping = $this->getApiMapping($className);
|
||||
$apiMapping = array_flip($apiMapping);
|
||||
return $this->getMappedKey($apiMapping, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 'api_field_mapping' config value for a class
|
||||
* or return an empty array
|
||||
*
|
||||
* @param string $className
|
||||
* @return string[]|array
|
||||
*/
|
||||
protected function getApiMapping($className)
|
||||
{
|
||||
$apiMapping = Config::inst()->get($className, 'api_field_mapping');
|
||||
if ($apiMapping && is_array($apiMapping)) {
|
||||
return $apiMapping;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get mapped field names
|
||||
*
|
||||
* @param array $map
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function getMappedKey($map, $key)
|
||||
{
|
||||
if (is_array($map)) {
|
||||
if (array_key_exists($key, $map)) {
|
||||
return $map[$key];
|
||||
} else {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,8 @@ class JSONDataFormatter extends DataFormatter
|
||||
}
|
||||
|
||||
$fieldValue = $obj->obj($fieldName)->forTemplate();
|
||||
$serobj->$fieldName = $fieldValue;
|
||||
$mappedFieldName = $this->getFieldAlias($className, $fieldName);
|
||||
$serobj->$mappedFieldName = $fieldValue;
|
||||
}
|
||||
|
||||
if ($this->relationDepth > 0) {
|
||||
|
@ -137,7 +137,8 @@ class XMLDataFormatter extends DataFormatter
|
||||
} else {
|
||||
$fieldValue = Convert::raw2xml($fieldValue);
|
||||
}
|
||||
$xml .= "<$fieldName>$fieldValue</$fieldName>\n";
|
||||
$mappedFieldName = $this->getFieldAlias(get_class($obj), $fieldName);
|
||||
$xml .= "<$mappedFieldName>$fieldValue</$mappedFieldName>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -259,7 +259,8 @@ class RestfulServer extends Controller
|
||||
$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
|
||||
|
||||
$rawFields = $this->request->getVar('fields');
|
||||
$fields = $rawFields ? explode(',', $rawFields) : null;
|
||||
$realFields = $responseFormatter->getRealFields($className, explode(',', $rawFields));
|
||||
$fields = $rawFields ? $realFields : null;
|
||||
|
||||
if ($obj instanceof SS_List) {
|
||||
$objs = ArrayList::create($obj->toArray());
|
||||
@ -347,10 +348,12 @@ class RestfulServer extends Controller
|
||||
|
||||
// set custom fields
|
||||
if ($customAddFields = $this->request->getVar('add_fields')) {
|
||||
$formatter->setCustomAddFields(explode(',', $customAddFields));
|
||||
$customAddFields = $formatter->getRealFields($className, explode(',', $customAddFields));
|
||||
$formatter->setCustomAddFields($customAddFields);
|
||||
}
|
||||
if ($customFields = $this->request->getVar('fields')) {
|
||||
$formatter->setCustomFields(explode(',', $customFields));
|
||||
$customFields = $formatter->getRealFields($className, explode(',', $customFields));
|
||||
$formatter->setCustomFields($customFields);
|
||||
}
|
||||
$formatter->setCustomRelations($this->getAllowedRelations($className));
|
||||
|
||||
@ -495,6 +498,13 @@ class RestfulServer extends Controller
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$reqFormatter = $this->getRequestDataFormatter($className);
|
||||
if (!$reqFormatter) {
|
||||
return $this->unsupportedMediaType();
|
||||
}
|
||||
|
||||
$relation = $reqFormatter->getRealFieldName($className, $relation);
|
||||
|
||||
if (!$obj->hasMethod($relation)) {
|
||||
return $this->notFound();
|
||||
}
|
||||
@ -573,16 +583,23 @@ class RestfulServer extends Controller
|
||||
}
|
||||
|
||||
if (!empty($body)) {
|
||||
$data = $formatter->convertStringToArray($body);
|
||||
$rawdata = $formatter->convertStringToArray($body);
|
||||
} else {
|
||||
// assume application/x-www-form-urlencoded which is automatically parsed by PHP
|
||||
$data = $this->request->postVars();
|
||||
$rawdata = $this->request->postVars();
|
||||
}
|
||||
|
||||
$className = $this->unsanitiseClassName($this->request->param('ClassName'));
|
||||
// update any aliased field names
|
||||
$data = [];
|
||||
foreach ($rawdata as $key => $value) {
|
||||
$newkey = $formatter->getRealFieldName($className, $key);
|
||||
$data[$newkey] = $value;
|
||||
}
|
||||
|
||||
// @todo Disallow editing of certain keys in database
|
||||
$data = array_diff_key($data, ['ID', 'Created']);
|
||||
|
||||
$className = $this->unsanitiseClassName($this->request->param('ClassName'));
|
||||
$apiAccess = singleton($className)->config()->api_access;
|
||||
if (is_array($apiAccess) && isset($apiAccess['edit'])) {
|
||||
$data = array_intersect_key($data, array_combine($apiAccess['edit'], $apiAccess['edit']));
|
||||
|
@ -17,6 +17,7 @@ use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\RestfulServer\DataFormatter\JSONDataFormatter;
|
||||
use Page;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -113,6 +114,18 @@ class RestfulServerTest extends SapphireTest
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testGETWithFieldAlias()
|
||||
{
|
||||
Config::inst()->set(RestfulServerTestAuthorRating::class, 'api_field_mapping', ['rate' => 'Rating']);
|
||||
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals(3, $responseArr['rate']);
|
||||
}
|
||||
|
||||
public function testAuthenticatedPUT()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
@ -160,6 +173,28 @@ class RestfulServerTest extends SapphireTest
|
||||
$this->assertContains($rating2->ID, $ratingIDs);
|
||||
}
|
||||
|
||||
public function testGETRelationshipsWithAlias()
|
||||
{
|
||||
// Alias do not currently work with Relationships
|
||||
Config::inst()->set(RestfulServerTestAuthor::class, 'api_field_mapping', ['stars' => 'Ratings']);
|
||||
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||
|
||||
// @todo should be set up by fixtures, doesn't work for some reason...
|
||||
$author1->Ratings()->add($rating1);
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID . '?add_fields=stars';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$xmlTagSafeClassName = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
|
||||
$this->assertTrue(array_key_exists('Ratings', $responseArr));
|
||||
$this->assertFalse(array_key_exists('stars', $responseArr));
|
||||
}
|
||||
|
||||
public function testGETManyManyRelationshipsXML()
|
||||
{
|
||||
// author4 has related authors author2 and author3
|
||||
@ -395,6 +430,17 @@ class RestfulServerTest extends SapphireTest
|
||||
$this->assertContains('<Rating>' . $rating1->Rating . '</Rating>', $response->getBody());
|
||||
}
|
||||
|
||||
public function testXMLValueFormattingWithFieldAlias()
|
||||
{
|
||||
Config::inst()->set(RestfulServerTestAuthorRating::class, 'api_field_mapping', ['rate' => 'Rating']);
|
||||
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertContains('<rate>' . $rating1->Rating . '</rate>', $response->getBody());
|
||||
}
|
||||
|
||||
public function testApiAccessFieldRestrictions()
|
||||
{
|
||||
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||
@ -500,6 +546,23 @@ class RestfulServerTest extends SapphireTest
|
||||
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
|
||||
}
|
||||
|
||||
public function testFieldAliasWithPUT()
|
||||
{
|
||||
Config::inst()->set(RestfulServerTestAuthorRating::class, 'api_field_mapping', ['rate' => 'Rating']);
|
||||
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||
// Test input with original fieldname
|
||||
$data = array(
|
||||
'Rating' => '42',
|
||||
);
|
||||
$response = Director::test($url, $data, null, 'PUT');
|
||||
// Assumption: XML is default output
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
// should output with aliased name
|
||||
$this->assertEquals(42, $responseArr['rate']);
|
||||
}
|
||||
|
||||
public function testJSONDataFormatter()
|
||||
{
|
||||
$formatter = new JSONDataFormatter();
|
||||
@ -527,6 +590,29 @@ class RestfulServerTest extends SapphireTest
|
||||
);
|
||||
}
|
||||
|
||||
public function testJSONDataFormatterWithFieldAlias()
|
||||
{
|
||||
Config::inst()->set(Member::class, 'api_field_mapping', ['MyName' => 'FirstName']);
|
||||
$formatter = new JSONDataFormatter();
|
||||
$editor = $this->objFromFixture(Member::class, 'editor');
|
||||
$user = $this->objFromFixture(Member::class, 'user');
|
||||
|
||||
// The DataFormatter performs canView calls
|
||||
// these are `Member`s so we need to be ADMIN types
|
||||
$this->logInWithPermission('ADMIN');
|
||||
|
||||
$set = Member::get()
|
||||
->filter('ID', [$editor->ID, $user->ID])
|
||||
->sort('"Email" ASC'); // for sorting for postgres
|
||||
|
||||
$this->assertEquals(
|
||||
'{"totalSize":null,"items":[{"MyName":"Editor","Email":"editor@test.com"},' .
|
||||
'{"MyName":"User","Email":"user@test.com"}]}',
|
||||
$formatter->convertDataObjectSet($set, ["FirstName", "Email"]),
|
||||
"Correct JSON formatting with field alias"
|
||||
);
|
||||
}
|
||||
|
||||
public function testApiAccessWithPOST()
|
||||
{
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
@ -542,6 +628,19 @@ class RestfulServerTest extends SapphireTest
|
||||
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
|
||||
}
|
||||
|
||||
public function testFieldAliasWithPOST()
|
||||
{
|
||||
Config::inst()->set(RestfulServerTestAuthorRating::class, 'api_field_mapping', ['rate' => 'Rating']);
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/";
|
||||
$data = [
|
||||
'rate' => '42',
|
||||
];
|
||||
$response = Director::test($url, $data, null, 'POST');
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals(42, $responseArr['rate']);
|
||||
}
|
||||
|
||||
public function testCanViewRespectedInList()
|
||||
{
|
||||
// Default content type
|
||||
|
Loading…
Reference in New Issue
Block a user