I have 2 collection types in my Strapi setup: product and review where a product has many reviews.
I want to add 2 new fields to the response of /products and /products/:id:
averageRaing: number
totalReviews: number
I want to override the default find service to implement this, but I am unable to find the source code for strapi.query("product").find(params, populate) to override it.
If possible, I need this done in a single query rather than making multiple queries.
So far I have:
find(params, populate) {
return strapi.query("product").model.query(db => {
// I want the same query as when I run `strapi.query("product").find(params, populate)`
});
},
But I am unsure of how to handle the params and populate in the exact same way that .find(params, populate) does.
After digging into the source code, I found a solution:
const { convertRestQueryParams, buildQuery } = require("strapi-utils");
function find(params, populate) {
const model = strapi.query("product").model;
const filters = convertRestQueryParams(params);
const query = buildQuery({ model, filters });
return model
.query((qb) => {
const totalReviewsQuery = strapi.connections
.default("reviews")
.count("*")
.where("product", strapi.connections.default.ref("products.id"))
.as("total_reviews");
const averageRatingQuery = strapi.connections
.default("reviews")
.avg("rating")
.where("product", strapi.connections.default.ref("products.id"))
.as("average_rating");
query(qb);
qb.column("*", totalReviewsQuery, averageRatingQuery);
})
.fetchAll({
withRelated: populate,
publicationState: filters.publicationState,
})
.then((results) => results.toJSON());
}
Related
I'm trying to make a filter based on a Strapi relation.
My Strapi response looks like this:
I'm using Next Js for the frontend, so I assign props like so:
return {
props: {
projects: data.projects.data,
categories: categoriesData.data.categories.data,
},
}
I map through all of the project and list them all as cards. Then I want to filter them based on a button click. The buttons are the categories names and I map over that array as well. On click of each button I run a function called "handleProjects" and this is where I run into an issue.
I have previously made a filter using SanityCMS and Next Js, however, the code structure of the response in Sanity is much clearer compared to that of Strapi.
This is the code I've used in my previous project with Sanity:
const [filteredProducts, setFilteredProducts] = useState(products)
function handleProducts(e) {
let categoryType = e.target.value
setFilteredProducts(
products.filter((product) => product.category == categoryType)
)
}
I'm using this query where I alias the category as such and it is much simpler:
const productQuery = `*[_type == "product"] {
_id,
slug,
name,
image,
"category": category->name
}`
My function for my Strapi project looks like this:
function handleProjects(e) {
let categoryType = e.target.value
setFilteredProjects(
projects.filter((project) =>
project.attributes.categories.data.map((i) =>
i.attributes.title.includes(categoryType)
)
)
)
}
I'm having an array of objects, but at some point I have to map over an array of objects one level deeper and see if the categoryType matches some of the object values two levels deep.
Can anyone show me what I'm doing wrong here?
I'm trying to get the lastest records in a database. I have a field with their category. I'm trying to get 20 records, 5 of the latest post in each category. What I mean is that it should return 20 latest records but 5 of each category since the latest 20 does not necessarily mean a balanced 5 of each category. Basically what I have below, but I feel there's a better way and wasn't able to see it from reading the Sequelize docs. Thanks guys, I really appreciate it! Pardon the pseudocode. I have 4 categories. and im actually also sorting by createdAt timestamp field with limits and attributes and other checks/ error handling that I have not included for the sake of readability.
Posts.findAll({ where: { category: "Tech"}})
.then(techPosts => {
Posts.findAll({where:{category: "Science"},})
.then(sciencePosts => {
//actually 2 more nested findAlls before sending
const posts = [...techPosts, ...sciencePosts]
res.status(200).json(posts).end();
})
})
I would suggest fetching 5 different categories at the same time using "Promise.all".
The code would look like this:
const fetchTechPosts = () => {
return Posts.findAll({ where: { category: "Tech"}})
}
const fetchSciencePosts = () => {
return Posts.findAll({ where: { category: "Science"}})
}
const promises = Promise.all([fetchTech, fetchScience])
promises.then(posts => {
res.status(200).json(posts).end()
})
question is possibly a duplicate but I haven't found anything that provides an appropriate answer to my issue.
I have an ExpressJS server which is used to provide API requests to retrieve data from a MongoDB database. I am using mongoosejs for the MongoDB connection to query/save data.
I am building a route that will allow me to find all data that matches some user input but I am having trouble when doing the query. I have spent a long while looking online for someone with a similar issue but coming up blank.
I will leave example of the code I have at the minute below.
code for route
// -- return matched data (GET)
router.get('/match', async (req, res) => {
const style_data = req.query.style; // grab url param for style scores ** this comes in as a string **
const character_data = req.query.character; // grab url param for character scores ** this comes in as a string **
// run matcher systems
const style_matches = style_match(style_data);
res.send({
response: 200,
data: style_matches
}); // return data
});
code for the query
// ---(Build the finder)
const fetch_matches_using = async function(body, richness, smoke, sweetness) {
return await WhiskyModel.find({
'attributes.body': body,
'attributes.richness': richness,
'attributes.smoke': smoke,
'attributes.sweetness': sweetness
});
}
// ---(Start match function)---
const style_match = async function (scores_as_string) {
// ---(extract data)---
const body = scores_as_string[0];
const richness = scores_as_string[1];
const smoke = scores_as_string[2];
const sweetness = scores_as_string[3];
const matched = [];
// ---(initialise variables)---
let match_count = matched.length;
let first_run; // -> exact matches
let second_run; // -> +- 1
let third_run; // -> +- 2
let fourth_run; // -> +- 3
// ---(begin db find loop)---
first_run = fetch_matches_using(body, richness, smoke, sweetness).then((result) => {return result});
matched.push(first_run);
// ---(return final data)---
return matched
}
example of db object
{
_id: mongoid,
meta-data: {
pagemd:{some data},
name: whiskyname
age: whiskyage,
price: price
},
attributes: {
body: "3",
richness: "3",
smoke: "0",
sweetness: "3",
some other data ...
}
}
When I hit the route in postman the JSON data looks like:
{
response: 200,
data: {}
}
and when I console.log() out matched from within the style match function after I have pushed the it prints [ Promise(pending) ] which I don't understand.
if I console.log() the result from within the .then() I get an empty array.
I have tried using the populate() method after running the find which does technically work, but instead of only returning data that matches it returns every entry in the collection so I think I am doing something wrong there, but I also don't see why I would need to use the .populate() function to access the nested object.
Am I doing something totally wrong here?
I should also mention that the route and the matching functions are in different files just to try and keep things simple.
Thanks for any answers.
just posting an answer as I seem to have fixed this.
Issue was with my .find() function, needed to pass in the items to search by and then also a call back within the function to return error/data. I'll leave the changed code below.
new function
const fetch_matches_using = async function(body, richness, smoke, sweetness) {
const data = await WhiskyModel.find({
'attributes.body': body,
'attributes.richness': richness,
'attributes.smoke': smoke,
'attributes.sweetness': sweetness
}, (error, data) => { // new ¬
if (error) {
return error;
}
if (data) {
console.log(data)
return data
}
});
return data; //new
}
There is still an issue with sending the found results back to the route but this is a different issue I believe. If its connected I'll edit this answer with the fix for that.
I have an Ionic app, on the service I declared a function which is supposed to obtain multiple collections from a Firestore Database and must return all of them.
I have managed to get one collection on the service section like this:
read_Divisions() {
return this.firestore.collection('divisions').snapshotChanges();
}
And here is the the page typescript
ngOnInit() {
this.crudService.read_Divisions().subscribe(data => {
this.Divisions = data.map(e => {
return {
Name: e.payload.doc.data()['name'],
};
})
});
}
This is my idea of the service function for multiple collections:
read_Divisions() {
let divisions = this.firestore.collection('divisions').snapshotChanges();
let teams = this.firestore.collection('teams').snapshotChanges();
return [divisions,teams];
}
the way I obtain one collection on the page doesn't seem to be easily applicable for an array. What's the best way to go about it?
You can use forkJoin to wait for multiple observables, in that case making two separate queries, so probably the code would look like this:
readDivisions() {
return forkJoin({
divisions: this.firestore.collection('divisions').snapshotChanges(),
teams: this.firestore.collection('teams').snapshotChanges()
});
}
in another place in your code you would subscribe to readDivisions:
readDivisions.subscribe(result => console.log(result));
it will be smth like:
{divisions: [], teams: []}
This is a similar question to this.
I'm looking for help programmatically creating pages using createPage or createPages and am having trouble - the docs give an example for creating pages from markdown files but not much explanation.
I am using a plugin in plugins\characters\gatsby-node.js to add data from the Rick & Morty API to the GraphQL data layer. My plugin is at the bottom of the question in case it is relevant.
The plugin does add the data successfully as I can see the data in http://localhost:8000/___graphql, and I have successfully managed to use the data in (static) pages.
Where I am lost is that I would like to be able to create a page for each individual character, using the url characters/<characterIdHere> for each of the pages. I am aware that I need to add some logic to my (main or plugins version of....?) gatsby-node.js file, but this is the part I am stuck on. I do not know what I need to put into the gatsby-node.js file. The examples I can find all use json or markdown files and I would like to use data that I have pulled in (from an API) to gatsby's data layer. I have obviously researched this for a few hours and played around with it before asking, but not had any luck.
The component on the pages I would like to create should look something like this:
const CharactersViewSingle = ({ character}) => {
return (
<div>
<Helmet>
<title>{character.name && character.name}</title>
</Helmet>
<NavBar />
<CharactersViewBox character={character} width={300} height={520} />
</div>
)
}
The above code is taken from what the component returned when I was using create-react-app.
The graphQL query (which obviously reflects the structure of the data I would like to use) I use to get the data on other (static) pages looks like this:
export const query = graphql`
query CharactersQuery {
allCharacters(limit: 5) {
edges {
node {
id
name
status
gender
image
}
}
}
}
`
Plugin code:
const axios = require("axios")
exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
}) => {
const { createNode } = actions
const integerList = (start, length) =>
Array.from({ length: length }, (v, k) => k + start)
const rickMortyURL = `https://rickandmortyapi.com/api/character/${integerList(
1,
493
)}`
const rickMorty = await axios.get(rickMortyURL)
const query = await axios.get(rickMortyURL)
rickMorty.data.forEach(character => {
const nodeContent = JSON.stringify(character)
const nodeMeta = {
id: character.id.toString(),
//id: createNodeId(`char-data-${character.id}`),
parent: null,
children: [],
internal: {
type: `Characters`,
content: nodeContent,
contentDigest: createContentDigest(character),
},
}
const node = Object.assign({}, character, nodeMeta)
createNode(node)
})
}
Gatsby's createPages API is what you might be looking for.
I used it to create multiple pages like blog1, blog2, blog3 etc...
In the same way, you can create multiple pages for your characters.
Since you mentioned you have a graphql call to get your characters using
const pages = await graphql(`
query CharactersQuery {
allCharacters(limit: 5) {
edges {
node {
id
name
status
gender
image
}
}
}
}
`)
The above graphql call returns results in pages.data.allCharacters.edges
Now you can iterate them using foreach and use createPage to create the pages.
Below is complete mock code you might need to add in your gatsby-node.js file
const path = require('path');
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const templateOfYourCharacterPage = path.resolve(`src/templates/exampleTemplateFile.jsx`)
const pages = await graphql(`
query CharactersQuery {
allCharacters(limit: 5) {
edges {
node {
id
name
status
gender
image
}
}
}
}
`)
let characters = pages.data.allCharacters.edges;
characters.forEach(edge => {
createPage({
path: `/${edge.node.id}`,
component: templateOfYourCharacterPage,
context: {id: edge.node.uid, name: edge.node.name } // This is to pass data as props to your component.
})
})
}