Get count and specify range in a supabase js query - reactjs

Im making a recipe website with nextjs and supabase. I have an occasions table which has a foreign key to many recipes. I can easily query the occasions for all recipe in that occasion however I want to paginate so I need a count and a range. How would I go about doing this or would I have to make an RPC instead?
Here is my current query that returns all recipes in an occasion - comments in the query is what I want to put a count/range on
.from("occasions")
.select(
`*,
recipes_occasions(
recipes(
// I WANT TO GET EXACT COUNT OF ALL RECIPES IN AN OCCASION HERE
recipeId, title, slug, totalTime, yield, ingredients,
recipes_categories (
categories(title, slug)
),
recipes_diets (
diets(title, slug)
),
recipes_cuisines (
cuisines(title, slug, code)
)
),
)`
)
.eq("slug", slug )
.range(0,4)
// I WANT TO TAKE SPECIFIC RANGE HERE (e.g. range(12, 24) or range(200, 212)

There are multiple ways you could solve this problem. Splitting it up to two separate queries is certainly one option.
Another option might be to flip the query and get the recipes along with it's associated occasions.
const { data, error } = supabase
.from("recipes")
-- Edit above fixed recipe typo
.select(
`*,
recipes_categories (
categories(title, slug)
),
recipes_diets (
diets(title, slug)
),
recipes_cuisines (
cuisines(title, slug, code)
),
occasions!inner(*)`,
{ count: 'exact' }
)
.eq("occasions.slug", slug )
.range(0,4)
count: 'exact' will get the number of recipes so that you can use it to paginate.
The !inner keyword used after occasions allows you to filter the results of recipes selected based on values of occasions.
-- Edit above fixed recipe typo

I managed to get the range working by first getting the occasionID from the slug and then another query to get the recipes with that occasion ID like so. Added count to destructure also.
const { data, error } = await supabase
.from("occasions")
.select(`occasionId`)
.eq("slug", context.query.slug )
const occasionId = await data[0].occasionId;
const { data: recipes, count, recipeError } = await supabase
.from('recipes_occasions')
.select(`*, recipeDetails: recipes(title)`, {count: 'exact'})
.eq('occasion_id', occasionId)
.range(0,1)
Result in my terminal
[
{
id: 134,
created_at: '2022-12-13T18:17:35.433285+00:00',
recipe_id: 'd7c493ff-6aae-4929-bf74-c6d44e2eb8f7',
occasion_id: '1c907e40-4717-4c22-aeb6-c51044fb276a',
recipeDetails: { title: 'Easter egg cheesecake' }
},
{
id: 135,
created_at: '2022-12-13T18:18:39.011853+00:00',
recipe_id: '30254e94-6692-4a57-a173-c5ccee758d59',
occasion_id: '1c907e40-4717-4c22-aeb6-c51044fb276a',
recipeDetails: { title: 'Rhubarb & custard tart' }
}
]
13

Related

Updating multiple queries in useMutation Apollo-client without refetching them

