首页 > 解决方案 > Mongoose $gte 日期搜索返回该日期之前的文档

问题描述

我有一个 Mongoose 模式/模型,其属性completedSetup是一种Date类型。

项目回购:https ://github.com/rmgreenstreet/custom-forms

const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');
const Schema = mongoose.Schema;
const Location = require('./location');
const Response = require('./response');
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const crypto = require('crypto');


const forbiddenWords = ['realtor', 'realty', 'realestate', 'agent', 'broker', 'admin'];

const userSchema = new Schema({
    firstname: {
        type: String,
        required: true
    },
    lastname: {
        type: String,
        required: true
    },
    username: String,
    personalEmail:{
        type:String,
        required:true,
        unique:true
    },
    role:{
        type:String,
        default:'User'
    },
    isCompanyAdmin: {
        type:Boolean,
        default: false
    },
    company: {
        type:Schema.Types.ObjectId,
        ref: 'Company'
    },
    location: {
        type:Schema.Types.ObjectId,
        ref: 'Location'
    },
    image: {
        url: {
            type:String,
            default:'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png'
        },
        public_id: String
    },
    isExpedited: {
        type:Boolean,
        default:false
    },
    isHidden: {
        type:Boolean,
        default:false
    },
    formAccessToken: {
        type: String,
        default: crypto.randomBytes(16).toString('hex')
    },
    completedSetup: Date,
    responses: [
        {
            type:Schema.Types.ObjectId,
            ref:'Response'
        }
    ],
    createAccountToken : {
        type: String,
        default: crypto.randomBytes(16).toString('hex')
    },
    resetPasswordToken : String,
    resetPasswordExpires: Date,
    created:{
        type:Date,
        default:Date.now()
    }
});

userSchema.plugin(passportLocalMongoose);

module.exports = mongoose.model('User',userSchema);

查询人createdDate

let beginDate = new Date();
beginDate.setMonth(beginDate.getMonth() - 6);
if (req.body.beginDate) {
    beginDate = new Date(req.body.beginDate);
}
let endDate = new Date();
if (req.body.endDate) {
    endDate = new Date(req.body.endDate);
}

const recentSetups = await User.find({completedSetup: {$gt: beginDate, $lte: endDate}});

这将返回所有Users,而不仅仅是带有completedSetupbetweenbeginDate和的那些endDate

奇怪的是,相同的查询在其他模式/模型上正确返回,但它们的日期设置不同。

在某些模型上,我有一个created默认设置为 的属性,该属性Date.now()在创建时设置,并且查询会返回这些属性。

然而,种子数据completedSetup使用pickaADate我定义的函数来选择前一年某个时间的日期,或者从当年到当月的日期(这是一个仍在开发中的投资组合项目):

const fs = require('fs');
const faker = require('faker');
const crypto = require('crypto');
const Company = require('./models/company');
const Location = require('./models/location');
const User = require('./models/user');
const Form = require('./models/form');
const Question = require('./models/question');
const Response = require('./models/response');

const sampleImages = fs.readdirSync('./public/images/seeds');

function flipACoin() {
    const yesOrNo = Math.floor(Math.random() *2);
    // console.log(yesOrNo);
    return yesOrNo;
}

async function pickADate() {
    return new Promise((resolve, reject) => {
        try {
            const today = new Date();
            const day = Math.ceil(Math.random() * 27);
            // const thisOrLastYear = flipACoin();
            const month = Math.ceil(Math.random() * today.getMonth())
            const returnDate = new Date(today.getFullYear() - flipACoin(),month,day);
            resolve(returnDate);
            return;
        } catch (err) {
            console.log(`Error creating random date: ${err.message}`);
            reject(Date.now());
            return;
        }
    });
};

async function seedDefaultQuestions() {
    try {
        console.log('clearing all default questions from database')
        await Question.deleteMany({});
    } catch (err) {
        console.log(err.message);
    }
    try {
        console.log('adding default questions to database')
        const defaultQuestionsJSON = await JSON.parse(await fs.readFileSync('./private/defaultQuestions.json'));
        for (let question of defaultQuestionsJSON) {
            // console.log(question);
            await Question.create(question);
        }
        console.log(`${defaultQuestionsJSON.length} default questions added to database`);
    } catch (err) {
        console.log(err.message);
    }

};


async function clearDatabase() {

    console.log('Clearing database \n Clearing Companies');
    await Company.deleteMany({});
    console.log('All Companies deleted \n Clearing Locations');
    await Location.deleteMany({});
    console.log('All Locations deleted \n Clearing Users');
    await User.deleteMany({role: {$ne:'owner'}});
    console.log('All Users deleted \n Clearing Forms');
    await Form.deleteMany({});
    console.log('All forms deleted \n Clearing responses');
    await Response.deleteMany({});
    console.log('Database cleared');

};


