Strapi GraphQL search by multiple attributes - graphql

I've got a very simple Nuxt app with Strapi GraphQL backend that I'm trying to use and learn more about GraphQL in the process.
One of my last features is to implement a search feature where a user enters a search query, and Strapi/GraphQL performs that search based on attributes such as image name and tag names that are associated with that image. I've been reading the Strapi documentation and there's a segment about performing a search.
So in my schema.graphql, I've added this line:
type Query {
...other generated queries
searchImages(searchQuery: String): [Image
}
Then in the /api/image/config/schema.graphql.js file, I've added this:
module.exports = {
query: `
searchImages(searchQuery: String): [Image]
`,
resolver: {
Query: {
searchImages: {
resolverOf: 'Image.find',
async resolver(_, { searchQuery }) {
if (searchQuery) {
const params = {
name_contains: searchQuery,
// tags_contains: searchQuery,
// location_contains: searchQuery,
}
const searchResults = await strapi.services.image.search(params);
console.log('searchResults: ', searchResults);
return searchResults;
}
}
}
},
},
};
At this point I'm just trying to return results in the GraphQL playground, however when I run something simple in the Playground like:
query($searchQuery: String!) {
searchImages(searchQuery:$searchQuery) {
id
name
}
}
I get the error: "TypeError: Cannot read property 'split' of undefined".
Any ideas what might be going on here?
UPDATE:
For now, I'm using deep filtering instead of the search like so:
query($searchQuery: String) {
images(
where: {
tags: { title_contains: $searchQuery }
name_contains: $searchQuery
}
) {
id
name
slug
src {
url
formats
}
}
}
This is not ideal because it's not an OR/WHERE operator, meaning it's not searching by tag title or image name. It seems to only hit the first where. Ideally I would like to use Strapi's search service.

I actually ran into this problem not to recently and took a different solution.
the where condition can be combined with using either _and or _or. as seen below.
_or
articles(where: {
_or: [
{ content_contains: $dataContains },
{ description_contains: $dataContains }
]})
_and
(where: {
_and: [
{slug_contains: $categoriesContains}
]})
Additionally, these operators can be combined given that where in this instance is an object.
For your solution I would presume you want an or condition in your where filter predicate like below
images(where: {
_or: [
{ title_contains: $searchQuery },
{ name_contains: $searchQuery }
]})
Lastly, you can perform a query that filters by a predicate by creating an event schema and adding the #search directive as seen here

Related

Dynamically create pages with Gatsby based on many Contentful references

I am currently using Gatsby's collection routes API to create pages for a simple blog with data coming from Contentful.
For example, creating a page for each blogpost category :
-- src/pages/categories/{contentfulBlogPost.category}.js
export const query = graphql`
query categoriesQuery($category: String = "") {
allContentfulBlogPost(filter: { category: { eq: $category } }) {
edges {
node {
title
category
description {
description
}
...
}
}
}
}
...
[React component mapping all blogposts from each category in a list]
...
This is working fine.
But now I would like to have multiple categories per blogpost, so I switched to Contentful's references, many content-type, which allows to have multiple entries for a field :
Now the result of my graphQL query on field category2 is an array of different categories for each blogpost :
Query :
query categoriesQuery {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
Output :
{
"data": {
"allContentfulBlogPost": {
"edges": [
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
},
{
"id": "568r9e48-t1i8-sx4t8-9742-cdf70c4ed789vtu",
"name": "Test2",
"slug": "test-2"
}
]
}
},
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
}
]
}
},
...
Now that categories are inside an array, I don't know how to :
write a query variable to filter categories names ;
use the slug field as a route to dynamically create the page.
For blogposts authors I was doing :
query authorsQuery($author__slug: String = "") {
allContentfulBlogPost(filter: { author: { slug: { eq: $author__slug } } }) {
edges {
node {
id
author {
slug
name
}
...
}
...
}
And creating pages with src/pages/authors/{contentfulBlogPost.author__slug}.js
I guess I'll have to use the createPages API instead.
You can achieve the result using the Filesystem API, something like this may work:
src/pages/category/{contentfulBlogPost.category2__name}.js
In this case, it seems that this approach may lead to some caveats, since you may potentially create duplicated pages with the same URL (slug) because the posts can contain multiple and repeated categories.
However, I think it's more succinct to use the createPages API as you said, keeping in mind that you will need to treat the categories to avoid duplicities because they are in a one-to-many relationship.
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
query {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
`)
let categories= { slugs: [], names: [] };
result.data.allContentfulBlogPost.edges.map(({node}))=> {
let { name, slug } = node.category2;
// make some checks if needed here
categories.slugs.push(slug);
categories.names.push(name);
return new Set(categories.slugs) && new Set(categories.names);
});
categories.slugs.forEach((category, index) => {
let name = categories.names[index];
createPage({
path: `category/${category}`,
component: path.resolve(`./src/templates/your-category-template.js`),
context: {
name
}
});
});
}
The code's quite self-explanatory. Basically you are defining an empty object (categories) that contains two arrays, slugs and names:
let categories= { slugs: [], names: [] };
After that, you only need to loop through the result of the query (result) and push the field values (name, slug, and others if needed) to the previous array, making the needed checks if you want (to avoid pushing empty values, or that matches some regular expression, etc) and return a new Set to remove the duplicates.
Then, you only need to loop through the slugs to create pages using createPage API and pass the needed data via context:
context: {
name
}
Because of redundancy, this is the same than doing:
context: {
name: name
}
So, in your template, you will get the name in pageContext props. Replace it with the slug if needed, depending on your situation and your use case, the approach is exactly the same.

