I'm trying to use Redux-Toolkit's createEntityAdapter in an entity that has compound keys. For example, my territories entity has a type (municipality, state, biome, etc) and a geocode number. The pair {type, geocode} is the compound key of this entity.
I want to be able to use selectById and other selectors. My first thought was to create an id field that concatenates type, ";" and geocode, but I'm sure there's a better way.
import { createEntityAdapter } from '#reduxjs/toolkit'
const adapter = createEntityAdapter({
// selectId: (item) => ???,
})
const APIresponse = {
data: [
{ type: 'state', geocode: 1, value: 123},
{ type: 'state', geocode: 2, value: 66},
{ type: 'municipality', geocode: 1, value: 77},
{ type: 'municipality', geocode: 2, value: 88},
{ type: 'municipality', geocode: 3, value: 99}
]
}
I'm a Redux maintainer and the person who implemented createEntityAdapter for RTK.
createEntityAdapter does assume that you have some kind of unique ID field in your data. If you don't have a unique ID field from the original data, you've got three options I can think of:
Generate a unique ID for each item when you are processing the API response (but before any "loaded" action is dispatched)
Concatenate together some combination of fields to create a synthesized id field when you are processing the API response
Implement selectId so that it returns a combination of fields each time
Related
Description
My objective is to evaluate a req.body as a CASL.js subject (built with the built-in subject helper) and to determine not only if the user can perform an action against that subject under the predefined conditions, but also if the provided req.body has only the permitted fields defined in the AbilityTuple used for the Ability constructor.
I was almost sure that this was the predetermined functionality of the can() utility until I ran with an unorthodox use of it (not important in this context), where it was obvious that, as long as the conditions matched, the subject evaluated could hold any other fields different than the ones defined in the fields array.
Illustrative example
I have a user 'grant' (a permission) defined in a database JSON:
{
"action": "create",
"subject": "users",
"conditions": {
"idPlatform": 8,
"idRole": 14
},
"fields": [
"idPlatform",
"idRole",
"attributes.**",
"key",
"person.documents.**",
"person.emails.**
]
}
This is passed to the new Ability() constructor along with all the other permissions of a given user, fetched from database. Then an endpoint controller evaluates a user authorization for performing a user creation action before moving on to further methods, using the ability.can() utility in the following way:
async authorize(req) {
const userSubject = subject('users', req.body);
if (!await this.ability.can('create', userSubject)) {
throw new Error('Unauthorized');
}
}
Now suppose a user sends the following body to this endpoint:
idPlatform: 8,
idRole: 14,
key: "myusername",
active: 0,
person: {
documents: [
{
idType: 1,
document: "37548323"
},
{
idType: 3,
document: "20375483231"
},
emails: [
{
idType: 2,
email: "mypersonalemail#gmail.com"
},
{
idType: 3,
email: "myworkemail"
}
],
telephones: [
{
"idType": 1
"telephone": "29445368432"
]
}
As you may see, there are two additional fields apart from those declared in the fields ability object: active and person.telephones. But since the body matches the two conditions defined (idPlatform and idRole), the ability.can() method returns true.
What I'd like is that the ability.can() utility would also evaluate if the provided subject has only the fields allowed for the user, or return false if there are any extra fields. Currently I can achieve that by flattening the subject keys into an strings array:
['idPlatform', 'idRole', 'key', 'active', 'person.documents.idType', 'person.documents.document', ...]
...and iterating through them with the ability.can('create', subject, flattenedField).
can dynamodb query data for list contains object that match some attribute?
my data format:
[{
pk,
sk,
gsi1pk: 'USER',
gsi1sk,
list:[
{
id,
type, // admin, moderator, user
name,
}
]
}]
can we do something like this to find data with list contains type is 'admin'?
await Ddb.query( {
IndexName: 'GSI1',
KeyConditionExpression: 'gsi1pk = :gsi1pk',
FilterExpression: 'contains(list, :list)'
ExpressionAttributeValues: {
':gsi1pk': 'USER',
':list': { type: 'admin'}
},
});
this does not work now :(
i using redux tool kit to build react native app and i try to normalize my data like this
const postEntitiy = new schema.Entity('post');
const postAdapter = createEntityAdapter({
selectId: (post) => post._id,
});
const normalized = normalize(response.data, postEntitiy);
this is my resopose.data
Array [
Object {
"__v": 5,
"_id": "6020b367cb94a91c9cd48c34",
"comments": Array [],
"date": "2021-02-08T03:43:35.742Z",
"likes": Array [
Object {
"_id": "60216bd341b3744ce4b13bee",
"user": "601f2d46017c85357800da96",
},
],",
},
]
and this is the error it throw
The entity passed to the `selectId` implementation returned undefined., You should probably provide
your own selectId implementation.,
The entity that was passed:, Object {
"undefined": Object {
"0": Object {
"__v": 5,
"_id": "6020b367cb94a91c9cd48c34",
"comments": Array [],
"date": "2021-02-08T03:43:35.742Z",
"likes": Array [
Object {
"_id": "60216bd341b3744ce4b13bee",
"user": "601f2d46017c85357800da96",
},
],
},
]
As this is written the postAdapter and the normalize are independent of each other. What's happening here is that normalize is attempting the map the data into entities, but it ends up with the key as undefined because it is using the default key of id which has an undefined value. Additionally you have an array of entities rather than just one.
The postAdapter then tries to find the selectId on this data which has already been normalized. But the postAdapter is not looking at a single post object -- it is looking at the nested data created by normalize. It is looking at the data structure Object { "undefined": Object { "0": Object {... from your error. The "undefined": is caused by the bad id attribute and the "0": is because it is an array.
How you do write this correctly kind of depends on what it is that you are trying to do. It's possible that normalize is not really necessary here. It looks like the likes in response.data are already returning a string id for the like and the user instead of the deeply-nested data that the normalizr package is designed to flatten. If you want to use normalize and have it work properly then you need to set the idAttribute on the options argument (docs link).
const postEntity = new schema.Entity('post', {}, {idAttribute: '_id'});
const normalized = data.map( o => normalize(o, postEntity));
idAttribute also accepts a function. You can create a reusable id extractor function that pulls the _id property from any object with {_id: string;}.
const myIdExtractor = (object : {_id: string;}) => object._id;
const postEntity = new schema.Entity('post', {}, {idAttribute: myIdExtractor});
const normalized = data.map( o => normalize(o, postEntity));
That same id extractor function also works with createEntityAdapter.
const postAdapter = createEntityAdapter({
selectId: myIdExtractor,
});
I'm using graphql through AWS AppSync. Given my models below I would expect when I successfully createClassroom with createClassroomInput that the teacherClassrooms would have a new Classroom associated to it and that the newly created Classroom would have a teacher associated to it.
The outcome, however, is that Classroom is created and the User is correctly associated with the new Classroom but the Classroom is not associated to the existing User.
type User #model {
id: ID!
userType: String!
teacherClassrooms: [Classroom] #connection(name: "TeacherClassrooms")
}
type Classroom #model {
id: ID!
teacher: User #connection(name: "TeacherClassrooms")
linkCode: String!
name: String!
}
export type CreateClassroomInput = {
id?: string | null,
linkCode: string,
name: string,
classroomTeacherId?: string | null,
};
So, if I query for listClassrooms, each classroom comes back with its associated User. But if I query for a User they do not have any classrooms in their teacherClassrooms array.
Do I need to updateUser when I create a new Classroom? My intuition, and my understanding of the docs, lead me to believe that AppSync would handle both updates when #connection is specified on a model property.
Or is this just a way of indicating to the backend that "for each Id in this property array assume it's of type X and when queried go fetch by Id against the corresponding table"?
Check your list query. I had the same issue and then realised that generated list query was missing relation attributes after I updated schema with name connection. In your case something like this
listUsers {
items {
id
teacherClassrooms {
items {
linkCode
id name
}
}
}
}
inside your listUsers query
I'm using Redux and ImmutableJS to manage the state of my app. I've created the following two Records:
export const OrderRecord = Record({
id: null,
productId: null,
amount: 1,
});
export const ProductRecord = Record({
id: null,
name: '',
price: 0,
});
My global state is normalized based on the normalizr approach like this:
const state = {
entities: {
orders: new OrderedMap(new Map({
1: new OrderRecord(createOrderItem(1, 1)),
})),
products: new OrderedMap(new Map({
1: new ProductRecord(createProductItem(1)),
})),
},
};
I'm using this specification for testing purposes.
Now I'm trying to make some selects with computed fields using Reselect.
export const getVisibleOrders = createSelector(
[getProducts, getOrders],
(products, orders) => {
orders.map(order => {
const product = products.get(order.productId.toString());
if (!product) {
return order;
}
const totalPrice = order.amount * product.price;
order.set('productName', product.name);
order.set('totalPrice', totalPrice);
return order;
});
}
);
, but I get the following error message:
Error: Cannot set unknown key "productName" on Record
I know the reason - Record cannot contain any undefined keys, but my question is: Is there any suggested approach how gracefully solved this problem?
I don't want to extend my Records to support this kind of computed parameters (product.name and totalPrice).
I don't want to keep the static and computed parameters in one place, because for example the 'productName' parametr is from "Product" entity and not from "Order" entity.
Thank you.
The whole point of using Immutable.Record is to not let you add new keys to your record, hence the error message you get. And the whole point of selectors is to expose such "computed" property if you want to consume them outside. In your case, you can simply return a new Map() object or a new record type if you need to use the dotted syntax :
return Map({
productName: 'foobar'
}).merge(order)