async function seedDatabase() {
    // const companyCount = Math.ceil(Math.random() * 200);
    const companyCount = 10;
    const defaultQuestions = await Question.find({isDefault:true});

    async function createLocations(companyId) {
        const locationCount = Math.ceil(Math.random() * 5);
        let locationsArr = [];
        for (let i = 0; i < locationCount; i++) {
            let isPrimary = false;
            if (i=== 0) {
                isPrimary = true;
            }
            const randomImageIndex = Math.ceil(Math.random() * sampleImages.length);
            const newLocation = await Location.create({
                primary: isPrimary,
                officeNumber: Math.ceil(Math.random() * 1000).toString(),
                name: faker.company.companyName(),
                phone: faker.phone.phoneNumber(),
                fax: faker.phone.phoneNumber(),
                address: {
                    streetNumber: Math.random(Math.ceil() * 1000),
                    streetName: faker.address.streetName(),
                    secondary: `Ste ${faker.random.alphaNumeric()}`,
                    city: faker.address.city(),
                    state: faker.address.stateAbbr(),
                    postal: faker.address.zipCode(),
                    country: 'USA'
                },
                website: faker.internet.url(),
                images: [
                    {
                        secure_url:`/images/seeds/${sampleImages[randomImageIndex]}`
                    }
                ],
                company: companyId,
                created: await pickADate()
            });
            await newLocation.save();
            newLocation.contacts = await createUsers(newLocation._id, companyId, 'Admin', (Math.ceil(Math.random() * 5)));
            await newLocation.save();
            console.log(`Location ${newLocation.name} created with ${newLocation.contacts.length} contacts`)
            await createUsers(newLocation._id, companyId, 'User', (Math.ceil(Math.random() * 30)));
            locationsArr.push(newLocation._id);
            await newLocation.addDefaultForm();
        }
        return locationsArr;
    };

    async function createUsers(locationId, companyId, role, count) {
        let contactsArr = [];
        for (let i = 0; i < count; i++) {
            const newFirstName = await faker.name.firstName();
            const newLastName = await faker.name.lastName();
            let newUser;
            try {
                newUser = await User.register({
                firstname: newFirstName,
                lastname: newLastName,
                username: newFirstName+newLastName,
                personalEmail: newFirstName+newLastName+'@test.com',
                role: role,
                company: companyId,
                location: locationId,
                formAccessToken: crypto.randomBytes(16).toString('hex'),
                createAccountToken: crypto.randomBytes(16).toString('hex'),
                created: await pickADate()
            },'password');
        } catch (err) {
            if (err.message.includes('UserExistsError')) {
                continue;
            }
        }
        if(role === 'User');{
            if(flipACoin()) {
                newUser.responses.push(await createResponse(newUser)); 
                await newUser.save();
            } else {
                continue;
            }                
            if(flipACoin()) {
                newUser.completedSetup = await pickADate(); 
                await newUser.save();
            } else {
                continue;
            }                
        };
        contactsArr.push(newUser._id);
        console.log(`${role} ${newUser.firstname} ${newUser.lastname} created`);
        };
        return contactsArr;
    };

    async function createResponse(user) {
        return new Promise(async (resolve, reject) => {
            console.log(`Creating a response for ${user.firstname}`)
            const makeString = (charLimit) => {
                let str = faker.lorem.paragraph()
                    if (str.length > charLimit) {
                        str = str.slice(0, charLimit - 1)
                    }
                    return str
                }
            let response = await Response.create({owner:user._id, created:await pickADate()});
            try {
                for (let question of defaultQuestions) {
                    const answer = {
                        questionId: question._id
                    }
                    if(question.inputType == 'Checkbox') {
                        answer.value = true;
                    }
                    if(question.inputType == 'Email') {
                        answer.value = faker.internet.email();
                    }
                    if(question.inputType == 'File') {
                        continue;
                    }
                    if(question.inputType == 'Image') {
                        continue;
                    }
                    if(question.inputType == 'Number') {
                        answer.value = Math.ceil(Math.random() * 99);
                    }
                    if(question.inputType == 'Password') {
                        answer.value = 'Pa55w0rd123';
                    }
                    if(question.inputType == 'Radio') {
                        question.value = 'No';
                    }
                    if(question.inputType == 'Select') {
                        question.value = "At&t";
                    }
                    if(question.inputType == 'Tel') {
                        answer.value = faker.phone.phoneNumber();
                    }
                    if (question.inputType == 'Text') {
                        if(question.maxLength) {
                            answer.value = makeString(question.maxLength);
                        } else {
                            answer.value = faker.lorem.words()
                        }
                    }
                    if(question.inputType == 'Textarea') {
                        answer.value = faker.lorem.paragraph();
                    }
                    if(question.inputType == 'Url') {
                        answer.value = faker.internet.url();
                    }
                    response.answers.push(answer);
                }
                await response.save();
                resolve(response._id);
                return;
            } catch (err) {
                console.log(`Error creating random response: ${err.message}.`);
                reject(response);
                return;
            }
        });
    }

    console.log(`Creating ${companyCount} companies`)
    for(let i = 0; i < companyCount; i++) {
        const newCompany = await Company.create({
            name: faker.company.companyName(),
            created: await pickADate()
        });
        newCompany.locations = await createLocations(newCompany._id);
        await newCompany.save();
        console.log(`${newCompany.name} created with ${newCompany.locations.length} locations`)
    }
    console.log('database seeded')
};

