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
|
## Supported operations
|
||||||
|
|
||||||
- `GET /api/v1/(ClassName)/(ID)` - gets a database record
|
- `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);
|
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();
|
$fieldValue = $obj->obj($fieldName)->forTemplate();
|
||||||
$serobj->$fieldName = $fieldValue;
|
$mappedFieldName = $this->getFieldAlias($className, $fieldName);
|
||||||
|
$serobj->$mappedFieldName = $fieldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->relationDepth > 0) {
|
if ($this->relationDepth > 0) {
|
||||||
|
@ -137,7 +137,8 @@ class XMLDataFormatter extends DataFormatter
|
|||||||
} else {
|
} else {
|
||||||
$fieldValue = Convert::raw2xml($fieldValue);
|
$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());
|
$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
|
||||||
|
|
||||||
$rawFields = $this->request->getVar('fields');
|
$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) {
|
if ($obj instanceof SS_List) {
|
||||||
$objs = ArrayList::create($obj->toArray());
|
$objs = ArrayList::create($obj->toArray());
|
||||||
@ -347,10 +348,12 @@ class RestfulServer extends Controller
|
|||||||
|
|
||||||
// set custom fields
|
// set custom fields
|
||||||
if ($customAddFields = $this->request->getVar('add_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')) {
|
if ($customFields = $this->request->getVar('fields')) {
|
||||||
$formatter->setCustomFields(explode(',', $customFields));
|
$customFields = $formatter->getRealFields($className, explode(',', $customFields));
|
||||||
|
$formatter->setCustomFields($customFields);
|
||||||
}
|
}
|
||||||
$formatter->setCustomRelations($this->getAllowedRelations($className));
|
$formatter->setCustomRelations($this->getAllowedRelations($className));
|
||||||
|
|
||||||
@ -495,6 +498,13 @@ class RestfulServer extends Controller
|
|||||||
return $this->notFound();
|
return $this->notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$reqFormatter = $this->getRequestDataFormatter($className);
|
||||||
|
if (!$reqFormatter) {
|
||||||
|
return $this->unsupportedMediaType();
|
||||||
|
}
|
||||||
|
|
||||||
|
$relation = $reqFormatter->getRealFieldName($className, $relation);
|
||||||
|
|
||||||
if (!$obj->hasMethod($relation)) {
|
if (!$obj->hasMethod($relation)) {
|
||||||
return $this->notFound();
|
return $this->notFound();
|
||||||
}
|
}
|
||||||
@ -573,16 +583,23 @@ class RestfulServer extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($body)) {
|
if (!empty($body)) {
|
||||||
$data = $formatter->convertStringToArray($body);
|
$rawdata = $formatter->convertStringToArray($body);
|
||||||
} else {
|
} else {
|
||||||
// assume application/x-www-form-urlencoded which is automatically parsed by PHP
|
// 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
|
// @todo Disallow editing of certain keys in database
|
||||||
$data = array_diff_key($data, ['ID', 'Created']);
|
$data = array_diff_key($data, ['ID', 'Created']);
|
||||||
|
|
||||||
$className = $this->unsanitiseClassName($this->request->param('ClassName'));
|
|
||||||
$apiAccess = singleton($className)->config()->api_access;
|
$apiAccess = singleton($className)->config()->api_access;
|
||||||
if (is_array($apiAccess) && isset($apiAccess['edit'])) {
|
if (is_array($apiAccess) && isset($apiAccess['edit'])) {
|
||||||
$data = array_intersect_key($data, array_combine($apiAccess['edit'], $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\Dev\SapphireTest;
|
||||||
use SilverStripe\RestfulServer\DataFormatter\JSONDataFormatter;
|
use SilverStripe\RestfulServer\DataFormatter\JSONDataFormatter;
|
||||||
use Page;
|
use Page;
|
||||||
|
use SilverStripe\Core\Config\Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -113,6 +114,18 @@ class RestfulServerTest extends SapphireTest
|
|||||||
unset($_SERVER['PHP_AUTH_PW']);
|
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()
|
public function testAuthenticatedPUT()
|
||||||
{
|
{
|
||||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
@ -160,6 +173,28 @@ class RestfulServerTest extends SapphireTest
|
|||||||
$this->assertContains($rating2->ID, $ratingIDs);
|
$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()
|
public function testGETManyManyRelationshipsXML()
|
||||||
{
|
{
|
||||||
// author4 has related authors author2 and author3
|
// author4 has related authors author2 and author3
|
||||||
@ -395,6 +430,17 @@ class RestfulServerTest extends SapphireTest
|
|||||||
$this->assertContains('<Rating>' . $rating1->Rating . '</Rating>', $response->getBody());
|
$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()
|
public function testApiAccessFieldRestrictions()
|
||||||
{
|
{
|
||||||
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||||
@ -500,6 +546,23 @@ class RestfulServerTest extends SapphireTest
|
|||||||
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
|
$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()
|
public function testJSONDataFormatter()
|
||||||
{
|
{
|
||||||
$formatter = new JSONDataFormatter();
|
$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()
|
public function testApiAccessWithPOST()
|
||||||
{
|
{
|
||||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||||
@ -542,6 +628,19 @@ class RestfulServerTest extends SapphireTest
|
|||||||
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
|
$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()
|
public function testCanViewRespectedInList()
|
||||||
{
|
{
|
||||||
// Default content type
|
// Default content type
|
||||||
|
Loading…
Reference in New Issue
Block a user