Multiple graphql queries in Gatsby component

I need to run multiple graphQL queries within a component and within the gatsby-node.js file. (Because Prismic is limited to 20 entries per answer...🙄)
I tried the following, just to see if I could create the graphql loop in the default function:
export default () => {
async function allPosts() {
let data
await graphql(`
query allDitherImages {
prismic {
allProjects(sortBy: meta_firstPublicationDate_DESC) {
totalCount
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
edges {
node {
cover_image
cover_imageSharp {
name
}
}
}
}
}
}
`).then(initialRes => {
data = initialRes
})
return data
}
allPosts().then(result => {
console.log(result)
})
return null
}
But then Gatsby tells me that Gatsby related 'graphql' calls are supposed to only be evaluated at compile time, and then compiled away. Unfortunately, something went wrong and the query was left in the compiled code.
How can I run multiple graphql queries?
Thank you in advance :)
Michael
The gatsby-source-prismic-graphql package will create pages for all of your Prismic items (more than just the first 20), as it iterates over all items under the hood, so I'd advise looking into using that if you are looking to generate pages for all of those items.
But if you need to get all items and pass them in the pageContext or something, you'll need to do the recursion yourself in the gatsby-node.
In the gatsby-node, after you have defined the query, you can use something like this to iterate over the results and push to an array.
let documents = [];
async function getAllDocumentsRecursively (query, prop, endCursor = '') {
const results = await graphql(query, { after: endCursor })
const hasNextPage = results.data.prismic[prop].pageInfo.hasNextPage
endCursor = results.data.prismic[prop].pageInfo.endCursor
results.data.prismic[prop].edges.forEach(({node}) => {
documents.push(node)
});
if (hasNextPage) {
await getAllDocumentsRecursively(query, 'allDitherImages ', endCursor)
}
}
await getAllDocumentsRecursively(documentsQuery, 'allDitherImages ');
Then in your createPage, pass the array into the context:
createPage({
path: `/`+ node._meta.uid,
component: allDitherTempate,
context: {
documents: documents
}
})

How to transform Markdown query to Strapi query on GraphiQL?