module.exports = {seedDatabase, clearDatabase, seedDefaultQuestions};

根据MDN,我认为问题在于该函数生成从去年年初到当前日期的随机日期,但老实说,我看不出new Date(yyyy(整数),monthIndex,day)与创建的日期有何不同Date.now()https ://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date在“个人日期和时间组件值”下

这是用户数据在 MongoDB 中的外观,completedSetup属性是最后一个,并且看起来格式正确:

在此处输入图像描述

最后,这里是一些查询返回的日志(全部达到字符限制):

[
{
    image: {
      url: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png'
    },
    role: 'Admin',
    isCompanyAdmin: false,
    isExpedited: false,
    isHidden: false,
    formAccessToken: 'de5459c2cbf3ca1cbdb0a5daceb6ab61',
    responses: [ 5e8b71b7e75a0726242637b9 ],
    createAccountToken: 'a826054b8055243c52247a656eed9340',
    created: 2020-03-04T06:00:00.000Z,
    _id: 5e8b71b6e75a0726242637b8,
    firstname: 'Larue',
    lastname: 'Armstrong',
    username: 'LarueArmstrong',
    personalEmail: 'LarueArmstrong@test.com',
    company: 5e8b71abe75a072624263688,
    location: 5e8b71b6e75a0726242637b5,
    __v: 1,
    completedSetup: 2020-03-07T06:00:00.000Z
  },
  {
    image: {
      url: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png'
    },
    role: 'User',
    isCompanyAdmin: false,
    isExpedited: false,
    isHidden: false,
    formAccessToken: '7bf9d040c0009691191f3122c14d3d51',
    responses: [ 5e8b71c2e75a07262426392a ],
    createAccountToken: 'b284c3f43fad0081f967f06926ee1d6d',
    created: 2019-11-25T06:00:00.000Z,
    _id: 5e8b71c1e75a072624263929,
    firstname: 'Lance',
    lastname: 'Wolff',
    username: 'LanceWolff',
    personalEmail: 'LanceWolff@test.com',
    company: 5e8b71bfe75a0726242638dd,
    location: 5e8b71bfe75a0726242638de,
    __v: 1,
    completedSetup: 2020-03-02T06:00:00.000Z
  },
  {
    image: {
      url: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png'
    },
    role: 'User',
    isCompanyAdmin: false,
    isExpedited: false,
    isHidden: false,
    formAccessToken: '319cd6d626a48617c012be6c25fe66a8',
    responses: [ 5e8b71c5e75a072624263977 ],
    createAccountToken: '43e9161a30f7ebd672315c761da9e3d7',
    created: 2019-06-03T05:00:00.000Z,
    _id: 5e8b71c5e75a072624263976,
    firstname: 'Kailey',
    lastname: 'Ruecker',
    username: 'KaileyRuecker',
    personalEmail: 'KaileyRuecker@test.com',
    company: 5e8b71bfe75a0726242638dd,
    location: 5e8b71bfe75a0726242638de,
    __v: 1,
    completedSetup: 2020-02-23T06:00:00.000Z
  },
  {
    image: {
      url: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png'
    },
    role: 'User',
    isCompanyAdmin: false,
    isExpedited: false,
    isHidden: false,
    formAccessToken: 'ccb5fc2c6ff671a8cb5bdcf432cbcb1b',
    responses: [ 5e8b71c7e75a0726242639bd ],
    createAccountToken: '8d3aa2ecd6e0da797d8cbf9846c78c9b',
    created: 2019-04-20T05:00:00.000Z,
    _id: 5e8b71c6e75a0726242639bc,
    firstname: 'Skyla',
    lastname: 'Dicki',
    username: 'SkylaDicki',
    personalEmail: 'SkylaDicki@test.com',
    company: 5e8b71bfe75a0726242638dd,
    location: 5e8b71bfe75a0726242638de,
    __v: 1,
    completedSetup: 2019-10-22T05:00:00.000Z
  },
  {
    image: {
      url: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png'
    },
    role: 'User',
    isCompanyAdmin: false,
    isExpedited: false,
    isHidden: false,
    formAccessToken: '0641d2cf09459724ffea35d5cc96e2f1',
    responses: [ 5e8b71c7e75a0726242639e0 ],
    createAccountToken: '0f5e1bfd23df18835378c314efbcb206',
    created: 2019-12-02T06:00:00.000Z,
    _id: 5e8b71c7e75a0726242639df,
    firstname: 'Rasheed',
    lastname: 'Walsh',
    username: 'RasheedWalsh',
    personalEmail: 'RasheedWalsh@test.com',
    company: 5e8b71bfe75a0726242638dd,
    location: 5e8b71bfe75a0726242638de,
    __v: 1,
    completedSetup: 2020-02-16T06:00:00.000Z
  },
  {
    image: {
      url: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png'
    },
    role: 'User',
    isCompanyAdmin: false,
    isExpedited: false,
    isHidden: false,
    formAccessToken: '7f584ded07bfe5b20e95ef72ed2a7749',
    responses: [ 5e8b71cae75a072624263a2b ],
    createAccountToken: '0934cd5e7ccb75d476ecc23624e491f1',
    created: 2020-02-27T06:00:00.000Z,
    _id: 5e8b71cae75a072624263a2a,
    firstname: 'Leanna',
    lastname: 'Kuphal',
    username: 'LeannaKuphal',
    personalEmail: 'LeannaKuphal@test.com',
    company: 5e8b71bfe75a0726242638dd,
    location: 5e8b71bfe75a0726242638de,
    __v: 1,
    completedSetup: 2019-11-02T05:00:00.000Z
  }
]