I'm executing this mutation in my NewBook component:
const [addBook] = useMutation(ADD_BOOK, {
update: (cache, response) => {
cache.updateQuery({ query: ALL_BOOKS }, ({ allBooks }) => {
return { allBooks: allBooks.concat(response.data.addBook) };
});
},
refetchQueries: [{ query: ALL_AUTHORS }, { query: ALL_GENRES }],
options: {
awaitRefetchQueries: true,}});
Instead of having to refetch those two queries, I'd like to update them like ALL_BOOKS - but could not find any example in the docs. Does anyone know a way to accomplish that?
Thank you.
What you need to do is make multiple cache updates based on response data.
Once you add your new book to the query, the next step is to fetch all authors.
cache.updateQuery({ query: ALL_BOOKS }, ({ allBooks }) => {
return { allBooks: allBooks.concat(response.data.addBook) };
});
//Get all authors
const existingAuthors = cache.readQuery({
query: ALL_AUTHORS,
//variables: {}
});
//If we never called authors, do nothing, as the next fetch will fetch updated authors. This might be a problem in the future is some cases, depending on how you fetch data. If it is a problem, just rework this to add newAuthor to the array, like allAuthors: [newAuthor]
if(!existingAuthors.?length) {
return null
}
The next thing is that we need to compare the new book's author with existing authors to see if a new author was added.
//continued
const hasAuthor = existingAuthors.find(author => author.id === response.data.createBook.id)
//Double check response.data.createBook.id. I don't know what is returned from response
//If author already exists, do nothing
if(hasAuthor) {
return null
}
//Get a new author. Make sure that this is the same as the one you fetch with ALL_AUTHORS.
const newAuthor = {
...response.data.createBook.author //Check this
}
cache.writeQuery({
query: ALL_AUTHORS,
//variables: {}
data: {
allAuthors: [newAuthor, ...existingAuthors.allAuthors]
},
});
Then continue the same with ALL_GENRES
Note:
If you called ALL_GENERES or ALL_BOOKS with variables, you MUST put the SAME variables in the write query and read query. Otherwise Apollo wont know what to update
Double check if you are comparing numbers or strings for authors and genres
Double check all of the variables I added, they might be named different at your end.
Use console.log to check incoming variables
You can probably make this in less lines. There are multiple ways to update cache
If it doesn't work, console.log cache after the update and see what exactly did apollo do with the update (It could be missing data, or wrong variables.)
Add more checks to ensure some cases like: response.data returned null, authors already fetched but there are none, etc...

SelectInput react admin to filter duplicate values in choices and display only unique values

