React-admin: specify ID name and type

Created on 26 Sep 2016  路  19Comments  路  Source: marmelab/react-admin

Hi,

First let me say that this tool is awesome.
Looks great and has introduced me to React as well.

I have a few issues / questions.. I'm not sure where to put them and if they should be separate threads. Let me know if I should be doing things differently.

  1. I have an existing service that has a row ID with a different name (EG _id) and the value is a string instead of a number (think of a GUID). The datagrid component looks to have the field and type specific to "id" as an Integer. Would I be able to over ride this?
  2. I have a reference field from another object/service. The existing create/update service requires the data to be embedded rather than referenced. I was going to update the convertRESTRequestToHTTP to embed the data but I wasn't able to see a way to access existing resources/objects from another service.
    2.1 Is there a way to specify inline/embed rather than reference
    2.2 Or is there an obvious way to get access to other objects/resources so that I can embed with my own custom rest client in convertRESTRequestToHTTP ?

Thanks!!

Most helpful comment

Coming back to my question, I'd like to answer it myself, for people coming from search-engines 馃

Rest clients are now called dataProvider https://marmelab.com/react-admin/DataProviders.html.
The documentation gives examples on what a dataProvider script/function should include.

Basically, what the exported function in the dataProvider is supposed to do is map react-admin's internal requests for specific resources to a URL at your REST service and then map the response of the service to a format the internal store understands.

To create a dataProvider, start by creating a dataProvider.js and reference it in your App.js like so:

import jsonServerProvider from './dataProvider';
const dataProvider = jsonServerProvider('https://example.org/rest-api');
const App = () => (
    <Admin dataProvider={dataProvider}>
...
        </Admin>
};

All 19 comments

Hi !

And thanks for your feedback.

  1. you'll have to map your id name with id in the REST client. The name is compulsory. As for ythe type, it shouldn't be integer-only. the fact that it currently is is a bug (cf #7)
  2. The REST client does whatever you want in to do ; it's pure JS. if you need to aggregate multiple API endpoints before giving back the control to admin-on-rest, feel free to do it.

2.1 Embedded documents are not yet supported, but they should be fairly easy to deal with. You'll need to write your own Field and Input components though.

Hi, Francois - thanks for the quick response!

  1. Sorry I should have seen the the previous issue!
  2. Is there an existing object reference or method/class I can call to re-use the data that's already present? EG a reference from the last REST call. That way I'm dealing with an API rather than making separate AJAX calls to the same services and it ensures the data is in sync with the UI, and re-use caching or anything that may be implemented in the future.

EG (using posts and comments as the example - embedding comments with posts)
if (resource === "posts")
comments = commentsList.getById(resourceId)

No, if you need some form of intermediate persistence / caching, you'll have to implement it yourself in the REST client. Alternatively, you could put some of this logic in a custom saga.

Thanks for clarifying. Perhaps it's an item for a feature request

@fzaninotto Could you please give more details about how to map our "_id" and "id" in our admin-on-rest?

How about something like that:

    const convertHTTPResponseToREST = (response, type, resource, params) => {
        const { headers, json } = response;
        switch (type) {
        case GET_LIST:
        case GET_MANY_REFERENCE:
            if (!headers.has('content-range')) {
                throw new Error('The Content-Range header is missing in the HTTP Response. The simple REST client expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?');
            }
            return {
                data: json.map(record => ({ id: record._id, ...record })),
                total: parseInt(headers.get('content-range').split('/').pop(), 10),
            };
        case CREATE:
            return { data: { id: json._id, ...params.data } };
        default:
            return { data: { id: json._id, ...json } };
        }
    };

Added to the FAQ: https://marmelab.com/admin-on-rest/FAQ.html#can-i-have-custom-identifiers-primary-keys-for-my-resources

Excuse my maybe dumb question, but how do I actually get to a custom rest client? I mean everything is in place, I don't want to reinvent the wheel, just map the primary key. My main question being: Where should I put the code displayed here: https://marmelab.com/admin-on-rest/FAQ.html#can-i-have-custom-identifiers-primary-keys-for-my-resources?

Coming back to my question, I'd like to answer it myself, for people coming from search-engines 馃

Rest clients are now called dataProvider https://marmelab.com/react-admin/DataProviders.html.
The documentation gives examples on what a dataProvider script/function should include.

Basically, what the exported function in the dataProvider is supposed to do is map react-admin's internal requests for specific resources to a URL at your REST service and then map the response of the service to a format the internal store understands.

To create a dataProvider, start by creating a dataProvider.js and reference it in your App.js like so:

import jsonServerProvider from './dataProvider';
const dataProvider = jsonServerProvider('https://example.org/rest-api');
const App = () => (
    <Admin dataProvider={dataProvider}>
...
        </Admin>
};

@te-online thanks for that heads up on the documentation.

I actually got pretty far into building a demo app that worked with a real server to CRUD objects against SQL via an auto-generated data access layer, when I found the subtle requirement that your key has to be "id" to use the ra-data-simple-rest data provider.

I guess in their latest parlance I need to write some sort of data adapter that changes "payload" for the query for those types to use the key name I need?

It seems like it would be much LESS of a headache and less work to supply the data provider some sort of key-value JSON blob that defines the key map overrides by their type, in a simple init method option.

That's literally all I need, as it wasn't too much work to create the server-side API that conforms to the client's needs otherwise.

Without this, it seems I either have to go touch a lot of files (4 per data type on the server side) to fudge an id alias property, or I have to go extend the data provider to add a lot of wrapping calls just to get it to use a different key name for queries. Too bad, this tool otherwise really lowers the bar for adding a CRUD interface for an existing system. I have 250+ types of objects I have to touch...That's a lot of work.

Someone please tell me I'm missing something?

So I started into what I was proposing above, adding another argument to the constructor to ra-data-simple-rest import call to specify a blob of key names by resource.

What I quickly found is that unfortunately the ra-core itself builds the payloads and has a pretty tight dependency on key names being "id." It seems baked in the module at multiple levels, unfortunately.

In other words, there is a tight/hard API dependency between ra-core and anything that would work with it, not just at the data provider, that it plays by ra-core's rules - which is that 'id' is your id parameter.

I'm not sure if I understand your problem. But I've used react-admin with REST APIs that use uuid instead of id.

// dataProvider.js
// ... imports
export default (apiUrls, httpClient = fetchUtils.fetchJson) => {
        // ...
    const convertHTTPResponse = (response, type, resource, params) => {
        const { headers, json } = response;
        switch (type) {
            case GET_LIST:
            case GET_MANY_REFERENCE:
                // Apply transformations
                if (json.data) {
                    json.data = json.data.map((document) => {
                        return applyDocumentFilters(document);
                    });
                }
                if (!headers.has('x-total-count')) {
                    throw new Error(
                        'The X-Total-Count header is missing in the HTTP Response. The jsonServer Data Provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare X-Total-Count in the Access-Control-Expose-Headers header?'
                    );
                }

                return {
                    data: json.data,
                    total: parseInt(headers.get('x-total-count').split('/').pop(), 10)
                };
            case CREATE:
                return { data: { ...params.data, id: json.data.uuid } };
            default:
                if (json.data && json.data.length > 0) {
                    // Apply transformations
                    json.data = json.data.map((document) => {
                        return applyDocumentFilters(document);
                    });
                    return { data: json.data };
                } else if (json.data && json.data.constructor.name.toLowerCase() === 'object') {
                    json.data = applyDocumentFilters(json.data);
                    return { data: json.data, id: json.data.uuid };
                }

                return { data: [] };
        }
    };

    // ...

        const applyDocumentFilters = (document) => {
        // Apply id from uuid parameter if necessary.
        document.id = document.id || document.uuid;
        // ...
        return document;
    };

    return (type, resource, params) => {
        const apiUrl = getAPIUrl(resource);
        // ...
        const { url, options } = convertDataRequestToHTTP(type, resource, params);
        return httpClient(url, options).then((response) => convertHTTPResponse(response, type, resource, params));
    };
};

@te-online not exactly, no.

My Typescript and ECMAScript is a little rusty, but I think I managed to figure out how to roll and npm publish my own data provider, based on ra-data-simple-rest, since that seems to be what is needed to support my existing real-world data.

It allows you to specify via a single simple json hash, say in your app.js, the resources that don't actually use 'id' for their identifier. React-admin will operate fine b/c on its side they use 'id', but the data provider translates to the real identifier property name on your behalf.

It can be found in my github at zachrybaker/ra-data-rest-client and in npm.

// app.js
import customKeysDataProvider from 'ra-data-rest-client';

const customKeysHash = {
    'testKVP': 'key'
};

const dataProvider = customKeysDataProvider('https://localhost:44377/api', customKeysHash);

const App = () => (
    <Admin  dashboard={Dashboard} dataProvider={dataProvider}>
        <Resource name="testKVP"  list={TestKVPList}  edit={TestKVPEdit}  create={TestKVPCreate}  icon={ViewListIcon}  />
    </Admin>
);

Hopefully this will help others get up and running with a large pre-existing web service that can't be modified to play by react-admin's terms.

@fzaninotto I saw in documentation an appeal to let your team know of data providers. Hopefully this is helpful!
https://github.com/marmelab/react-admin/pull/5290

@zachrybaker Okay, cool that you solved it :-) Just to be clear, you don't have to publish an npm package for your custom data provider. You can create a JavaScript file in your project and then import your data provider from that file.