在完整列表(大约 60 多个结果)中,有几个是 2019 年 11 月之前的,就像这个片段中的那个来自 2019 年 10 月。

标签: javascriptnode.jsmongoose

解决方案


像往常一样,现在我发现了这个问题,我感到很愚蠢。

问题出在getPoints函数中。

它将接收到的每个子数组中的每个项目都视为一个点,并为它创建一个点。所以如果graphDatasets看起来像:

[
    ['Invitations','Completions','Setups'],
    [/* actual points data */]
]

并且点数数据有 7 个月的价值,它在points数组中增加了 3 个点,扩展了点数。

我不得不完全重写结构graphDatasets并相应地更新renderCanvas函数:

/* Don't know why I didn't have this as arrays of objects before, it's so much simpler */
const graphDatasets = [
              [
                {label:'Invitations',payload:recentInvitations,searchProperty:'created'},
                {label:'Completions',payload:recentCompletions,searchProperty:'created'},
                {label:'Setups',payload:recentSetups,searchProperty:'completedSetup'}
              ],
              [
                {label:'Companies',payload:recentCompanies,searchProperty:'created'},
                {label:'Locations',payload:recentLocations,searchProperty:'created'}
              ]             
            ];
//converts full object data from server into {month: String, count: Number} objects
//Add searchProperty argument
function getPoints(items, searchProperty) {
    let points = [];
//access the data to be turned into points using the value at the .payload property
    items.payload.forEach((item) => {
        /* find the date to compare against using the searchProperty provided */
        const workingDate = new Date(item[searchProperty])
        const monthYear = `${workingDate.getMonth() + 1}/${workingDate.getFullYear()}`;
        let existingPoint = points.find(point => point.x === monthYear);
        if (existingPoint) {
            existingPoint.y ++;
        } else {
            points.push({x:monthYear, y: 1});
        }
    });
    return points;
};

// Returns the max Y value in our data list
 function getMaxY(data) {
    var max = 0;

    for(var i = 0; i < data.length; i ++) {
        if(data[i].y > max) {
            max = data[i].y;
        }
    }
    max += 10 - max % 10;
    return max;
}
/* Removes objects from combined data where month matches, in order to draw only one copy of that 
month on the X axis */
function removeDuplicates(originalArray, objKey) {
    var trimmedArray = [];
    var values = [];
    var value;

    for(var i = 0; i < originalArray.length; i++) {
      value = originalArray[i][objKey];
      if(values.indexOf(value) === -1) {
        trimmedArray.push(originalArray[i]);
        values.push(value);
      }
    }
    return trimmedArray;
};

