I have three actions in a MobX State Tree store: the first fetches data form the API, the second sends a POST request using the data from the API in the database, and the third takes the response and saves it to the store.
The store is comprised simply of a map of these data structures called Lists:
export const ListStore = types
.model('ListStore', {
lists: types.map(List),
})
The first two actions to send the GET and POST requests work fine:
.actions((self) => ({
fetchTitles: flow(function* fetchTitles(params: Params) {
const env = getStoreEnv(self);
const { clients } = env;
const browseParams: BrowseParams = {
category: 'movies',
imdb_score_min: params.filter.imdbFilterScore,
};
let browseResult;
try {
browseResult = yield clients.browseClient.fetch(browseParams);
} catch (error) {
console.error('Failed to fetch titles', error);
}
return browseResult.data.results.map((title) => title.uuid);
}),
}))
.actions((self) => ({
postList: flow(function* postList(params: Params) {
const env = getStoreEnv(self);
const { clients } = env;
const titles = yield self.fetchTitles(params);
return clients.listClient.create({
name: params.name,
titles,
invites: params.invites,
filter: params.filter,
});
}),
}))
But when it comes to the third action, actually saving the List to the ListStore, no such luck. I've tried quite a few variations, but simply none of them work. Honestly I am not too familiar with generator syntax, and I even tried doing it without the generator. Here you can see my attempts:
createList: flow(function* createList(params: Params) {
const env = getStoreEnv(self);
const list = yield self.postList(params);
console.log('list in createList', list.data);
return self.lists.put(List.create({ ...list.data }, env));
// return self.lists.set(list.id, list.data);
}),
createList: flow(function* createList(params: Params) {
const list = yield self.postList(params);
console.log('list in createList', list.data);
yield self.lists.set(list.id, list.data);
}),
createList(params: Params) {
return self.postList(params).then((list) => {
console.log('list in createList', list.data);
self.lists.set(list.id, list.data);
});
},
createList: flow(function* createList(params: Params) {
yield self.postList(params).then((list) => {
console.log('list in createList', list.data);
return self.lists.set(list.id, list.data);
});
}),
I've tried with both .set() and .put(), but to no avail. I've also tried using yield and return...nothing seems to work. The data logged in console.log('list in createList', list.data); looks correct and matches the model (and if it didn't, wouldn't I get an error saying so?). No errors are logged to the console, it just silently fails.
If you can spot the error and see how this should be written, I will be extremely grateful. Thank you!
It turns out that the issue was not with the syntax: as you can see in the first comment by the maintainer of MST, the second version is correct.
The problem lies in the way the model (not shown here) was created.
Related
I have a three-check box type,
When I check any box I call refetch() in useEffect().
The first time, I check all boxes and that returns the expected data!
but for some cases "rechange the checkboxes randomly", the returned data from API is "undefined" although it returns the expected data in Postman!
So I Guess should I need to provide a unique queryKey for every data that I want to fetch
so I provide a random value "Date.now()" but still return undefined
Code snippet
type bodyQuery = {
product_id: number;
values: {};
};
const [fetch, setFetch] = useState<number>();
const [bodyQuery, setBodyQuery] = useState<bodyQuery>({
product_id: item.id,
values: {},
});
const {
data: updatedPrice,
status,
isFetching: loadingPrice,
refetch,
} = useQuery(
['getUpdatedPrice', fetch, bodyQuery],
() => getOptionsPrice(bodyQuery),
{
enabled: false,
},
);
console.log('#bodyQuery: ', bodyQuery);
console.log('#status: ', status);
console.log('#updatedPrice: ', updatedPrice);
useEffect(() => {
if (Object.keys(bodyQuery.values).length > 0) {
refetch();
}
}, [bodyQuery, refetch]);
export const getOptionsPrice = async (body: object) => {
try {
let response = await API.post('/filter/product/price', body);
return response.data?.detail?.price;
} catch (error) {
throw new Error(error);
}
};
So after some elaboration in the chat, this problem can be solved by leveraging the useQuery key array.
Since it behaves like the dependency array in the useEffect for example, everything that defines the resulted data should be inserted into it. Instead of triggering refetch to update the data.
Here the key could look like this: ['getUpdatedPrice', item.id, ...Object.keys(bodyQuery.values)], which will trigger a new fetch if those values change and on initial render.
Im trying to figure this out.
I want to get all my users from my database, cache them
and then when making a new request I want to get those that Ive cached + new ones that have been created.
So far:
const batchUsers = async ({ user }) => {
const users = await user.findAll({});
return users;
};
const apolloServer = new ApolloServer({
schema,
playground: true,
context: {
userLoader: new DataLoader(() => batchUsers(db)),// not sending keys since Im after all users
},
});
my resolver:
users: async (obj, args, context, info) => {
return context.userLoader.load();
}
load method requiers a parameter but in this case I dont want to have a specific user I want all of them.
I dont understand how to implement this can someone please explain.
If you're trying to just load all records, then there's not much of a point in utilizing DataLoader to begin in. The purpose behind DataLoader is to batch multiple calls like load(7) and load(22) into a single call that's then executed against your data source. If you need to get all users, then you should just call user.findAll directly.
Also, if you do end up using DataLoader, make sure you pass in a function, not an object as your context. The function will be ran on each request, which will ensure you're using a fresh instance of DataLoader instead of one with a stale cache.
context: () => ({
userLoader: new DataLoader(async (ids) => {
const users = await User.findAll({
where: { id: ids }
})
// Note that we need to map over the original ids instead of
// just returning the results of User.findAll because the
// length of the returned array needs to match the length of the ids
return ids.map(id => users.find(user => user.id === id) || null)
}),
}),
Note that you could also return an instance of an error instead of null inside the array if you want load to reject.
Took me a while but I got this working:
const batchUsers = async (keys, { user }) => {
const users = await user.findAll({
raw: true,
where: {
Id: {
// #ts-ignore
// eslint-disable-next-line no-undef
[op.in]: keys,
},
},
});
const gs = _.groupBy(users, 'Id');
return keys.map(k => gs[k] || []);
};
const apolloServer = new ApolloServer({
schema,
playground: true,
context: () => ({
userLoader: new DataLoader(keys => batchUsers(keys, db)),
}),
});
resolver:
user: {
myUsers: ({ Id }, args, { userLoader }) => {
return userLoader.load(Id);
},
},
playground:
{users
{Id
myUsers
{Id}}
}
playground explained:
users basically fetches all users and then myusers does the same thing by inhereting the id from the first call.
I think I choose a horrible example here since I did not see any gains in performence by this. I did see however that the query turned into:
SELECT ... FROM User WhERE ID IN(...)
I have graphql User type that needs information from multiple REST api's and different servers.
Basic example: get the user firstname from rest domain 1 and get lastname from rest domain 2. Both rest domain have a common "userID" attribute.
A simplefied example of my resolver code atm:
user: async (_source, args, { dataSources }) => {
try {
const datasource1 = await dataSources.RESTAPI1.getUser(args.id);
const datasource2 = await dataSources.RESTAPI2.getUser(args.id);
return { ...datasource1, ...datasource2 };
} catch (error) {
console.log("An error occurred.", error);
}
return [];
}
This works fine for this simplefied version, but I have 2 problems with this solution:
first, IRL there is a lot of logic going into merging the 2 json results. Since some field are shared but have different data (or are empty). So it's like cherry picking both results to create a combined result.
My second problem is that this is still a waterfall method. First get the data from restapi1, when thats done call restapi2. Basicly apollo-server is reintroducing rest-waterfall-fetch graphql tries to solve.
Keeping these 2 problems in mind.. Can I optimise this piece of code or rewrite is for better performance or readability? Or are there any packages that might help with this behavior?
Many thanks!
With regard to performance, if the two calls are independent of one another, you can utilize Promise.all to execute them in parallel:
const [dataSource1,dataSource2] = await Promise.all([
dataSources.RESTAPI1.getUser(args.id),
dataSources.RESTAPI2.getUser(args.id),
])
We normally let GraphQL's default resolver logic do the heavy lifting, but if you're finding that you need to "cherry pick" the data from both calls, you can return something like this in your root resolver:
return { dataSource1, dataSource2 }
and then write resolvers for each field:
const resolvers = {
User: {
someField: ({ dataSource1, dataSource2 }) => {
return dataSource1.a || dataSource2.b
},
someOtherField: ({ dataSource1, dataSource2 }) => {
return someCondition ? dataSource1.foo : dataSource2.bar
},
}
}
Assuming your user resolver returns type User forsake...
type User {
id: ID!
datasource1: RandomType
datasource1: RandomType
}
You can create individual resolvers for each field in type User, this can reduce the complexity of the user Query, to only the requested fields.
query {
user {
id
datasource1 {
...
}
}
}
const resolvers = {
Query: {
user: () => {
return { id: "..." };
}
},
User: {
datasource1: () => { ... },
datasource2: () => { ... } // i wont execute
}
};
datasource1 & datasource2 resolvers will only execute in parallel, after Query.user executes.
For parallel call.
const users = async (_source, args, { dataSources }) => {
try {
const promises = [
dataSources.RESTAPI1,
dataSources.RESTAPI2
].map(({ getUser }) => getUser(args.id));
const data = await Promise.all(promises);
return Object.assign({}, ...data);
} catch (error) {
console.log("An error occurred.", error);
}
return [];
};
I am trying to push a user's choice as a string to their array of choices and return the updated document.
The route and function work successfully however it returns the User with an empty choice array. I believe the problem lies somewhere in the controller function but I cannot figure it out.
Any help is greatly appreciated!
To help, here is a screenshot of my console where you can see an empty choice array being returned.
Here is an image of my console.log
This is where I call the function
handleAnswerInput = (question) => {
let answerTextSelected = question.Text;
let answerTypeSelected = question.Type;
let usersName = this.state.user._id
this.setState({
count: this.state.count + 1
})
saveUserandScore(usersName, answerTextSelected)
.then(
this.loadQuestion(this.state.count)
)
console.log(answerTextSelected)
console.log(answerTypeSelected)
};
This is the controller function (updated from suggestions)
const saveUserAndTheirScore = (req, res) => {
let filter = { _id: req.params.id }
// let update = { choices: req.params.answer] }
console.log(req.params.id)
console.log(req.params.answer)
User.update(
{ filter },
{
$push: { choices: req.params.answer }
},
{
returnOriginal: false,
},
)
.then(dbData => res.json(dbData))
.catch(err => {
console.log(err);
res.json(err);
});
};
here is the axios call
export const saveUserandScore = (id, answer) => {
return axios.post(`/api/user/${id}/${answer}`);
};
you need to change user schema, in that you might have defined choices type as string. It must be an array.
findOneAndUpdate(filter, update, options, callback) has a returnOriginal option, if set to true (which is the default), it will return the document BEFORE the update. In your case, you might want to set it to false [1].
Unfortunately, the respective option for mongoose is named new [2].
[1] https://mongodb.github.io/node-mongodb-native/3.4/api/Collection.html#findOneAndUpdate
[2] https://mongoosejs.com/docs/api.html#query_Query-findOneAndUpdate
I'm running a GraphQL server using the serverless framework on AWS Lambda.
I'm fetching the data in the UI using apollo-link-batch-http.
If I run it locally using serverless-offline, it works fine. But if I run it on AWS Lambda, it successfully resolves the fooResolver but not the barResolver as it throws the above error message.
The Model.cached(300) is a tiny cache wrapper I made. You can see it here:
https://gist.github.com/lookapanda/4676083186849bb6c5ae6f6230ad7d8f
It basically just makes me able to use my own findById function and so on.
The weird thing is, this error only appears, if I use apollo-link-batch-http but not if I use apollo-link-http. So if the request is batched into a single GraphQL request, there is no such errors (although, then I get this error: https://github.com/sequelize/sequelize/issues/9242)
I really don't know what is going on there, there is no raw where query in any of those resolvers. And it gets even weirder: It only happens with the cached result. The first request is totally valid and successful, but then every consecutive request fails with the above error message.
I really hope someone can help me, I'm getting insane :D
export const fooResolver = async () => {
const Model = db.getDB().sequelize.models.fooModel;
const data = await Model.cached(300).findAll({
where: {
time: {
[Op.gt]: Model.sequelize.literal('CURRENT_TIMESTAMP()'),
},
enabled: true,
state: 'PLANNED',
},
order: [['time', 'DESC']],
limit: 5,
});
return data.value;
};
export const barResolver = async () => {
const models = db.getDB().sequelize.models;
const Model = models.fooModel;
const data = await Model.findById(data.id, {
include: [
{
model: models.barModel,
include: [
{
association: 'fooAssociation',
include: [{ association: 'barAssociation' }],
order: ['showOrder', 'ASC'],
},
],
},
],
});
return {
data,
};
};
I faced similar situation, except in my case using the code below works well:
.findAll({
where: {
title: req.params.title
}
})
Okay, so after tedious debugging I found out that in the cacheable wrapper I was using this snippet:
https://github.com/sequelize/sequelize/issues/2325#issuecomment-366060303
I don't really know still, why exactly this error only showed up on Lambda and not locally, but it stopped erroring when I only used the selectQuery() method and only returned that instead of the whole Model.addHook stuff and so on. So basically changed this
export const getSqlFromSelect = (Model, method, args) => {
if (!SUPPORTED_SELECT_METHODS.includes(method)) {
throw new Error('Unsupported method.');
}
const id = generateRandomHash(10);
return new Promise((resolve, reject) => {
Model.addHook('beforeFindAfterOptions', id, options, => {
Model.removeHook('beforeFindAfterOptions', id);
resolve(
Model.sequelize.dialect.QueryGenerator.selectQuery(
Model.getTableName(),
options,
Model
).slice(0, -1)
);
});
return Model[method](...args).catch(reject);
});
};
to this
export const getSqlFromSelect = (Model, identifier, options) => {
if (typeof identifier === 'number' || typeof identifier === 'string' || Buffer.isBuffer(identifier) {
options.where = {
[Model.primaryKeyAttribute]: identifier,
};
};
return Model.sequelize.dialect.QueryGenerator.selectQuery(
Model.getTableName(),
options,
Model
).slice(0, -1);
};