Prisma, Can't use '_count' in 'FindMany' query to get total count of the data - prisma

const fictions = await client.fiction.findMany({
**_count: true,**
take: 18,
skip: (+page!.toString() - 1 || 0) * 18,
where: {
AND: [
{ OR: [...genresMany] },
{
OR: [...nationalitiesMany],
},
{
AND: [...keywordMany],
},
],
},
include: {
_count: {
select: {
favs: true,
},
},
author: true,
},
...sortingOne(),
});
I'd like to get total count of the query for pagination. And the manual says the using _count is the way.
However, it seems _count cannot be used in the query, and I can't find out the reason.
Is it a wrong usage?
If it's wrong, then what can I do for getting the total count of the query (not 18, the taked items)?
Should I count them again with the similar code after the query? I think that's too wasteful.
Would be very thanks if there's any help.

there should be a top-level count method for your model. You could that first to just return a count, that request should be fast and cheap since it has to do no sorting / fetching of data. And then you can do a separate call to actually return data and paginate through it.
const fictions = await client.fiction.count({
where: {
AND: [
{ OR: [...genresMany] },
{
OR: [...nationalitiesMany],
},
{
AND: [...keywordMany],
},
],
}
});

Related

use where query depends on query on prisma

const fictions = await client.fiction.findMany({
where: {
AND: [
{ genre: genres as string },
{ nationality: nationalities as string },
{
keywords: {
some: {
keyword: {
name: keywords?.toString().split(",").join(" | "),
},
},
},
},
],
},
include: { ~~~
},
orderBy: {
userFictionStat: {
total: "asc",
},
},
});
Hi, I'm new to prisma so stuck in createing api pages.
I want to filter my 'fictions' with multiple criteria
('genre' or 'nationality' or 'keywords').
The problem is that when 'genre' or 'nationality' or 'keywords' are 'undefined'(button unclicked), the result becomes 0.(because of the 'AND').
I can't find out how can I filter my fictions with given criteria(with clicked buttons, even if some buttons are unclicked).
Also, I'd like to know how can I sort my fictions using my queries.
(lik if i get query {sorting : totalpoint} I can sory by totalPoint, and if i get {sorting : title} then I can sort by title name.
If anyone could help, I would be very thank you.
If genres can be undefined or false-y (including '' or "") then you can do typeof genres === "string" ? genres : undefined as part of your filter. Same for nationalities. That way you know you're passing a string which has meaningful content OR you are ignoring the filter.

Prisma splice Item from Array

I have been pushing updates to an array and was wondering if there is a built-in method to remove an entry from the array as well. Basically reversing the push command. I suspect that I have to query all documents and remove the item myself. But maybe there is some functionality I was unable to find inside the documentation.
Push:
const addTag = await prisma.post.update({
where: {
id: 9,
},
data: {
tags: {
push: 'computing',
},
},
})
Remove Expectation:
const removeTag = await prisma.post.update({
where: {
id: 9,
},
data: {
tags: {
splice: 'computing',
},
},
})
As of writing, there's no method to splice/remove items from a scalar list using Prisma. You would have to fetch the scalar list from your database, modify it manually in your application code and overwrite the record in your database with an update operation.
There is a feature request for this, please feel free to follow/comment with your use-case to help us track demand for this feature.
const { dogs } = await prisma.user.findOne({
where: {
id: userId
},
select: {
dogs: true
},
});
await prisma.user.update({
where: {
id: userId
},
data: {
dogs: {
set: dogs.filter((id) => id !== 'corgi'),
},
},
});

How do I count all the documents in a collection and use the cont in a controller, with MongoDB and Express.js?

I am working on a blogging application (click the link to see the GitHub repo) with Express (version 4.17.1), EJS and MongoDB (version 4.0.10).
Trying to paginate the posts I did the following, in the controller:
exports.getPosts = (req, res, next) => {
const perPage = 5;
const currPage = req.query.page ? parseInt(req.query.page) : 1;
let postsCount = 0;
const posts = Post.find({}, (err, posts) => {
postsCount = posts.length;
let pageDecrement = currPage > 1 ? 1 : 0;
let pageIncrement = postsCount >= perPage ? 1 : 0;
if (err) {
console.log('Error: ', err);
} else {
res.render('default/index', {
moment: moment,
layout: 'default/layout',
website_name: 'MEAN Blog',
page_heading: 'XPress News',
page_subheading: 'A MEAN Stack Blogging Application',
currPage: currPage,
posts: posts,
pageDecrement: pageDecrement,
pageIncrement: pageIncrement
});
}
})
.sort({
created_at: -1
})
.populate('category')
.limit(perPage)
.skip((currPage - 1) * perPage);
};
And in the view:
<a class="btn btn-primary <%= pageDecrement == 0 ? 'disabled' : '' %>" href="/?page=<%= currPage - pageDecrement %>">← Newer Posts</a>
and
<a class="btn btn-primary <%= pageIncrement == 0 ? 'disabled' : '' %>" href="/?page=<%= currPage + pageIncrement %>">Older Posts →</a>
That works fine unless there are is a number of posts equal to perPage x N, where N is an integer, in which case the "Older Posts" button becomes disabled one page too late.
That is because postsCount = posts.length counts the posts after they are limited by .skip((currPage - 1) * perPage).
So I need to count the posts from the model/collection and bring that count variable in the controller.
My model:
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
short_description: {
type: String,
required: true
},
full_text: {
type: String,
required: true
},
category: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category'
},
post_image: {
type: String,
required: false
},
updated_at: {
type: Date,
default: Date.now()
},
created_at: {
type: Date,
default: Date.now()
}
});
module.exports = mongoose.model('Post', postSchema);
How do I count all the documents in the posts collection and use that number in the posts controller?
This can be done easier with mongodb aggregation framework.
We use $facet aggregation to get the paginated data along with the total number of documents.
In aggregation framework we use $lookup instead of mongoose populate. $lookup returns an array, to get the first item in array we use $arrayElemAt operator inside $addFields.
Playground
And here is the code to apply to your app:
(The first $match aggregation is unnecessary here, but I put in in case you may need it in the future)
exports.getPosts = async (req, res, next) => {
const perPage = 5;
const currPage = req.query.page ? parseInt(req.query.page) : 1;
const skip = (currPage - 1) * perPage;
try {
const result = await Post.aggregate([{
$match: {},
},
{
$sort: {
created_at: -1,
},
},
{
$lookup: {
from: "categories",
localField: "category",
foreignField: "_id",
as: "category",
},
},
{
$addFields: {
category: {
$arrayElemAt: ["$category", 0],
},
},
},
{
$facet: {
totalRecords: [{
$count: "total",
}, ],
data: [{
$skip: skip,
},
{
$limit: perPage,
},
],
},
},
]);
let postsCount = result[0].totalRecords[0].total;
const pageCount = Math.ceil(postsCount / perPage);
const pageDecrement = currPage > 1 ? 1 : 0;
const pageIncrement = currPage < pageCount ? 1 : 0;
const posts = result[0].data;
res.render("default/index", {
moment: moment,
layout: "default/layout",
website_name: "MEAN Blog",
page_heading: "XPress News",
page_subheading: "A MEAN Stack Blogging Application",
currPage,
posts,
pageDecrement,
pageIncrement,
});
} catch (err) {
console.log("Error: ", err);
res.status(500).send("something went wrong");
}
};
By the way, in the post schema, for date fields you use default: Date.now(), this will cause the date value always the same value, it should be in this format: default: Date.now
Read $facet.
New in version 3.4.
Processes multiple aggregation pipelines within a single stage on the
same set of input documents. Each sub-pipeline has its own field in
the output document where its results are stored as an array of
documents.
Example: See here
db.collection.aggregate([
{
$facet: {
"count": [
{ $match: {} },
{ $count: "totalCount" }
],
"data": [
{ $match: {} },
{ $sort: { _id: -1 } },
{ $skip: 1 },
{ $limit: 2 }
]
}
}
])
Mongoose Version:
Model.aggregate([
{
$facet: {
"count": [
{ $match: {} },
{ $count: "totalCount" }
],
"data": [
{ $match: {} },
{ $sort: { _id: -1 } },
{ $skip: 1 },
{ $limit: 2 }
]
}
}
]).
then(res => console.log(res)).
catch(error => console.error('error', error));
In case of Mongoose you should use this:
https://mongoosejs.com/docs/api.html#aggregate_Aggregate-facet
Official Mongodb docs:
https://docs.mongodb.com/manual/reference/operator/aggregation/facet
General idea is to perform aggregation instead of multiple calls (1 for getting needed info + 1 to get the total count of documents)
You can perform 2 separate calls of course but it will hit your performance (not much for small data volumes but still...)
So you can get all needed data with .find() and then get count like this:
https://mongoosejs.com/docs/api.html#model_Model.count
PS. btw, use async/await instead of callbacks to avoid callback hell

