I am implementing basic pagination but startAfter isn't working as intended. I have attached a snippet of my function which returns the desired documents and the last document.
The first query is working fine and correct last document object is being returned. However, the second time I run this function with lastVisible variable set to the previously returned object the function returns exactly the same values as the first execution even though the else statement containing startAfter is executed.
I am using firebase-admin that's why I am not using newer methods like getDoc etc.
export const fetchPosts = async (lastVisible, uid) => {
if (!admin.apps.length)
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
})
const db = getFirestore()
try {
let querySnap
if (lastVisible === null)
querySnap = await db.collection("posts").where("userId", "==", uid).orderBy("time", "desc").limit(2).get()
else {
querySnap = await db.collection("posts").where("userId", "==", uid).orderBy("time", "desc").startAfter(lastVisible).limit(2).get()
}
const lastDoc = querySnap.docs[querySnap.docs.length - 1]
// console.log(typeof lastDoc)
const docSnaps = querySnap.docs
let data = []
for (let i in docSnaps) {
// console.log(docSnaps[i].data())
data.push(docSnaps[i].data())
}
// console.log(data)
// console.log(lastDoc)
return { lastDoc: lastDoc, docs: data }
} catch (e) {
console.log(e)
}
}
This is what the lastDoc value is after first execution (some values have been altered by me here)
{
_fieldsProto: {
title: { stringValue: 'test', valueType: 'stringValue' },
time: { integerValue: '1656157500080', valueType: 'integerValue' },
userId: {
stringValue: 'YHX',
valueType: 'stringValue'
},
postId: { stringValue: 'dJ4QY', valueType: 'stringValue' },
text: {
stringValue: 'ship',
valueType: 'stringValue'
}
},
_ref: {
_firestore: { projectId: 'project-t' },
_path: {
segments: [Array],
projectId: 'project-t',
databaseId: '(default)'
},
_converter: {}
},
_serializer: { allowUndefined: false },
_readTime: { _seconds: 1656332177, _nanoseconds: 366724000 },
_createTime: { _seconds: 1656157500, _nanoseconds: 603745000 },
_updateTime: { _seconds: 1656157500, _nanoseconds: 603745000 }
}
I was able to fix it using this answer.
But it doesn't make sense when you look at the documentation. It says
You can also pass a document snapshot to the cursor clause as the start or end point of the query cursor. The values in the document snapshot serve as the values in the query cursor.
That's what I was doing (at least in my opinion) but it wasn't working. So, instead of returning document snapshot I used the document itself and changed the startAfter query as follows
// using document instead of documentSnapshot
const lastDoc = querySnap.docs[querySnap.docs.length - 1].data()
// modified query
querySnap = await db.collection("posts").where("userId", "==", uid).orderBy("time", "desc").startAfter(lastVisible.time).limit(2).get()
Related
I have Mongoose CastError issue. I made a nodeJs API. At the specific route, it returns data appended with some other data. I saw many fixes available here but my scenario is different.
Here is my model and the problem occurs at fields property.
const deviceSchema = new Schema({
device_id: { type: String, required: true },
user_id: { type: Schema.Types.ObjectId, ref: 'User', require: true },
location_latitude: { type: String, default: '0' },
location_longitude: { type: String, default: '0' },
fields: [{ type: String }],
field_id: { type: Schema.Types.ObjectId, ref: 'Field', required: true },
timestamp: {
type: Date,
default: Date.now,
},
});
and my controller is
exports.getAllDevices = async (req, res) => {
try {
let devices = await Device.find({})
.sort({
timestamp: 'desc',
})
.populate('user_id', ['name']);
// Let us get the last value of each field
for (let i = 0; i < devices.length; i++) {
for (let j = 0; j < devices[i].fields.length; j++) {
if (devices[i].fields[j] !== null && devices[i].fields[j] !== '') {
await influx
.query(
`select last(${devices[i].fields[j]}), ${devices[i].fields[j]} from mqtt_consumer where topic = '${devices[i].device_id}'`
)
.then((results) => {
************** Problem occurs here **************
if (results.length > 0) {
devices[i].fields[j] = {
name: devices[i].fields[j],
last: results[0].last,
};
} else {
devices[i].fields[j] = {
name: devices[i].fields[j],
last: 0,
};
}
************** Problem occurs here **************
});
}
}
}
// Return the results
res.status(200).json({
status: 'Success',
length: devices.length,
data: devices,
});
} catch (err) {
console.log(err);
res.status(500).json({
error: err,
});
}
};
It actually gets data from InfluxDB and appends it to fields property which was fetched from MongoDB as mentioned in my model. But it refused to append and CastError occurs.
After addition, it will look like this
I can't resolve this error after trying so many fixes. I don't know where I'm wrong. Please suggest to me some solution for this.
I can see you are not using devices variable as Mongoose Document. devices is an array of Documents.
I would like to suggest you to use lean() function to convert from Document to plain JavaScript object like
let devices = await Device.find({})
.sort({
timestamp: 'desc',
})
.populate('user_id', ['name'])
.lean();
async fetch() {
try {
console.log(await this.$api.events.all(-1, false)); // <-- First log statement
const res = await this.$api.events.all(-1, false); // <-- Assignment
console.log(res); // <-- Second log statement
if (!this.events) {
this.events = []
}
res.data.forEach((event, index) => {
const id = event.hashid;
const existingIndex = this.events.findIndex((other) => {
return other.hashid = id;
});
if (existingIndex == -1) {
this.events.push(events);
} else {
this.events[existingIndex] = event;
}
});
for (var i = this.events.length - 1; i >= 0; --i) {
const id = this.events[i].hashid
const wasRemoved =
res.data.findIndex((event) => {
return event.hashid == id
}) == -1
if (wasRemoved) {
this.events.splice(i, 1)
}
}
this.$store.commit('cache/updateEventData', {
updated_at: new Date(Date.now()),
data: this.events
});
} catch (err) {
console.log(err)
}
}
// The other functions, maybe this somehow helps
async function refreshTokenFirstThen(adminApi, func) {
await adminApi.refreshAsync();
return func();
}
all(count = -1, description = true) {
const func = () => {
return $axios.get(`${baseURL}/admin/event`, {
'params': {
'count': count,
'description': description ? 1 : 0
},
'headers': {
'Authorization': `Bearer ${store.state.admin.token}`
}
});
}
if (store.getters["admin/isTokenExpired"]) {
return refreshTokenFirstThen(adminApi, func);
}
return func();
},
Both log statements are giving slightly different results even though the same result is expected. But this only happens when is use the function in this specific component. When using the same function in other components, everything works as expected.
First data output:
[
{
"name": "First Name",
"hashid": "VQW9xg7j",
// some more correct attributes
},
{
"name": "Second name",
"hashid": "zlWvEgxQ",
// some more correct attributes
}
]
While the second console.log gives the following output:
[
{
"name": "First Name",
"hashid": "zlWvEgxQ",
// some more correct attributes, but this time with reactiveGetter and reactiveSetter
<get hashid()>: reactiveGetter()
length: 0
name: "reactiveGetter"
prototype: Object { … }
<prototype>: function ()
<set hashid()>: reactiveSetter(newVal)
length: 1
name: "reactiveSetter"
prototype: Object { … }
<prototype>: function ()
},
{
"name": "Second name",
"hashid": "zlWvEgxQ",
// some more correct attributes and still without reactiveGetter and reactiveSetter
}
]
As it can be seen, somehow the value of my hashid attribute changes, when assigning the response of the function call.
The next weird behavior happening here, is that the first object where the hashid field changes also gets reactiveGetter and reactiveSetter (but the second object in the array does not get these).
So it looks to me like something is happening with the assignment that I don't know about. Another guess would be that this has something to do with the Vuex store, because I do not change the Vuex tore in the other place where I use the same function.
It is verified that the backend always sends the correct data, as this is dummy data, consisting of an array with two objects with some attributes. So no other data except this two objects is expected.
Can someone explain to me why this behavior occurs?
There are few problems...
Do not use console.log with objects. Browsers tend to show "live view" of object - reference
this.events.findIndex((other) => { return other.hashid = id; }); is wrong, you are using assignment operator (=) instead of identity operator (===). That's why the hashid of the first element changes...
:)
I have run into an error using $function inside of an aggregation with MongoDB. I have an aggregation which should start with:
db.collection.aggregate([
{
$addFields: {
geohash: {
$function: {
body: function (coord1, coord2) {
let geohash = Geohash.encode(coord1, coord2);
return geohash
},
args: [
"$location.coordinates[0]",
"$location.coordinates[1]",
],
lang: "js",
},
},
},
},
])
However when i run the query it gives me this error: UnhandledPromiseRejectionWarning: MongoError: Invalid $addFields :: caused by :: The body function must be specified.
I am on version 4.4 of Mongo and running this code inside a Node.Js function. Any help would be much appreciated. Thanks in forward. :)
Btw, as reference i used this doc: https://docs.mongodb.com/manual/reference/operator/aggregation/function/
i try the same thing before and i found out, the function body has to be string and it seem you can not access function outside. so the solution for your issue could be something like this:
db.collection.aggregate([
{
$addFields: {
geohash: {
$function: {
body: `function (coord1, coord2) {
// NOTE: you need to define Geohash inside the function
let geohash = Geohash.encode(coord1, coord2);
return geohash
}`,
args: [
"$location.coordinates[0]",
"$location.coordinates[1]",
],
lang: "js",
},
},
},
},
])
this is what i try before:
const updateDocEx = [{
$set: {
status: 'Modified',
comments: {
$function: {body: `function (val, newVal) {
const vx = '';
const v1 = !val ? newVal : vx.concat(val, ',', newVal);
const v2 = v1.split(',');
const v3 = [...new Set(v2)];
return v3.join(',');
}`, args: ['$comments', 'A5'], lang: 'js'}
},
lastUpdate: '$$NOW'
},
}];
I have large array of objects and filtered the objects based on the userID. Here is the code below.
const filteredArr = LargeArr.Items.reduce(
async(acc, { attributes: { dob, name, picture} = { dob: null, name: null, picture: null }, userID }) => {
let pic = null;
if (picture) { pic = await getPic(picture); } // async here
acc[userID] = { name, userID, pic, dob };
return acc;
}, {});
Expected Output :
{
'1595232114269': {
name: 'Mark Status',
userID: '1595232114269',
picture: 'mark-status.jpg',
dob: '2020-08-10'
},
'48e69555d778f9b9a3a1d553b9c3b8f7dd6a3394ac82df1433b60a69c055d23d': {
name: 'Jack Thomas',
userID: '48e69555d778f9b9a3a1d553b9c3b8f7dd6a3394ac82df1433b60a69c055d23d',
picture: 'jack-thomas.jpg',
dob: '1990-12-20'
},
'48e69555d778f9b9a3a1d553b9c3b8f7dd6a3394ac82df1433b60a69c055d47p': {
name: 'Petro Huge',
userID: '48e69555d778f9b9a3a1d553b9c3b8f7dd6a3394ac82df1433b60a69c055d47p',
picture: 'petro huge.jpg',
dob: '1856-12-20'
},
'48e69555d778f9b9a3a1d553b9c3b8f7dd6a3394ac82df1433b60a69c055d55j': {
name: 'Mark Henry',
userID: '48e69555d778f9b9a3a1d553b9c3b8f7dd6a3394ac82df1433b60a69c055d55j',
picture: 'mark-henry.jpg',
dob: '2005-12-29'
}
}
I need to get picture from an api which is asynchronous, so used async await inside the reduce method. The problem here is it is always showing as Promise pending. If this was an array of object, then i can return Promise.all, but since this is object containing object how can i proceed with this inside reduce method? I need the exact same expected output.
Can somebody help me with this? Any help would be really appreciated.
To use reduce while iterating over items asynchronously, you'd have to have the accumulator which gets passed from callback to callback to be a Promise. While this is possible, it'll make things pretty difficult to read, and introduces some unnecessary syntax noise.
Use a plain for loop instead:
const filteredArr = {};
for (const item of LargeArr.Items) {
const { attributes: { dob, name, picture} = { dob: null, name: null, picture: null } } = item;
const pic = picture ? await getPic(picture) : null;
filteredArr[userID] = { name, uesrID, pic, dob };
}
If you really wanted to take the reduce route:
LargeArr.Items.reduce(
(acc, { attributes: { dob, name, picture} = { dob: null, name: null, picture: null }, userID }) => {
return acc.then(async (acc) => {
let pic = null;
if (picture) { pic = await getPic(picture); } // async here
acc[userID] = { name, userID, pic, dob };
return acc;
});
}, Promise.resolve({})
)
.then((filteredArr) => {
// do stuff with filteredArr
});
Unless the getPic calls need to be made in serial, you could consider using Promise.all instead, to iterate through the whole array at once, rather than waiting on the resolution of the prior Promise before going onto the next.
If your API can handle Promise.all:
const filteredArr = {};
await Promise.all(LargeArr.Items.map(async (item) => {
const { attributes: { dob, name, picture} = { dob: null, name: null, picture: null } } = item;
const pic = picture ? await getPic(picture) : null;
filteredArr[userID] = { name, uesrID, pic, dob };
}));
I apologize if the title of the question is misleading, because I am not too sure how to explain this. I have 2 files, matchedTransaction.js and player.js.
sharedApi/player.js
const MatchedTransactionModel = require('../models/matchedTransaction');
// #1: If I try to console log here, the output will be an empty object "{}"
console.log(MatchedTransactionModel);
async function banPlayer(userId) {
// ...
// Because MatchedTransactionModel is an empty object,
// the code below will crash with the following error:
// "MatchedTransactionModel.findOne is not a function"
const pendingMatchedTransaction = await MatchedTransactionModel.findOne({
$and: [
{
$or: [
{ reserverAccountId: `${account._id}` },
{ sellerAccountId: `${account._id}` },
],
},
{
$or: [
{ status: 'pendingReserverPayment' },
{ status: 'pendingSellerConfirmation' },
],
},
],
});
// ...
}
module.exports = {
banPlayer,
};
models/matchedTransaction.js
const mongoose = require('mongoose');
const { banPlayer } = require('../sharedApi/player');
const MatchedTransactionSchema = new mongoose.Schema([
{
createdDate: {
type: Date,
required: true,
},
// ...
},
]);
MatchedTransactionSchema.post('init', async function postInit() {
// ...
await banPlayer(userId);
});
const MatchedTransactionModel = mongoose.model('matchedTransactions', MatchedTransactionSchema);
module.exports = MatchedTransactionModel;
Notice that in player.js when I tried to console.log the required MatchedTransactionModel, it returns an empty object. However, if I made the following changes to matchedTransaction.js:
models/matchedTransaction.js
// Instead of requiring banPlayer outside, I only require it when it is used
// const { banPlayer } = require('../sharedApi/player');
MatchedTransactionSchema.post('init', async function postInit() {
// ...
const { banPlayer } = require('../sharedApi/player');
await banPlayer(userId);
});
// ...
The output of the previously mentioned console.log will be a non-empty object, and MatchedTransactionModel.findOne is working as expected.
Why does that happen?
The only problem I see with your code is that when you define the schema on matchedTransaction.js, you passed an array which I think is problematic and does not make sense. You must pass an object there:
const MatchedTransactionSchema = new mongoose.Schema({
createdDate: {
type: Date,
required: true,
},
// ...
});