RTK Query correct way to map props with transformResponse - redux-toolkit

I'm working on linking up a scheduler component with data that I am receiving through an RTK Query api call. In order for the scheduler to work, I have to adjust the prop names to names the scheduler utilizes, before the state is available in the component (otherwise it does not load when I've tried in the React component). There is not a clear example of this being done in the RTK Query docs (https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#customizing-query-responses-with-transformresponse) , but it seems like it should be possible. When I've implemented it, the query gets fulfilled, but comes back as an empty object, or an array with a single empty object depending on how I've tried to structure it.
getUserScheduleVisits: builder.query({
query: userId => `/visitapi/visits/users/${userId}`,
providesTags: ['Visit', 'User'],
transformResponse: (response) => {
return {
data: [{
id: response._id,
startDate: response.visitStart,
endDate: response.visitEnd,
title: response?.client?.fullName
}]
}
},
}),
When I've run the query without the transform response, the data comes through as expected, however remains unusable by the Scheduler component I'm trying to implement. I'm still learning the complexities to RTK Query so I'm sure its how I'm trying to use transformResponse, I'm just not sure how to approach it correctly.

This would end up as result.data.data if you do const result = useMyQuery().
Did you maybe mean to do this?
getUserScheduleVisits: builder.query({
query: userId => `/visitapi/visits/users/${userId}`,
providesTags: ['Visit', 'User'],
transformResponse: (response) => {
return [{
id: response._id,
startDate: response.visitStart,
endDate: response.visitEnd,
title: response?.client?.fullName
}]
},
}),

Related

How should I store multiple nested arrays, populated using Mongoose populate(), in the cache using React Query?

Apologies if this is basic but I'm struggling to get my head around how to set this up.
I'm using MongoDB/Mongoose for my backend which returns a user object with nested arrays:
{
username: {
type: String,
unique: true
},
name: String,
avatar: String,
recommendations: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Media' }],
watchlist: [{
media: { type: mongoose.Schema.Types.ObjectId, ref: 'Media' },
date_added: Date,
}],
}
If a user visits their watchlist or recommendations page, the nested array gets populated, using mongoose populate(), with the referenced recommendations/watchlist items that they've added.
On the frontend I'm using React Query to handle the data returned from the server. Currently visiting either of the pages returns the whole user object, if I were to cache the entire object using the query key ['user'] the nested array not being populated will be stored as an array of reference id's. Instead I was thinking of maybe trying to update the nested arrays using setQueryData, however this doesn't work if the page is refreshed:
function useWatchlist() {
const { user } = useAuth()
const queryClient = useQueryClient()
const result = useQuery({
queryKey: ['user'],
queryFn: () =>
axios.get(`${baseUrl}/${user.profile_id}/watchlist`).then(response => response.data)
},
{onSuccess: (watchlist) => {
queryClient.setQueryData(['user'], oldUser => {
oldUser.watchlist === watchlist
})
}
})
return {...result, profile: result.data }
}
Should the recommendation/watchlist arrays instead be stored separately using different query keys - ['watchlist']/['recommendations'] or should I attempt to keep the user object structure being returned from the backend?
I would say that yes, you should store them separately. Yet, using relative keys (e.g. ['user', 'watchlist'] and ['user', 'recommendations']) as explained here under Structure:
Structure your Query Keys from most generic to most specific, with as many levels of granularity as you see fit in between
So, you can invalidate them both when the user is refetched.
When I store data such as the "watch list", which only changes when the user changes it, I put a staleTime: Infinity and use setQueryData in the onSuccess of the relevant mutation (when a user updates his watch list).
For the "recommendation list", it's different story, as it would be constantly changing by some logic in the backend. So, I would use invalidateQuery whenever the 'user' key is fetched (or expire the cache, if you update the list each certain interval), and populate it again, on the onSuccess for that query.

Insert into relationship table using id created at user registration

I have two tables as seen below
The first table is for users and is populated via a registration form on the client side. When a new user is created, I need the second 'quotas' table to be populated with date, amount, and linked with the user id. The 'user_id' is used to pull the quotas information in a GET and display client side. I am having issues using the 'id' to populate the second table at the time of creation. I am using knex to make all queries. Would I be using join to link them in knex?
server
hydrateRouter // get all users
.route('/api/user')
.get((req, res) => {
knexInstance
.select('*')
.from('hydrate_users')
.then(results => {
res.json(results)
})
})
.post(jsonParser, (req, res, next) => { // register new users
const { username, glasses } = req.body;
const password = bcrypt.hashSync(req.body.password, 8);
const newUser = { username, password, glasses };
knexInstance
.insert(newUser)
.into('hydrate_users')
.then(user => {
res.status(201).json(user);
})
.catch(next);
})
client
export default class Register extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
glasses: 0
}
}
handleSubmit(event) {
event.preventDefault();
fetch('http://localhost:8000/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.state)
})
.then(response => response.json())
.then(responseJSON => {
this.props.history.push('/login');
})
}
server side route for displaying the water amount
hydrateRouter
.route('/api/user/waterconsumed/:user_id') // display water consumed/day
.all(requireAuth)
.get((req, res, next) => {
const {user_id} = req.params;
knexInstance
.from('hydrate_quotas')
.select('amount')
.where('user_id', user_id)
.first()
.then(water => {
res.json(water)
})
.catch(next)
})
Thank you!
Getting the id of an inserted row
So this is a common pattern in relational databases, where you can't create the egg until you have the unique id of the chicken that lays it! Clearly, the database needs to tell you how it wants to refer to the chicken.
In Postgres, you can simply use Knex's .returning function to make it explicit that you want the new row's id column returned to you after a successful insert. That'll make the first part of your query look like this:
knexInstance
.insert(newUser)
.into('users')
.returning('id')
Note: not all databases support this in the same way. In particular, if you happen to be developing locally using SQLite, it will return the number of rows affected by the query, not the id, since SQLite doesn't support SQL's RETURNING. Best is just to develop locally using Postgres to avoid nasty surprises.
Ok, so we know which chicken we're after. Now we need to make sure we've waited for the right id, then go ahead and use it:
.then(([ userId ]) => knexInstance
.insert({ user_id: userId,
date: knex.fn.now(),
amount: userConstants.INITIAL_QUOTA_AMOUNT })
.into('quotas')
)
Or however you choose to populate that second table.
Note: DATE is a SQL keyword. For that reason, it doesn't make a great column name. How about created or updated instead?
Responding with sensible data
So that's basic "I have the ID, let's insert to another table" strategy. However, you actually want to be able to respond with the user that was created... this seems like sensible API behaviour for a 201 response.
What you don't want to do is respond with the entire user record from the database, which will expose the password hash (as you're doing in your first code block from your question). Ideally, you'd probably like to respond with some UI-friendly combination of both tables.
Luckily, .returning also accepts an array argument. This allows us to pass a list of columns we'd like to respond with, reducing the risk of accidentally exposing something to the API surface that we'd rather not transmit.
const userColumns = [ 'id', 'username', 'glasses' ]
const quotaColumns = [ 'amount' ]
knexInstance
.insert(newUser)
.into('users')
.returning(userColumns)
.then(([ user]) => knexInstance
.insert({
user_id: user.id,
date: knex.fn.now(),
amount: userConstants.INITIAL_QUOTA_AMOUNT
})
.into('quotas')
.returning(quotaColumns)
.then(([ quota ]) => res.status(201)
.json({
...user,
...quota
})
)
)
Async/await for readability
These days, I'd probably avoid a promise chain like that in favour of the syntactic sugar that await provides us.
try {
const [ user ] = await knexInstance
.insert(newUser)
.into('users')
.returning(userColumns)
const [ quota ] = await knexInstance
.insert({
user_id: userId,
date: knex.fn.now(),
amount: userConstants.INITIAL_QUOTA_AMOUNT
})
.into('quotas')
.returning(quotaColumns)
res
.status(201)
.json({
...user,
...quota
})
} catch (e) {
next(Error("Something went wrong while inserting a user!"))
}
A note on transactions
There are a few assumptions here, but one big one: we assume that both inserts will be successful. Sure, we provide some error handling, but there's still the possibility that the first insert will succeed, and the second fail or time out for some reason.
Typically, we'd do multiple insertions in a transaction block. Here's how Knex handles this:
try {
const userResponse = await knexInstance.transaction(async tx => {
const [ user ] = await tx.insert(...)
const [ quota ] = await tx.insert(...)
return {
...user,
...quota
}
})
res
.status(201)
.json(userResponse)
} catch (e) {
next(Error('...'))
}
This is good general practice for multiple inserts that depend on each other, since it sets up an "all or nothing" approach: if something fails, the database will go back to its previous state.