I'm trying to get my hands on Gatsby + Strapi development (adding Material for styling), I'm new to both Gatsby and Strapi although I have some basic knowledge of React and it's making the way easier to follow.
I'm using this Gatsby Starter: https://www.gatsbyjs.org/starters/Vagr9K/gatsby-material-starter/ which includes the Material design I'm trying to achieve, but I'm having some trouble changing the Markdown queries to Strapi queries to avoid making a lot of code changes (posts are exactly the same but stored in Strapi). I have three Content Types in Strapi corresponding to the three different pages the original starter provides: Post, Category, and Tag.
This is the original MarkdownRemark graphQL query included in the post.jsx template:
query BlogPostBySlug($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
timeToRead
excerpt
frontmatter {
title
cover
date
category
tags
}
fields {
slug
date
}
}
}
How can I change it to retrieve the same info from Strapi?
I'm really new to this Strapi world so I'm having a lot of doubts with the GraphQL, as I can't follow the guide from the Markdown query because the Information displayed is not the same.
I'm also having trouble differentiating between allStrapiArticles and StrapiArticle, what's the main purpose of those?
EDIT: I've been testing the existing queries on GraphiQL to check what they are returning and this is what I'm seeing:
For the tag.jsx query:
query TagPage($tag: String) {
allMarkdownRemark(
limit: 1000
sort: { fields: [fields___date], order: DESC }
filter: { frontmatter: { tags: { in: [$tag] } } }
) {
totalCount
edges {
node {
fields {
slug
date
}
excerpt
timeToRead
frontmatter {
title
tags
cover
date
}
}
}
}
}
GraphiQL returns nothing:
{
"data": {
"allMarkdownRemark": {
"totalCount": 0,
"edges": []
}
}
}
For the category.jsx query:
query CategoryPage($category: String) {
allMarkdownRemark(
limit: 1000
sort: { fields: [fields___date], order: DESC }
filter: { frontmatter: { category: { eq: $category } } }
) {
totalCount
edges {
node {
fields {
slug
date
}
excerpt
timeToRead
frontmatter {
title
tags
cover
date
}
}
}
}
}
In this case, everything works fine and it retrieves article data.
And for the case of the query I've added as an example in this post (upper part of the question) I'm getting the following error:
"errors": [
{
"message": "Variable \"$slug\" of required type \"String!\" was not provided."
...
Make sure you are passing your variable in through Query Variables at the bottom of GraphiQL.
First, I would query AllMarkdownRemark to make sure you're getting the nodes from Gatsby. Something like:
query MyQuery {
allMarkdownRemark {
edges {
node {
fields {
slug
}
frontmatter {
title
}
}
}
}
}
If the slug is showing up, then this should work:
Sometimes a slug is not being generated. Which should show up checking allMarkdownRemark.

GraphQL disable filtering if filter variable is empty

I have a Gatsby GraphQL query for a list of posts ordered by date and filtered by category.
{
posts: allContentfulPost(
sort: {fields: [date], order: DESC},
filter: {category: {slug: {eq: $slug}}}
) {
edges {
node {
title {
title
}
date
}
}
}
}
Right now when $slug is the empty string "", I get
{
"data": {
"posts": null
}
}
Is there a way to get all posts instead?
You can use the regex filter to your advantage. If you pass an empty expression, then all posts will be returned because everything will match.
query Posts($slugRegex: String = "//"){
posts: allContentfulPost(
sort: {fields: [date], order: DESC},
filter: {category: {slug: {eq: $slugRegex}}}
) {
# Rest of the query.
}
}
By default, all posts will be returned (the $slugRegex is an empty regex if nothing was passed). When the $slugRegex becomes a meaningful expression, then only matching posts will show up.
As for passing the value, I'm assuming you're using gatsby-node.js to create pages. In that case, it's as simple as that:
// gatsby-node.js
exports.createPages = async ({ actions }) => {
const { createPage } = actions
// Create a page with only "some-slug" posts.
createPage({
// ...
context: {
slugRegex: "/some-slug/"
}
})
// Create a page with all posts.
createPage({
// ...
context: {
// Nothing here. Or at least no `slugRegex`.
}
})
}
It's not possible with this query, even #skip/#include directives won't help because you can't apply them on input fields.
I would suggest to either adjust the server side logic so that null in the 'eq' field will ignore this filter or either to edit the query being sent (less favorable imo).
It seems that the graphql schema that you work against lacks the filtering support you need..
If anyone requires a solution for other systems than Gatsby this can be accomplished using #skip and #include.
fragment EventSearchResult on EventsConnection {
edges {
cursor
node {
id
name
}
}
totalCount
}
query Events($organizationId: UUID!, $isSearch: Boolean!, $search: String!) {
events(condition: { organizationId: $organizationId }, first: 100)
#skip(if: $isSearch) {
...EventSearchResult
}
eventsSearch: events(
condition: { organizationId: $organizationId }
filter: { name: { likeInsensitive: $search } }
first: 100
) #include(if: $isSearch) {
...EventSearchResult
}
}
Then in your client code you would provide search and isSearch to the query and get your events like:
const events = data.eventsSearch || data.events

Relay mutation fragments intersection

I don't use Relay container, because I'd like to have more control over components. Instead of it I use HOC + Relay.Store.forceFetch, that fetches any given query with variables. So I have the following query:
query {
root {
search(filter: $filter) {
selectors {
_id,
data {
title,
status
}
},
selectorGroups {
_id,
data {
title,
}
}
}
}
}
Then I have to do some mutation on selector type.
export default class ChangeStatusMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation {selectors_status_mutation}`;
}
getVariables() {
return {
id: this.props.id,
status: this.props.status
};
}
getFatQuery() {
return Relay.QL`
fragment on selectors_status_mutationPayload{
result {
data {
status
}
}
}
`;
}
static fragments = {
result: () => Relay.QL`
fragment on selector {
_id,
data {
title,
status
}
}`,
};
getOptimisticResponse() {
return {
result: {
_id: this.props.id,
data: {
status: this.props.status
}
}
};
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
result: this.props.id
},
}];
}
}
Call mutation in component:
const mutation = new ChangeStatusMutation({id, status, result: selector});
Relay.Store.commitUpdate(mutation);
After mutation commitment selector in Relay storage is not changed. I guess that's because of empty Tracked Fragment Query and mutation performs without any fields:
ChangeStatusMutation($input_0:selectors_statusInput!) {
selectors_status_mutation(input:$input_0) {
clientMutationId
}
}
But the modifying selector was already fetched by Relay, and I pass it to the mutation with props. So Relay knows the type, that should be changed, how to find the item and which fields should be replaced. But can not intersect. What's wrong?
So, you're definitely a bit "off the ranch" here by avoiding Relay container, but I think this should still work...
Relay performs the query intersection by looking up the node indicated by your FIELDS_CHANGE config. In this case, your fieldIDs points it at the result node with ID this.props.id.
Are you sure you have a node with that ID in your store? I'm noticing that in your forceFetch query you fetch some kind of alternative _id but not actually fetching id. Relay requires an id field to be present on anything that you later want to refetch or use the declarative mutation API on...
I'd start by checking the query you're sending to fetch whatever this result type is. I don't see you fetching that anywhere in your question description, so I'm just assuming that maybe you aren't fetching that right now?

Resources