I'm facing a weird situation where I need to call a bunch of my CMS API routes from my server in order to use their response.data into an object that will then be passed to my client side.
This is the code that caches my data: meaning that when I change a field on my CMS, that data that is being pulled is not updated.
The code is:
let baseUrl = "https://sismographie-cms.herokuapp.com/api/"
let locales = ["?locale=en", "?locale=fr"]
let links = [
"contact-page",
"keywords",
"podcasts",
"random-question-arrays",
"featured-entries-headlines-anims",
"main-text",
"headline",
"cookie-setting",
"header-info-array",
"random-question-prompt",
"contact-page",
"map-entry-right-text",
"map-entry-left-text",
"sponsor-logos",
"credit",
"projects-about-texts"
].map((ele, index) => {
return {
en: `${baseUrl + ele + locales[0]}`,
fr: `${baseUrl + ele + locales[1]}`,
}
});
let objectKeys = [
"info",
"keywords",
"podcasts",
"randomQuestions",
"featuredEntries",
"balladosSubtitle",
"balladosTitles",
"cookiesSetting",
"headerInfoArray",
"randomQuestionPrompt",
"conctactPage",
"mapEntryRightText",
"mapEntryLeftText",
"sponsorLogos",
"credit",
"ProjectsAboutText"
];
let getAxiosRequests = (locale) => {
return links
.map((ele, index) => {
return axios.get(ele[locale])
})
};
axios.all(getAxiosRequests("fr"))
.then(axios.spread((...responses) => {
let cmsObjFr = mapToObject(objectKeys, responses);
axios.all(getAxiosRequests("en"))
.then(axios.spread(
(...responses) => {
let cmsObjEn = mapToObject(objectKeys, responses);
console.log(cmsObjEn);
app.get('/cms-routes', (req, res) => {
res.json({fr: cmsObjFr, en: cmsObjEn})
})
})).catch(errors => {
console.error(errors);
});
})).catch(errors => {
console.error(errors);
});
const mapToObject = (objectKeys, responses) => {
return objectKeys.reduce(
(sum, key, index) => Object.assign(
sum, { [key]: responses[index].data.data}),{}
);
};
When I access the json object, I see that the field I just changed did not update.
When I individually call that same field's CMS route, however, the response contains the updated version of the data:
app.get("/credits", (req, res ) => {
console.log("/credits' call");
axios.get("https://sismographie-cms.herokuapp.com/api/credit?locale=en")
.then(data => res.json(data.data))
})
For, let's say, the credit field, this method will give me the updated version I don't have access when I'm using the axios.spread method.
The problem is that because you create your route handler (app.get("/cms-routes")) after retrieving data, the data it responds with is fixed and will never change.
You need to move the data retrieval logic into the route handler.
Also, as mentioned above axios.all() and axios.spread() are deprecated and should not be used.
const links = {
info: "contact-page",
keywords: "keywords",
podcasts: "podcasts",
randomQuestions: "random-question-arrays",
featuredEntries: "featured-entries-headlines-anims",
balladosSubtitle: "main-text",
balladosTitles: "headline",
cookiesSetting: "cookie-setting",
headerInfoArray: "header-info-array",
randomQuestionPrompt: "random-question-prompt",
conctactPage: "contact-page",
mapEntryRightText: "map-entry-right-text",
mapEntryLeftText: "map-entry-left-text",
sponsorLogos: "sponsor-logos",
credit: "credit",
ProjectsAboutText: "projects-about-texts",
};
const baseURL = "https://sismographie-cms.herokuapp.com/api/";
/**
* Resolves with an array of single property objects, eg
* [
* {
* info: {...}
* },
* {
* keywords: {...}
* },
* ...
* ]
*/
const getAll = (locale) =>
Promise.all(
Object.entries(links).map(async ([key, link]) => ({
[key]: (await axios.get(link, { baseURL, params: { locale } })).data.data,
}))
);
app.get("/cms-routes", async (req, res) => {
const [fr, en] = await Promise.all([getAll("fr"), getAll("en")]);
res.json({ fr: Object.assign(...fr), en: Object.assign(...en) });
});
I've taken the liberty of simplifying your data structures so your links and object keys are tightly coupled.
Related
I want to apply server side search filter by text using redux toolkit.
I have two query builder methods in place. One for fetching all items and second for fetching only filtered data.
Query builder for fetching all items is
getAllBlogs: builder.query<BlogType[], void>({
queryFn: async () => {
const collectionRef = collection(Firestore, BLOG_COLLECTION)
const q = query(collectionRef, limit(1000))
const resp = await getDocs(q)
return {
data: resp.docs.map((doc) => doc.data() as BlogType),
}
},
providesTags: (result) => {
const tags: { type: 'Blogs'; id: string }[] = [
{ type: 'Blogs', id: 'LIST' },
]
if (result) {
result.forEach(({ id }) => {
tags.push({
type: 'Blogs',
id,
})
})
}
return tags
},
}),
This works fine and I'm getting the whole list through useGetAllBlogsQuery data.
Query builder for fetching filtered data is here: (Partially completed)
getBlogsByTitle: builder.query<BlogType[], string>({
queryFn: async (title) => {
const collectionRef = collection(Firestore, BLOG_COLLECTION)
const q = query(
collectionRef,
where('searchIndex', 'array-contains', title),
limit(1000),
)
const resp = await getDocs(q)
return {
data: resp.docs.map((doc) => doc.data() as BlogType), // Correct data
}
},
// I'm trying to only push the resultant items in state. This is not working
providesTags: (result) => {
const tags: { type: 'Blogs'; id: string }[] = []
if (result) {
result.forEach(({ id }) => {
tags.push({
type: 'Blogs',
id,
})
})
}
return tags
},
}),
I have react component looks like this where I'm calling these queries.
const Blogs: NextPage = () => {
const { data: blogs } = blogsApi.useGetAllBlogsQuery()
const [getBlogsByTitle] = blogsApi.useLazyGetBlogsByTitleQuery()
const debounced = useDebouncedCallback(async (value) => {
const { data } = await getBlogsByTitle(value)
console.log(data) // Correct data
}, 500)
return (
<div>
<InputText
onChange={(e) => debounced(e.target.value)}
/>
</div>
)}
The above code has two functionalities.
Fetch all the items on initial load.
Filter when debounced function is being called.
What I want is when getBlogsByTitle is called it will auto update the same state blogs in redux and we don't have to do much.
We are getting correct response in getBlogsByTitle but this query is not updating state with only its filtered response.
I'm new to redux-toolkit. Can someone help me out here where am I doing wrong ?
I am using redux-tookit, rtk-query (for querying other api's and not just Firebase) and Firebase (for authentication and db).
The code below works just fine for retrieving and caching the data but I wish to take advantage of both rtk-query caching as well as Firebase event subscribing, so that when ever a change is made in the DB (from any source even directly in firebase console) the cache is updated.
I have tried both updateQueryCache and invalidateTags but so far I am not able to find an ideal approach that works.
Any assistance in pointing me in the right direction would be greatly appreciated.
// firebase.ts
export const onRead = (
collection: string,
callback: (snapshort: DataSnapshot) => void,
options: ListenOptions = { onlyOnce: false }
) => onValue(ref(db, collection), callback, options);
export async function getCollection<T>(
collection: string,
onlyOnce: boolean = false
): Promise<T> {
let timeout: NodeJS.Timeout;
return new Promise<T>((resolve, reject) => {
timeout = setTimeout(() => reject('Request timed out!'), ASYNC_TIMEOUT);
onRead(collection, (snapshot) => resolve(snapshot.val()), { onlyOnce });
}).finally(() => clearTimeout(timeout));
}
// awards.ts
const awards = dbApi
.enhanceEndpoints({ addTagTypes: ['Themes'] })
.injectEndpoints({
endpoints: (builder) => ({
getThemes: builder.query<ThemeData[], void>({
async queryFn(arg, api) {
try {
const { auth } = api.getState() as RootState;
const programme = auth.user?.unit.guidingProgramme!;
const path = `/themes/${programme}`;
const themes = await getCollection<ThemeData[]>(path, true);
return { data: themes };
} catch (error) {
return { error: error as FirebaseError };
}
},
providesTags: ['Themes'],
keepUnusedDataFor: 1000 * 60
}),
getTheme: builder.query<ThemeData, string | undefined>({
async queryFn(slug, api) {
try {
const initiate = awards.endpoints.getThemes.initiate;
const getThemes = api.dispatch(initiate());
const { data } = (await getThemes) as ApiResponse<ThemeData[]>;
const name = slug
?.split('-')
.map(
(value) =>
value.substring(0, 1).toUpperCase() +
value.substring(1).toLowerCase()
)
.join(' ');
return { data: data?.find((theme) => theme.name === name) };
} catch (error) {
return { error: error as FirebaseError };
}
},
keepUnusedDataFor: 0
})
})
});
I am trying to create a function for my custom resolver that gets all documents in a collection and returns an amended payload with new data. Below is the code that im using to get one client and amend its data:
exports = (input) => {
const clientId = input._id;
const openStatusId = new BSON.ObjectId("898999");
const mongodb = context.services.get("mongodb-atlas");
const clientRecords = mongodb.db("db-name").collection("clients");
const jobRecords = mongodb.db("db-name").collection("jobs");
let client = clientRecords.findOne({"_id": clientId});
const query = { "client_id": clientId};
let jobsForClient = jobRecords.count(query)
.then(items => {
console.log(`Successfully found ${items} documents.`)
// items.forEach(console.log)
return items
})
.catch(err => console.error(`Failed to find documents: ${err}`));
let openJobs = jobRecords.count({"client_id": clientId,"status": openStatusId})
.then(numOfDocs => {
console.log(`Found ${numOfDocs} open jobs.`)
// items.forEach(console.log)
return numOfDocs
})
.catch(err => console.error(`Failed to find documents: ${err}`));
return Promise.all([client, jobsForClient, openJobs]).then(values => {
return {...values[0], "jobs": values[1], "openJobs": values[2]}
})
};
How can i fix this function to get all clients and loop over them to add data to each client?
I understand that changing this:
let client = clientRecords.findOne({"_id": clientId});
to this
let clients = clientRecords.find();
will get all the documents from the clients collection. How would i loop over each client after that?
UPDATE:
I have updated the function to the below and it works when running it in the realm environment but gives me an error when running it as a GraphQL query.
Updated code:
exports = (input) => {
const openStatusId = new BSON.ObjectId("999999");
const mongodb = context.services.get("mongodb-atlas");
const clientRecords = mongodb.db("db-name").collection("clients");
const jobRecords = mongodb.db("db-name").collection("jobs");
const clients = clientRecords.find();
const formatted = clients.toArray().then(cs => {
return cs.map((c,i) => {
const clientId = c._id;
const query = { "client_id": clientId};
let jobsForClient = jobRecords.count(query)
.then(items => {
console.log(`Successfully found ${items} documents.`)
// items.forEach(console.log)
return items
})
.catch(err => console.error(`Failed to find documents: ${err}`));
let openJobs = jobRecords.count({"client_id": clientId,"status": openStatusId})
.then(numOfDocs => {
console.log(`Found ${numOfDocs} open jobs.`)
// items.forEach(console.log)
return numOfDocs
})
.catch(err => console.error(`Failed to find documents: ${err}`));
return Promise.all([jobsForClient, openJobs]).then(values => {
return {...c, "jobs": values[0], "openJobs": values[1]}
});
})
}).catch(err => console.error(`Failed: ${err}`));
return Promise.all([clients, formatted]).then(values => {
return values[1]
}).catch(err => console.error(`Failed to find documents: ${err}`));
};
Error in GraphQL:
"message": "pending promise returned that will never resolve/reject",
It looks like you need wait for the last promise in your function to resolve before the function returns. I would do something like this:
exports = async (input) => {
...
let values = await Promise.all([jobsForClient, openJobs]);
return {...c, "jobs": values[0], "openJobs": values[1]};
}
Managed to solve by using mongodb aggregate. Solution below:
exports = async function(input) {
const openStatusId = new BSON.ObjectId("xxxxxx");
const mongodb = context.services.get("mongodb-atlas");
const clientRecords = mongodb.db("xxxxx").collection("xxxx");
const jobRecords = mongodb.db("xxxxx").collection("xxxx");
return clientRecords.aggregate([
{
$lookup: {
from: "jobs",
localField: "_id",
foreignField: "client_id",
as: "totalJobs"
}
},
{
$addFields: {
jobs: { $size: "$totalJobs" },
openJobs: {
$size: {
$filter: {
input: "$totalJobs",
as: "job",
cond: { "$eq": ["$$job.status", openStatusId]},
}
}
},
}
}
]);
};
I was solving a problem on a platform, but not getting how to do it?Can anyone help me out for this?
Problem Description
You are given the following API -
GET /api/comments
This will return a list of all comments. A comment object contains the following information
userId - ID of the user who commented
data - comment data
Given a userId, return an Array of comment data of all the comments by the given user.
Note
Apart from .json(), don’t use any other methods on the response object returned from fetch() call. This can cause your tests to fail.
Input - userId - the user id whose comment is to be returned.
Output - A list of comments by the given user id
Sample input 1 -
userId = 1
Sample API response
comments = [
{
'userId': '1',
"data": 'This looks slick!'
},
{
'userId': '2',
"data": 'I think this can be improved.'
},
{
'userId': '1',
"data": 'What kind of improvement?'
}]
Sample output 1 - ['This looks slick!', 'What kind of improvement?']
Below code that we have to complete
// TODO - Implement getCommentsByUserId() function
async function getCommentsByUserId(userId)
{
let v=await fetch(`/api/comments/${userId}`,{})
.then(response=>)
}
// ----------- Don't modify -----------
const mockFetch = (url, responseData) => {
const mockJsonPromise = Promise.resolve(responseData);
const mockFetchPromise = (callUrl) => {
if (url === callUrl) {
return Promise.resolve({
json: () => mockJsonPromise
});
} else {
return Promise.reject('404: No such url')
}
}
global.fetch = mockFetchPromise;
}
const successResponse = [
{
'userId': '1',
"data": 'This looks slick!'
},
{
'userId': '2',
"data": 'I think this can be improved.'
},
{
'userId': '1',
"data": 'What kind of improvement?'
}];
mockFetch('/api/comments', successResponse);
module.exports = getCommentsByUserId;
// ----------- Don't modify -----------
getCommentsByUserId("1").then((res) => {
console.log(res);
});
Use Array.prototype.filter:
let v = await fetch(`/api/comments/${userId}`,{})
.then(response=> response.json())
.then(comments => comments.filter(comment => comment.userId === userId))
You are lacking some knowledge on how async/await works. For filtering, as Alan has said, just use Array.prototype.filter.
When calling fetch, it returns a Promise. If you await fetch(...), the code will wait till fetch is finished, and returns the response of that API call.
async functions always returns a Promise (unless called with await). However, when writing the return statement in async functions, you must return a normal value.
async function getCommentsByUserId(userId)
{
let response = await fetch(`/api/comments/${userId}`,{});
let comments = response.json();
return comments.filter(comment => comment.userId === userId);
}
Either that, or using a normal function.
function getCommentsByUserId(userId)
{
return fetch(`/api/comments/${userId}`,{})
.then(res => res.json())
.then(comments => comments.filter(comment => comment.userId === userId));
}
Your test codes has many problems as well:
mockFetch('/api/comments', successResponse);: Your test code is mocking calling for the url "/api/comments/1", while you are mocking the API call for "/api/comments"
json: () => mockJsonPromise: The Response.json() functions is expected to return an actual value, and not a promise.
Here is my fixed version.
const mockFetch = (url, responseData) => {
return (callUrl) => {
if (url === callUrl) {
return Promise.resolve({
json: () => (responseData)
});
} else {
return Promise.reject('404: No such url')
}
};
}
const successResponse = [
{
'userId': '1',
"data": 'This looks slick!'
},
{
'userId': '2',
"data": 'I think this can be improved.'
},
{
'userId': '1',
"data": 'What kind of improvement?'
}];
global.fetch = mockFetch('/api/comments/1', successResponse);
async function getCommentsByUserId(userId) {
let response = await fetch(`/api/comments/${userId}`, {});
let comments = response.json();
return comments.filter(comment => comment.userId === userId);
}
getCommentsByUserId("1").then((res) => {
console.log(res);
});
I have made a few changes, now it's working fine but still, it doesn't clear any test cases.
However, it returns the same output as required.
// TODO - Implement getCommentsByUserId() function
async function getCommentsByUserId(userId) {
try{
let response = await fetch(`/api/comments`, {});
let comments = response.json();
comments = comments.filter(comment => comment.userId === userId);
a=[]
for(i of comments){
a.push(i.data)
}
return a;
}
catch{
return('404: No such url')
}
}
// ----------- Don't modify -----------
const mockFetch = (url, responseData) => {
const mockJsonPromise = Promise.resolve(responseData);
const mockFetchPromise = (callUrl) => {
if (url === callUrl) {
return Promise.resolve({
json: () => (responseData)
});
} else {
return Promise.reject('404: No such url')
}
}
global.fetch = mockFetchPromise;
}
const successResponse = [
{
'userId': '1',
"data": 'This looks slick!'
},
{
'userId': '2',
"data": 'I think this can be improved.'
},
{
'userId': '1',
"data": 'What kind of improvement?'
}];
mockFetch('/api/comments', successResponse);
module.exports = getCommentsByUserId;
// ----------- Don't modify -----------
getCommentsByUserId("1").then((res) => {
console.log(res);
});
I have type definitions for A and B defined in the schema.graphql file. Resolvers for these are auto-generated and work well (in other places).
To create a scoring mechanism that ranks nodes with label B in relation to the node with label A, I am writing a CustomResolver query that executes a cypher query and returns a collection of bs and a computed score as defined in the ScoredBs type.
The schema.graphql file looks like this.
type Query {
CustomResolver(idA: ID!, idsB: [ID!]!): [ScoredBs]
}
type A {
id: ID! #id
# and some more stuff of course
}
type B {
id: ID! #id
# more fields that use the #relation or #cypher decorator
}
type ScoredBs {
bs: [B]
score: Float
}
This is where the custom resolver is defined:
const resolvers = {
Query: {
CustomResolver: async (
parent,
{ idA, idsB },
context,
info
) => {
const cypher = `
MATCH (a:A {id: $idA})
MATCH (a)<--(b:B) WHERE b.id IN $idsB
WITH
b
RETURN DISTINCT collect(b) AS bs, rand() AS score
ORDER BY score DESC
`
const session = context.driver.session()
const results = await session
.run(cypher, { idA, idsB })
.then((result) => {
return result.records.map((record) => {
return {
bs: record.get('bs'),
score: record.get('score')?.low || null,
}
})
})
.catch(console.log)
.then((results) => {
session.close()
return results
})
return results
},
},
}
When I run the query in the apollo-server graphql playground i am receiving an error:
"message": "Resolve function for "B.id" returned undefined",
query {
CustomResolver(
idA:"2560886f-654b-4047-8d7a-386cd7d9f670",
idsB: ["01ec8367-a8ae-4600-9f88-ec141b2cae2c", "032f9a88-98c3-4968-8388-659ae26d63b3"]
) {
bs {
id
}
score
}
}
I solved this by rewriting part of the code:
const results = await session
.run(cypher, { idA, idsB })
.then((result) => {
return result.records.map((record) => {
// diff start
const obj = record.toObject()
const bs = obj.bs.map((b) => b.properties)
return {
bs, // diff end
score: record.get('score')?.low || null,
}
})
})
.catch(console.log)
.then((results) => {
session.close()
console.log(results)
return results
})
return results