Support for resource nesting - rest
I am wondering, is it possible to configure DataProvider/Resource/List to support REST urls like api/users/1/roles?
For RESTful API it is very common use case to get childs of certain parent entity, but I cant figure it how to setup React Admin and achieve this. I am using custom DataProvider build on OData spec backend.
I understand that I can get roles of certain user by filtered request on api/roles?filter={userId: 1} or something like that, but my issue is that my users and roles are in many-to-many relation so relation references are stored in pivot table. In other words, I dont have reference on user in roles table so I cant filter them.
Am I overseeing something or is there some approach which I simply dont see?
EDIT:
REST API is built in OData spec and it supports many-to-many relations with classic pivot (or intermediate) table. This table is not exposed in API, but is utilized in urls like the one above. So I cant directly access it as a resource.
Schema for User - Role relations looks pretty standard too.
|----------| |-----------| |--------|
| USER | | User_Role | | Role |
|----------| |-----------| |--------|
| Id |-\ | Id | /-| Id |
| Login | \-| UserId | / | Name |
| Password | | RoleId |-/ | Code |
|----------| |-----------| |--------|
TL;DR: By default, React Admin doesn't support nested resource, you have to write a custom data provider.
This question was answered on a past issue: maremelab/react-admin#261
Detailed Answer
The default data provider in React Admin is ra-data-simple-rest.
As explained on its documentation, this library doesn't support nested resources since it only use the resource name and the resource ID to build a resource URL:
In order to support nested resources, you have to write your own data provider.
Nested resources support is a recurrent feature request but, at the time, the core team don't want to handle this load of work.
I strongly suggest to gather your forces and write an external data provider and publish it like ra-data-odata provider. It would be a great addition and we will honored to help you with that external package.
Your question was already answer here, but I would like to tell you about my workaround in order for React-Admin work with many-to-many relations.
As said in the mentioned answer you have to extend the DataProvider in order for it to fetch resources of a many-to-many relation. However you need to use the new REST verb, lets suppose GET_MANY_MANY_REFERENCE somewhere on your application. Since different REST services/API can have different routes formats to fetch related resources I didn't bother trying to build a new DataProvider, I know is not a great solution, but for short deadlines is considerable simple.
My solution was taking inspiration on <ReferenceManyField> and build a new component <ReferenceManyManyField> for many-to-many relations. This component fetches related records on componentDidMount using fetch API. On response uses the response data to build to objects one data being an object with keys being record ids, and values the respective record object, and an ids array with the ids of records. This is passes to children along with other state variables like page, sort, perPage, total, to handle pagination and ordering of data. Be aware that changing the order of the data in a Datagrid means a new request will be made to the API. This component is divided in a controller and a view, like <ReferencemanyField>, where controller fetches data, manages it and passes it to children and view that receives controller data and passes it to children render its content. That made me possible to render many-to-many relations data on a Datagrid, even if with some limitation, is a component to aggregated to my project and only work with my current API if something changes I would have to change the field to, but as for now it works and can be reused along my app.
Implementation details go as follow:
//ReferenceManyManyField
export const ReferenceManyManyField = ({children, ...prop}) => {
if(React.Children.count(children) !== 1) {
throw new Error( '<ReferenceManyField> only accepts a single child (like <Datagrid>)' )
}
return <ReferenceManyManyFieldController {...props}>
{controllerProps => (<ReferenceManyManyFieldView
{...props}
{...{children, ...controllerProps}} /> )}
</ReferenceManyManyFieldController>
//ReferenceManyManyFieldController
class ReferenceManyManyFieldController extends Component {
constructor(props){
super(props)
//State to manage sorting and pagination, <ReferecemanyField> uses some props from react-redux
//I discarded react-redux for simplicity/control however in the final solution react-redux might be incorporated
this.state = {
sort: props.sort,
page: 1,
perPage: props.perPage,
total: 0
}
}
componentWillMount() {
this.fetchRelated()
}
//This could be a call to your custom dataProvider with a new REST verb
fetchRelated({ record, resource, reference, showNotification, fetchStart, fetchEnd } = this.props){
//fetchStart and fetchEnd are methods that signal an operation is being made and make active/deactivate loading indicator, dataProvider or sagas should do this
fetchStart()
dataProvider(GET_LIST,`${resource}/${record.id}/${reference}`,{
sort: this.state.sort,
pagination: {
page: this.state.page,
perPage: this.state.perPage
}
})
.then(response => {
const ids = []
const data = response.data.reduce((acc, record) => {
ids.push(record.id)
return {...acc, [record.id]: record}
}, {})
this.setState({data, ids, total:response.total})
})
.catch(e => {
console.error(e)
showNotification('ra.notification.http_error')
})
.finally(fetchEnd)
}
//Set methods are here to manage pagination and ordering,
//again <ReferenceManyField> uses react-redux to manage this
setSort = field => {
const order =
this.state.sort.field === field &&
this.state.sort.order === 'ASC'
? 'DESC'
: 'ASC';
this.setState({ sort: { field, order } }, this.fetchRelated);
};
setPage = page => this.setState({ page }, this.fetchRelated);
setPerPage = perPage => this.setState({ perPage }, this.fetchRelated);
render(){
const { resource, reference, children, basePath } = this.props
const { page, perPage, total } = this.state;
//Changed basePath to be reference name so in children can nest other resources, not sure why the use of replace, maybe to maintain plurals, don't remember
const referenceBasePath = basePath.replace(resource, reference);
return children({
currentSort: this.state.sort,
data: this.state.data,
ids: this.state.ids,
isLoading: typeof this.state.ids === 'undefined',
page,
perPage,
referenceBasePath,
setPage: this.setPage,
setPerPage: this.setPerPage,
setSort: this.setSort,
total
})
}
}
ReferenceManyManyFieldController.defaultProps = {
perPage: 25,
sort: {field: 'id', order: 'DESC'}
}
//ReferenceManyManyFieldView
export const ReferenceManyManyFieldView = ({
children,
classes = {},
className,
currentSort,
data,
ids,
isLoading,
page,
pagination,
perPage,
reference,
referenceBasePath,
setPerPage,
setPage,
setSort,
total
}) => (
isLoading ?
<LinearProgress className={classes.progress} />
:
<Fragment>
{React.cloneElement(children, {
className,
resource: reference,
ids,
data,
basePath: referenceBasePath,
currentSort,
setSort,
total
})}
{pagination && React.cloneElement(pagination, {
page,
perPage,
setPage,
setPerPage,
total
})}
</Fragment>
);
//Assuming the question example, the presentation of many-to-many relationship would be something like
const UserShow = ({...props}) => (
<Show {...props}>
<TabbedShowLayout>
<Tab label='User Roles'>
<ReferenceManyManyField source='users' reference='roles' addLabel={false} pagination={<Pagination/>}>
<Datagrid>
<TextField source='name'/>
<TextField source='code'/>
</Datagrid>
</ReferenceManyManyField>
</Tab>
</TabbedShowLayout>
</Show>
)
//Used <TabbedShowLayout> because is what I use in my project, not sure if works under <Show> or <SimpleShowLayout>, but I think it work since I use it in other contexts
I think the implementation can be improved and be more compatible with React-Admin. In other reference fields data fetch is stored on react-redux state, in this implementation it's not. The relation is not saved anywhere besides the component making application not work on offline since can't fetch data, not even ordering is possible.
Had a very similar question. My solution was more of a hack but a little simpler to implement if all you want is to enable a ReferenceManyField. Only the dataProvider needs to be modified:
I'm repeating my solution here modified for the current question:
Using the stock ReferenceManyField:
<Show {...props}>
<TabbedShowLayout>
<Tab label="Roles">
<ReferenceManyField reference="roles" target="_nested_users_id" pagination={<Pagination/>} >
<Datagrid>
<TextField source="role" />
</Datagrid>
</ReferenceManyField>
</Tab>
</TabbedShowLayout>
</Show>
I then modified my dataProvider, which is a fork of ra-jsonapi-client.
I changed index.js under the case GET_MANY_REFERENCE from this:
// Add the reference id to the filter params.
query[`filter[${params.target}]`] = params.id;
url = `${apiUrl}/${resource}?${stringify(query)}`;
to this:
// Add the reference id to the filter params.
let refResource;
const match = /_nested_(.*)_id/g.exec(params.target);
if (match != null) {
refResource = `${match[1]}/${params.id}/${resource}`;
} else {
query[`filter[${params.target}]`] = params.id;
refResource = resource;
}
url = `${apiUrl}/${refResource}?${stringify(query)}`;
So basically I just remap the parameters to the url for the special case where the target matches a hard coded regex.
ReferenceManyField would normally have caused the dataProvider to call api/roles?filter[_nested_users_id]=1 and this modification makes the dataProvider call api/users/1/roles instead. It is transparent to react-admin.
Not elegant but it works and doesn't seem to break anything on the front end.
Related
DynamoDB - How to upsert nested objects with updateItem
Hi I am newbie to dynamoDB. Below is the schema of the dynamo table { "user_id":1, // partition key "dob":"1991-09-12", // sort key "movies_watched":{ "1":{ "movie_name":"twilight", "movie_released_year":"1990", "movie_genre":"action" }, "2":{ "movie_name":"harry potter", "movie_released_year":"1996", "movie_genre":"action" }, "3":{ "movie_name":"lalaland", "movie_released_year":"1998", "movie_genre":"action" }, "4":{ "movie_name":"serendipity", "movie_released_year":"1999", "movie_genre":"action" } } ..... 6 more attributes } I want to insert a new item if the item(that user id with dob) did not exist, otherwise add the movies to existing movies_watched map by checking if the movie is not already available the movies_watched map . Currently, I am trying to use update(params) method. Below is my approach: function getInsertQuery (item) { const exp = { UpdateExpression: 'set', ExpressionAttributeNames: {}, ExpressionAttributeValues: {} } Object.entries(item).forEach(([key, item]) => { if (key !== 'user_id' && key !== 'dob' && key !== 'movies_watched') { exp.UpdateExpression += ` #${key} = :${key},` exp.ExpressionAttributeNames[`#${key}`] = key exp.ExpressionAttributeValues[`:${key}`] = item } }) let i = 0 Object.entries(item. movies_watched).forEach(([key, item]) => { exp.UpdateExpression += ` movies_watched.#uniqueID${i} = :uniqueID${i},` exp.ExpressionAttributeNames[`#uniqueID${i}`] = key exp.ExpressionAttributeValues[`:uniqueID${i}`] = item i++ }) exp.UpdateExpression = exp.UpdateExpression.slice(0, -1) return exp } The above method just creates update expression with expression names and values for all top level attributes as well as nested attributes (with document path). It works well if the item is already available by updating movies_watched map. But throws exception if the item is not available and while inserting. Below is exception: The document path provided in the update expression is invalid for update However, I am still not sure how to check for duplicate movies in movies_watched map Could someone guide me in right direction, any help is highly appreciated! Thanks in advance
There is no way to do this, given your model, without reading an item from DDB before an update (at that point the process is trivial). If you don't want to impose this additional read capacity on your table for update, then you would need to re-design your data model: You can change movies_watched to be a Set and hold references to movies. Caveat is that Set can contain only Numbers or Strings, thus you would have movie id or name or keep the data but as JSON Strings in your Set and then parse it back into JSON on read. With SET you can perform ADD operation on the movies_watched attribute. https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.ADD You can go with single table design approach and have these movies watched as separate items with (PK:userId and SK:movie_id). To get a user you would perform a query and specify only PK=userId -> you will get a collection where one item is your user record and others are movies_watched. If you are new to DynamoDB and are learning the ropes, then I would suggest go with this approach. https://www.alexdebrie.com/posts/dynamodb-single-table/
How can I create a relation in Strapi if I don't know the id of the field?
I am creating a collection of judges and courthouses. Every judge will be assigned to one courthouse. I have set up my relation to be that courthouse has many judges I am attempting to do this programmatically when the app loads. I have a function that is able to populate all the fields in judge except the relation to courthouse. My function uses the Strapi API like this const judge = await strapi.query('judge').create({ name: data[i].name, }, { courthouse: data[i].courthouse_name // here is where I think the relation is created } ) I am passing in a string that has the name of courthouse, because I don't know the ID of the courthouse in the Courthouse collection. My question is it possible to create a relation to another collection by anything other than an ID? How can I create a relation to a courthouse by its name?
I couldn't find a way around building a relationship between two models without the ID, so I created a custom solution using the Strapi lifecycle hooks Essentially what I did I utilized the beforeCreate lifecycle hook to query and find the courthouse that matches the name like this: // judges.js async beforeCreate(result, data) { const courthouse = await strapi.query('courthouse').find( {courthouse_name:data.courthouse} ); // returns the courthouse that matches the name result['courthouse'] = courthouse[0].id; // populates the relational field with the // ID of the courthouse } The response object contained the courthouse's ID and I manipulated the data that is being sent to the create command like this: const judge = await strapi.query('judge').create({ name: data[i].name, courthouse: data[i].courthouse_name }) The result is an object that looks like this: {name: 'Garfield Lucas, courthouse: 7463987}
Strapi - How to GET data sorted based on relation property?
I have an Articles table and it has a relation with the Authors table. Each author has a name property. I want to make a GET request for the articles and receive the articles sorted based on their author's name. When I use _sort=author in the url parameters, I get the articles sorted based on the author object's ID. When I use _sort=author.name in the url parameters, I get the articles in a seemingly random order but definitely not based on author.name. How can I get the data sorted based on author.name? I use mongoDB as my database.
That is the default behavior of _sort. However, you can simply accomplish this by overriding the find method in api/article/services/article.js to sort by Author's name like so: module.exports = { async find(params, populate) { let result = await strapi.query('article').find(params, populate); if (params._sort == 'author') return result.sort((a, b) => (a.author.name > b.author.name) ? 1 : -1); return result; }, } Check the documentation for customizing services to get more info: https://strapi.io/documentation/v3.x/concepts/services.html#concept
create personal collections for logged in users
import {favRestaurants} from '/lib/collections'; import {Meteor} from 'meteor/meteor'; import {check} from 'meteor/check'; export default function () { Meteor.methods({ 'favRestaurants.create' (id, name, rating, priceLevel, type) { check(id, String); check(name, String); check(rating, Number); check(priceLevel, Number); check(type, String); const createdAt = new Date(); const restaurant = {id, name, rating, priceLevel, type, createdAt}; if(check(Meteor.user()) == null){ console.log('onlye logged in users can data'); }else{ FavRestaurants.insert(restaurant); } } }); } This is my insert method for adding data to the restaurants collections. When i console log the 'check(Meteor.user())' in the console i get null as output. By that logic you shouldn't be able to add data to the collection, although this is still possible. I would also like to make the FavResaurants collection personal for each user. Iv'e tried to check if there is a user and then adding a collection in the main.js file on the client side. Meteor.loggingIn(() => { console.log('check for user method'); var restId = 0; if(Meteor.user() != null){ console.log('created new collection for the user'); const FavRestaurants = new Mongo.Collection('favRestaurants' + restId); } restId++; }); I dont get any output to console using this method, which i found in the meteor docs. Is the code in the wrong place? Any help is much appriciated. According to the docs the Accounts.ui.config is the method i should use. But I'm not sure in code i should put it. So far the placement of this method has resulted in my application crashing.
Answering your first question, to allow only logged-in clients to access a method, you should use something like: if (!Meteor.userId()) { throw new Meteor.Error('403', 'Forbidden'); } Now, I see you want a collection to store favorite restaurants for each user in client side. But as I see it, there'd be only one logged in user per client, so you don't need a separate collection for each user (as the collection is in each client), you can just refer the user with it's id, and then fetch a user's favorite restaurants by a query like: FavRestaurants.find({user: Meteor.userId()}); Moreover, as the docs suggest, Meteor.loggingIn is a method which tells you if some user is in the process of logging in. What you are doing is over-riding it, which doesn't make sense. You should do something like: if (Meteor.loggingIn()) { // Do your stuff } Hope it gives you more clarity.
Creating a collection per user is a bad approach. Define your favRestaurants collection once and add a owner field in the restaurant document before insert. Create a publish method to publish to client side the userid favrestaurant only. One more thing, check your userid first in your Meteor method, it will avoid unnecessary proces. Regs
multiple nested navigation properties EF ?
i have in my database a User has list of Subscriptions, each subscription has a Category, each category has list of Paths, each path has list of Articles ? Model class User { ICollection<Subscription> Subscriptions; } class Subscription { Category category; } class Category { ICollection<Path> paths; } class Path{ ICollection<Article> Articles; } my question How to retrieve all Articles for a specific user ?
Using a combination of Select and SelectMany statements, you are able to traverse your entities and find the collection you need: var allUserArticles = myUser.Subscriptions .Select(sub => sub.Category) //take the category from each subscription .SelectMany(cat => cat.Paths) //Take each path from each category, put them in a single list .SelectMany(path => path.Articles) //Take each article from each path, put them in a single list .ToList(); Note that if these entities need to be loaded from your database, that EF either needs to implement eager loading, or you have to make sure that all needed related entities are retrieved before running the above statement. Unexpected nulls can cause your output to be incorrect. However, I have no info on this as it is missing from the question, so I can't know your specific case.