I am using react-admin SelectInput and uses name as options but there are in the list that has the same name so the option shows duplicate values. I have tried doing research about filtering duplicate values but I can't seem to make it work.
My data looks like this:
data: [{name: "car1",id: 1,}, {name: "car1",id: 2,}, {name: "car2",id: 3,}]
Select options will display: car1, car 1, car 2
I want to display no duplicate: car1, car2
Here is my code:
<SelectInput optionText={<Choice/>}/>
const Choice = ({...record}) => {
return(
<span>{`${record && record.record.carName}`}</span>
)
}
It allows me to play with the select options below like this and it display "test" so I think it can be done here but I cant find a good logic to implement the filter. Anyone has any idea? I'd greatly appreciate it!
`${record && record.record.carName}` !== 'car1' ? <span>test</span> : null
You may filter with Set. Set stores only unique values. We filter the data based on whether the name already exists in the Set or not. I hope you don't care about the id. As the first item will remain and the last items will be removed as duplicates. Hope the idea is clear.
let s = new Set();
let newData = data.filter(d => {
if (!s.has(d.name)) {
s.add(d.name);
return d;
}
});
If all you want is that there are no duplicates in the selection, I would suggest that you filter them before sending them to SelectInput.
You can do this by the following:
const data = [{name: "car1",id: 1,}, {name: "car1",id: 2,}, {name: "car2",id: 3,}];
const uniqueChoices = data.reduce((acc, choice) => {
if (acc.some(item => item.name === choice.name) {
return acc;
}
return [...acc, item]
}, []);
<SelectInput choices={uniqueChoices} />

How can I accessed upsertedId when using updateOne in MongoDB

I have new data that I want to insert in my array of blog (My collection look like this - shown below):-
{
_id: 0,
// other vars here...,
blog: [
{
_id: 0,
title: 'Article 1'
},
// add new article here
]
}
Right now I'm able to add new article in my blog array with the code shown below:-
const query = {}
const update = { $push: { blog: inputData } }
const options = { upsert: true }
All.updateOne(query, update, options)
.then(result => {
res.redirect(`/article/${result.upsertedId}`) // read MongoDB documentation that I can get the newly inserted data ID by using result.upsertedId
})
.catch(err => res.redirect(`/`)
But I can't seem to get the ID of the newly inserted data into my collection. Using result.upsertedId returns me undefined instead. According to the code above, I need the ID in order to redirect user to the new article page of that ID.
Here's the documentation telling that it will return upsertedId updateOne
this is the result I get when console.log it (There's no upsertedId anywhere in it)
Print your result to confirm an ID is being received, your result is likely something similar to this:
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
You could try using collection.findOneAndUpdate and retrieve upsertedId from that result
You need to pass new: true in the option to get the id of the updated document
i.e
const query = {}
const update = { $push: { blog: inputData } }
const options = { upsert: true, new: true }
All.updateOne(query, update, options)
.then(result => {
//result is your upserted document
console.log("Upserted document : ", result);// try getting your upserted _id
//from this result
res.redirect(`/article/${result.upsertedId}`) // change //result.upsertedId into result._id or result.id whatever you get into //your result
})
.catch(err => res.redirect(`/`)
It is not applicable for nested documents.
What you are doing is an update operation.
Doc says
The _id value of the document inserted by an upsert operation. This value is only present when the upsert option is enabled and the update query does not match any documents.
And your query results in update operation. That's why you are getting nModified.
One option is that you can use findOneAndUpdate with option returnNewDocument option true.

Nested sort in GraphQL

I'm using Contentful as a CMS, wherein I have the content models "Category" and "Subcategory". Each subcategory has a category as its parent. Both models have a property called order which I set within the CMS to decide the order in which they will appear on the navigation.
Here's my GraphQL query:
query {
allContentfulCategory(sort: {fields: [order, subcategory___order]}) {
edges {
node {
order
title
subcategory {
order
title
}
}
}
}
}
The first sort works correctly and the query outputs my categories by the order field, rather than in reverse-creation order which seems to be the default output from Contentful (the newest categories show first).
However, my second sort of subcategory___order, which was inspired by this blog post, does not evaluate. In other words, the subcategories (per Category) show up in reverse order so that the top item has an order of 8, and it decreases to 7, and 6, and so on.
For example, say I had category "Animals" with order 2, and "Trees" with order 1. My content in reverse-creation order: "Cat" (order 3), "Dog" (order 1), "Chicken" (order 2), "Birch" (order 2), "Oak" (order 3), and "Willow" (order 1).
These will come through the query like so:
Trees (1):
Birch (2)
Oak (3)
Willow (1)
Animals (2):
Cat (3)
Dog (1)
Chicken (2)
I can't find a way to make the GraphQL query perform both sort commands simultaneously so that my data is ordered according to the order I set in the CMS.
So many thanks in advance!
Edit: Here's a component solution:
Within each category node that comes from my query, I can access the subcategories and their order in an array.
const categories = data.allContentfulCategory.edges
{categories.map((category) => {
return (
<div>
{category.node.subcategory && category.node.subcategory.sort((a, b) => {return a.order - b.order}).map((subcategory) => {
return (
<div>
<p>{subcategory.title}</p>
</div>
)
})}
</div>
)
})}
The key element here is how I sort the array by subcategory order within the component:
category.node.subcategory.sort((a, b) => {return a.order - b.order})
To be clear, this is not a satisfying solution, nor does it answer my question about how I might write multiple GraphQL sorts. But this code does do what I wanted with regards to organising the subcategories.
You are querying category and not subcategory:
query {
allContentfulCategory(sort: {fields: [order, subcategory___order]}) {
edges {
node {
order
title
subcategory {
order
title
}
}
}
}
}
In the query above, only the returned array data.allContentfulCategory.edges is sorted. Gatsby won't sort the data nested in category.subcategory.
Further more, the second field you specified (subcategory___order) will only be considered if the first field (order) is the same. Since each category has a unique order, subcategory will never be used.
If it's possible for you to query for something like allContentfulSubcategory instead, it will work.
Otherwise, you can move the sort logic to build time instead by using createSchemaCustomization. I don't know what your schema look like so this is just an example:
// gatsby-node.js
exports.createSchemaCustomization = ({ actions, schema }) => {
const { createTypes } = actions
const typeDefs = [
schema.buildObjectType({
name: "ContentfulCategory",
fields: {
subcategory: {
type: "[ContentfulSubcategory]",
resolve: (src) => {
const subcategories = src.subcategory
return subcategories.sort()
}
},
},
interfaces: ["Node"],
extensions: { infer: true },
}),
]
createTypes(typeDefs)
}
If you need to use createSchemaCustomization, see the Gatsby docs on the topic here.
The docs' info could be overwhelming, I also wrote a bit about this API here. It is more about field extension, but the resolver function is basically the same.

How to search and return objects by value in javascript (and maybe lodash)

I have a complex query I'm trying to make. I've got courses that have 9 weeks. I'm trying to trigger an email based on the dates of those nine weeks. So the course is like...
jf892iejf929j
{name: Course Name,
month1: date,
month2: date,
month3: date
}
Now, if the date of one of those months is, let's say today, I want to send an email. This is what I have right now.
const ia = _(courses)
.map((course, id) => ({id, ...course}))
.filter(course => course.course === 'Course Name')
.filter(course => moment(new Date(course.month9)).isSameOrAfter(moment(new Date())))
.map(course => {
if (moment(new Date(course.month1)).isSame(moment(new Date()))) {
HERE IS WHERE I'M TRYING TO AVOID 9 IF THENS
}
})
.value()
I'm trying to get something back like
{courseId: courseId,
coursetype: Course,
month: month1 (the original key,
date: date (original value of month1 key
}
I hope this makes sense... Is there an easier way to find just the months that match the date and then hand back data?
EDITED QUESTION FOR CLARITY
So, I found a local mentor here to walk me through this, and here is what we did.
Instead of iterating inside the lodash query, we returned the value
const ia = _(courses)
.map((course, id) => ({id, ...course}))
.filter(course => course.course === 'Course Name')
.filter(course => moment(new Date(course.month9)).tz("America/Phoenix").isSameOrAfter(moment(new Date(), 'day').tz("America/Phoenix")))
.value()
Then, we created the final object like this:
var finalObj = [];
for (var i = 0; i<ia.length; i++) {
for (var key in ia[i]) {
if (key.indexOf("month") >= 0) {
if (moment(new Date(ia[i [key])).tz("America/Phoenix").isSame(moment(new Date('Mon Jul 10 2017 20:00:00 GMT-0700 (MST)'), 'day').tz("America/Phoenix"))) {
finalObj.push({
"courseId": ia[i].id,
"courseName": ia[i].course,
"month": key,
"date": ia[i][key]
});
}
}
}
}
This did achieve the result I wanted, but I'm wondering if there isn't a way with ES6 and Lodash to achieve the same effect.
It's a very interesting question - something that I have seen come up often, but not often addressed. If you are consuming a public api, it can be near impossible to change it on the backend.
From what I have seen, no, there is no "right" way of doing this, but for the sake of testing (and keeping future devs sane) I would encourage breaking it down a bit more.
For example, take your final snippet here:
const ia = _(courses)
.map((course, id) => ({id, ...course}))
.filter(course => course.course === 'Course Name')
.filter(course => moment(new Date(course.month9)).tz("America/Phoenix").isSameOrAfter(moment(new Date(), 'day').tz("America/Phoenix")))
.value()
That's great, and looks all slick because you have a bunch of one liners, but revisiting it later, eh, not so much.
You could do something like:
// -- BusinessRules.js --
/**
* Does x, y, z check
*/
export const createCourse = (course, id) => ({id, ...course});
/**
* Does x, y, z check
*/
export const isCourseX = course => course.course === 'Course Name';
/**
* Does x, y, z check
*/
function isTimezone(course, timezone) {
return moment(new Date(course.month9))
.tz(timezone)
.isSameOrAfter(
moment(new Date(), 'day').tz(timezone)
);
}
// Use some partial magic to create expressive verbs
export const isTimeZoneArizona = _.partial(isTimezone, "America/Phoenix");
export const isTimeZoneMelbourne = _.partial(isTimezone, "Aussie/Melbourne");
// -- App.js --
const ia = _(courses)
.map(createCourse)
.filter(isCourseX)
.filter(isTimeZoneArizona)
.value();
It looks similar to yours, but you can now build up an entire collection of business rules, as complex as you like, without "muddying" the main flow.
It's also easier to test, as you can easily test each "rule" by itself and in isolation.
You can also easily add more rules as required. The rules, once tested, can also then be shared with other filter sequences.
Edit
As a sidenote, avoid chains, they can come back to haunt you - plus they require you pulling in all of lodash to use them.
With the refined methods presented above, you can simplify it anyway with something like:
const a = courses.map(createCourse).filter(isCourseX).filter(isTimeZoneArizone);

Resources