Mongoose: consistently save document order

Suppose we have a schema like this:
const PageSchema = new mongoose.Schema({
content: String
order: Number
})
We want order to be always a unique number between 0 and n-1, where n is the total number of documents.
How can we ensure this when documents are inserted or deleted?
For inserts I currently use this hook:
PageSchema.pre('save', async function () {
if (!this.order) {
const lastPage = await this.constructor.findOne().sort({ order: -1 })
this.order = lastPage ? lastPage.order + 1 : 0
}
})
This seems to work when new documents are inserted.
When documents are removed, I would have to decrease the order of documents of higher order. However, I am not sure which hooks are called when documents are removed.
Efficiency is not an issue for me: there are not many inserts and deletes.
It would be totally ok if I could somehow just provide one function, say fix_order, that iterates over the whole collection. How can I install this function such that it gets called whenever documents are inserted or deleted?
You can use findOneAndDelete pre and post hooks to accomplish this.
As you see in the pre findOneAndDelete hook, we save a reference to the deleted document, and pass it to the postfindOneAndDelete, so that we can access the model using constructor, and use the updateMany method to be able to adjust orders.
PageSchema.pre("findOneAndDelete", async function(next) {
this.page = await this.findOne();
next();
});
PageSchema.post("findOneAndDelete", async function(doc, next) {
console.log(doc);
const result = await this.page.constructor.updateMany(
{ order: { $gt: doc.order } },
{
$inc: {
order: -1
}
}
);
console.log(result);
next();
});
Let's say you have these 3 documents:
[
{
"_id": ObjectId("5e830a6d0dec1443e82ad281"),
"content": "content1",
"order": 0,
"__v": 0
},
{
"_id": ObjectId("5e830a6d0dec1443e82ad282"),
"content": "content2",
"order": 1,
"__v": 0
},
{
"_id": ObjectId("5e830a6d0dec1443e82ad283"),
"content": "content3",
"order": 2,
"__v": 0
}
]
When you delete the content2 with "_id": ObjectId("5e830a6d0dec1443e82ad282") with findOneAndDelete method like this:
router.delete("/pages/:id", async (req, res) => {
const result = await Page.findOneAndDelete({ _id: req.params.id });
res.send(result);
});
The middlewares will run, and adjust the orders, the remaining 2 documents will look like this:
[
{
"_id": ObjectId("5e830a6d0dec1443e82ad281"),
"content": "content1",
"order": 0,
"__v": 0
},
{
"_id": ObjectId("5e830a6d0dec1443e82ad283"),
"content": "content3",
"order": 1, => DECREASED FROM 2 to 1
"__v": 0
}
]
Also you had better to include next in your pre save middleware so that other middlewares also work if you add later.
PageSchema.pre("save", async function(next) {
if (!this.order) {
const lastPage = await this.constructor.findOne().sort({ order: -1 });
this.order = lastPage ? lastPage.order + 1 : 0;
}
next();
});
Based on the answer of SuleymanSah, I wrote a mongoose plugin that does the job. This way, it can be applied to multiple schemas without unnessecary code duplication.
It has two optional arguments:
path: pathname where the ordinal number is to be stored (defaults to order)
scope: pathname or array of pathnames relative to which numbers should be given (defaults to [])
Example. Chapters should not be numbered globally, but relative to the book to which they belong:
ChapterSchema.plugin(orderPlugin, { path: 'chapterNumber', scope: 'book' })
File orderPlugin.js:
function getConditions(doc, scope) {
return Object.fromEntries([].concat(scope).map((path) => [path, doc[path]]))
}
export default (schema, options) => {
const path = (options && options.path) || 'order'
const scope = (options && options.scope) || {}
schema.add({
[path]: Number,
})
schema.pre('save', async function () {
if (!this[path]) {
const last = await this.constructor
.findOne(getConditions(this, scope))
.sort({ [path]: -1 })
this[path] = last ? last[path] + 1 : 0
}
})
schema.post('findOneAndDelete', async function (doc) {
await this.model.updateMany(
{ [path]: { $gt: doc[path] }, ...getConditions(doc, scope) },
{ $inc: { [path]: -1 } }
)
})
}