/* compare two arrays and if there are any missing months in either array, add them with a y value of 0, then sortby month/year */
function equalize(arr1, arr2) {
    let newArr = arr2.reduce(function (result, obj2) {
        if (arr1.some(obj1 => obj1['x'] === obj2['x'])) {
            return result;
        }
        return [...result, {'x' : obj2['x'], 'y':0}];
    }, arr1);
    newArr.sort(function (a, b) {
        a = a.x.split('/');
        b = b.x.split('/')
        return new Date(a[1], a[0], 1) - new Date(b[1], b[0], 1)
    });
    return newArr;
};

function renderCanvas(canvas, data) {
    console.log('drawing on canvas');
    const colors = ['indigo','blue','green','orange','purple','teal','fuschia'];

    if(canvas.getContext) {
        var xPadding = 30;
        var yPadding = 30;
        var xLength = canvas.width - yPadding;
        var yLength = canvas.height - xPadding;
        var pointsArr = [];
        data.forEach(function (obj) {
            pointsArr.push(getPoints(obj, obj.searchProperty));
        });
        for (let i = 0; i < pointsArr.length -1 ; i++) {
            pointsArr[i] = equalize(pointsArr[i], pointsArr[i+1]);
        };
        var combinedData = Array.prototype.concat.apply([], pointsArr);
        combinedData.sort(function(a,b) {
            return new Date(a.created) - new Date(b.created)
        });
        var filteredPoints = removeDuplicates(combinedData, 'x');

        /* cuts X axis into a number of sections double the number of points */ 
        var xSpacing = xLength / (filteredPoints.length * 2);

        var yMax = getMaxY(combinedData);

        var ctx = canvas.getContext('2d');
        ctx.font = 'italic 8pt sans-serif';
        ctx.beginPath();
        ctx.lineWidth = 6;
        ctx.moveTo(yPadding,0);
        ctx.lineTo(yPadding,yLength);
        ctx.lineTo(canvas.width,yLength);
        ctx.stroke();
        ctx.closePath();

        // Return the y pixel for a graph point
        function getYPixel(val) {
            return yLength - ((yLength / yMax) * (val));
        }

        // Return the y pixel for a graph point
        function getXPixel(val) {
            return ((xSpacing + yPadding) + (xSpacing * (2 * val)))
        }

        function drawLine(points, color, legendVal) {

            /* move one xSpacing out from y Axis */
            ctx.moveTo(yPadding + xSpacing, getYPixel(points[0].y));
            ctx.beginPath();
            ctx.fillStyle=color;
            ctx.strokeStyle=color;
            points.forEach((point) => {
                const x = getXPixel(points.indexOf(point));
                const y = (getYPixel(point.y)) ;
                ctx.lineWidth = 2;
                ctx.lineTo(x,y);
                ctx.closePath();
                ctx.stroke();
                ctx.beginPath();
                ctx.arc(x,y,5,0,360,false);
                ctx.fillText(point.y, x + 2, y - 15);
                ctx.closePath();
                ctx.fill();
                ctx.moveTo(x,y);
            });
            /* will need to update this (and other stuff) but use the label property of the data to label the line on the graph */
            ctx.moveTo(canvas.width - yPadding, getYPixel(points[points.length-1].y));
            ctx.fillText(legendVal, canvas.width - 20, getYPixel(points[points.length-1].y));
        }

        // Draw the X value texts
        for(var i = 0; i < filteredPoints.length; i ++) {
            if (i===0) {
                ctx.fillText(filteredPoints[i].x, yPadding, yLength +20);
            }else {
                ctx.fillText(filteredPoints[i].x, (yPadding) + (xSpacing * (2 * i)), yLength + 20);
            }
        }

        // Draw the Y value texts
        ctx.textAlign = "right"
        ctx.textBaseline = "middle";

        for(var i = 0; i <= yMax; i += 10) {
            if (i === yMax) {
                ctx.fillText(i, xPadding - 10, getYPixel(i-1));
            } else {
                ctx.fillText(i, xPadding - 10, getYPixel(i));
            }
        };

        pointsArr.forEach(async function (points) {
            drawLine(points, colors[pointsArr.indexOf(points)], data[pointsArr.indexOf(points)].label);
        });
    }
};


NodeList.prototype.indexOf = Array.prototype.indexOf
canvases.forEach((canvas) => {
    renderCanvas(
        canvas,
        graphDatasets[canvases.indexOf(canvas)]
        );
});

推荐阅读