What exactly is "data" that is passed to responses?

I'm writing a custom response that takes data as an input, and I am finding strange properties being added, namely:
add: [Function: add],
remove: [Function: remove]
When I log out some example data, I get:
[ { books:
[ { id: 1,
title: 'A Game of Thrones',
createdAt: '2015-08-04T04:53:38.043Z',
updatedAt: '2015-08-04T04:53:38.080Z',
author: 1 } ],
id: 1,
name: 'George R. R. Martin',
createdAt: '2015-08-04T04:53:38.040Z',
updatedAt: '2015-08-04T04:53:38.073Z' },
{ books:
[ { id: 2,
title: 'Ender\'s Game',
createdAt: '2015-08-04T04:53:38.043Z',
updatedAt: '2015-08-04T04:53:38.080Z',
author: 2 },
{ id: 3,
title: 'Speaker for the Dead',
createdAt: '2015-08-04T04:53:38.043Z',
updatedAt: '2015-08-04T04:53:38.081Z',
author: 2 } ],
id: 2,
name: 'Orson Scott Card',
createdAt: '2015-08-04T04:53:38.042Z',
updatedAt: '2015-08-04T04:53:38.074Z' } ]
Which looks innocent enough, but results in the strange add and remove functions when I use a custom serializer on it. If I take this data and hard-code it straight into the serializer, those are not present. Apparently something is lurking inside of data that's not being printed to the console.
So, what is data?
Edit: So, I'm still not quite sure what other magical properties live in here, but:
Object.keys(data[0].books))
reveals
[ '0', 'add', 'remove' ]
Which is where those are coming from. Why is this included in the data passed to custom responses? And what else might be hiding in there...
More importantly, how do I strip this gunk out and make data a normal object?
JSON.parse(JSON.stringify(data));
That cleans it up nicely, though it feels like a hack. (Actually, it's definitely a hack.)
I assume your data attribute is returned by a database query. e.g.:
Model.find(...).exec(function (err, data) { ... });
But what are these .add() and .remove() methods?
Here is what you can find in the docs:
For the most part, records are just plain old JavaScript objects (aka POJOs). However they do have a few protected (non-enumerable) methods for formatting their wrapped data, as well as a special method (.save()) for persisting programmatic changes to the database.
We can go deeper:
"collection" associations, on the other hand, do have a couple of special (non-enumerable) methods for associating and disassociating linked records. However, .save() must still be called on the original record in order for changes to be persisted to the database.
orders[1].buyers.add({ name: 'Jon Snow' });
orders[1].save(function (err) { ... });
So these methods (.add(), .remove(), .save()) are useful if you play with "collection" associations.
How to remove them?
You'll need to use .toObject() which returns a cloned model instance stripped of all instance methods.
You might want to use .toJSON() that also returns a cloned model instance. This one however includes all instance methods.

Can I use Sails native query with populate?

I am using the native method of sails-mongo to query a collection. I need to use native to access some of the Mongo geospatial query features.
I would like to use the sails populate syntax to include associated models.
Is there a way to do this?
Here is an example of my existing code:
Trip.native(function(err, collection) {
collection.find(
{
"locationTo": {
"$near": {
"$maxDistance": 80467.35439432222,
"$geometry": {
"type": "Point",
"coordinates": [-117.133655, 32.720519]
}
}
}
}
)
.toArray(function(err, trips) {
console.log("Trips nearby:", trips);
});
});
Here is my Trip model for reference.
var Trip = {
attributes: {
owner: {
model: 'User'
},
title: 'string',
addressFrom: 'string',
locationFrom: 'json', // geoJson
dateTimeDepart: 'datetime',
dateTimeArrive: 'datetime',
dateTimeReturn: 'datetime',
addressTo: 'string',
locationTo: 'json', // geoJson
driver: {
model: 'User'
},
status: {
type: 'string',
defaultsTo: 'PENDING'
}
}
}
Would be helpful if you share the Trip model as well. If the field you wish to populate has type "collection" (not "array"), you should be able to populate it just fine.
Update: Alright, I got your question wrong. There doesn't seem to be any way of populating directly after a native call. There's really not much you can do with a native call as far as Waterline functions are concerned. I would suggest either running another query(Waterline) after you've fetched locationTo or populating the fields yourself since you only need to populate two of them(and that too from the same model). I can't think of anything that would suffice with a single query.
Thanks, I ended up doing it in two queries for now.
First, I build an array of matching ID's via the native query.
var tripIdList = trips.map(function (trip) {
return trip._id
});
Second, I do a normal find query using the ID list. It's not a single query but works well. Thanks for the help
Trip.find(filter)
.where({id: tripIdList})
.populate('driver')
.exec(function (err, trips) {
console.log("Trips:", trips);
}

Does Moongoose 3.8.8 support $position operator?

Does Moongoose 3.8.8 (the lastest version) support $position (http://docs.mongodb.org/manual/reference/operator/update/position/) operator from MongoDB 2.6.0?
In the following code example the new elements is inserted in the end of the array userActivity.activities:
model:
var userActivity = new schema({
userId: {type:String, required:true, unique:true},
activities: [activity]
});
var activity = new schema({
act: {type: Number, required:true},
});
query:
var activity = { act: 1 };
model.userActivity.update(
{ _id: dbact._id },
{ $push: { activities: {
$each: [ activity ],
$position: 0
}
}
},
function (err, numAffected) {
if (!err) {
// do something
}
});
This actually doesn't matter and never matters for any "framework" implementation and I do not mind explaining why.
Every single "framework" ( such as Mongoose, Mongoid, Doctrine, MongoEngine, etc, etc, etc ) are all basically built upon a basic "driver" implementation that has in most cases been developedby the MongoDB staff themselves. So the basic functionality is always ther even if you need to "delve" down to a level in order to use those "native" methods.
So here would be the native usage example in this case:
List.collection.update(
{},
{ "$push": {
"list": {
"$each": [ 1, 2, 3 ],
"$position": 0 }
}
},function(err,NumAffected) {
console.log("done");
});
Note the "collection" method used from the model, which is getting the "raw" collection details from the driver. So you are using it's method and not some "wrapped" method that may be doing additional processing.
The next and most basic reason is if you cannot find the method and application of the operators that you need the here is a simple fact.
Every single operation as used by the methods in every framework and basic driver method is essentially a call to the "runCommand" method in the basic API. So since that basic call is available everywhere ( in some form or another, because it has to be ), then you can do everything that you find advertised on the MongoDB site with every language implementation on any framework.
But the short call to your particular request is, since this is not actually a method call but is simply part of the BSON arguments as passed in, then of course there is no restriction by a particular language driver to actually use this.
So you can use these new argument without of course updating to the most recent version. But you probably will get some nice methods to do so if you actually do.
Yes, you should be able to use it directly as Mongoose will pass through the update clause:
Model.update(
query, /* match the document */
{ $push:
{ yourArrayField:
{
$each: [ 1, 2, 3 ],
$position: 0
}
}
}, function (err, res) { /* callback */ });
The above would insert the values 1, 2, 3 at the front of the array named yourArrayField.
As it's just a pass-through, you'll need to make sure it works with the server version that you're connecting the client to.