Distinct Query with Cloudant Connector using Loopback in API Connect/StrongLoop

I am trying to get distinct values for a query using Loopback with a Cloudant Connector, but I haven't found anything about this in the documentation.
e.g. I need a query to turn this:
[
{
rating: "★★★★★"
},
{
rating: "★★★★★"
},
{
rating: "★★★★★"
},
{
rating: "★★★★★"
},
{
rating: "★★★☆☆"
},
{
rating: "★★★☆☆"
}
]
into this:
[
{
rating: "★★★★★"
},
{
rating: "★★★☆☆"
}
]
I'm using the REST API to query my Products model (above is a filtered view of just the rating field). If there is some sort of filter that I can use without modifying the server that I somehow just missed in the documentation, that would be the best choice.
Is there any way I can add a distinct field like:
/Products?filter[fields][rating]=true?distinct=true
or how can I go about solving this?
Also, I've seen another answer talking about adding a remote method to solve this (something like this for mySQL):
Locations.regions = function (cb) {
var ds = Locations.app.datasources.myDS;
var sql = "SELECT DISTINCT region FROM Locations ORDER BY region"; // here you write your sql query.
ds.connector.execute(sql, [], function (err, regions) {
if (err) {
cb(err, null);
} else {
cb(null, regions);
}
});
};
Locations.remoteMethod(
'regions', {
http: {
path: '/regions',
verb: 'get'
},
returns: {
root: true,
type: 'object'
}
}
);
If this would work, how would I implement it with the Cloudant NoSQL DB connector?
Thanks!
If your documents looked like this:
{
"name": "Star Wars",
"year": 1978,
"rating": "*****"
}
You can create a MapReduce view, which emits doc.rating as the key and uses the build-in _count reducer:
function(doc) {
emit(doc.rating,null);
}
When you query this view with group=true, distinct values of rating will be presented with counts of their occurrence in the data set.