This page didn't help you in terms of documentation? https://marmelab.com/react-admin/DataProviders.html
What do you think needs to be improved? :-)

The package is for reuse and for others, as packages go. There's a PR out there #5290 to bring that into the provider list, as this is undoubtedly generic enough yet helpful for others.

Honestly I'm pretty fresh on react and react-admin, so I may not be the best person to ask, b/c I'm still learning to think more react-y, but IMO that page would do well to re-emphasize the hard requirement on 'id' being your identifier property name, at a minimum.

For me there was a lot to understand even after reading that page before I knew what I needed to do conceptually, and then as is sometimes typical, actually implementing an alternate data provider really brought it all together for me, mentally-speaking.

IMO that page would do well to re-emphasize the hard requirement on 'id' being your identifier property name, at a minimum.

See https://marmelab.com/react-admin/DataProviders.html#response-format

A {Record} is an object literal with at least an id property, e.g. { id: 123, title: "hello, world" }.

I think it does already. Anyway, your dataProvider is indeed a good idea. Good job :+1:

@djhi,

Quick documentation question.

I've published a .NetCore REST server zachrybaker/React-Admin-RestServer that speaks the react-admin dialect you guys chose and works nicely with #5290 or the default provider.

It basically bootstraps the process of getting your resources exposed by [re]generating your data access layer from examining a database for you, and you just have to pick what you actually expose from that.

Do you guys document any server API projects or know any existing place to mention this that others might find it?

Thanks in advance.

I think you can mention it in the miscelanous section of the documentation :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

marknelissen picture marknelissen  路  3Comments

pixelscripter picture pixelscripter  路  3Comments

aserrallerios picture aserrallerios  路  3Comments

waynebloss picture waynebloss  路  3Comments

phacks picture phacks  路  3Comments