Relative time queries (#4289)
* Add relative time queries
* Encode successful result
* Add integration test
* Add more error cases
* Remove unnecessary new Date
* Error when time has both 'in' and 'ago'
* naturalTimeToDate -> relativeTimeToDate
* Add $relativeTime operator
* Throw error if $relativeTime is invalid
* Add integration test for invalid relative time
* Exclude $exists query
* Only run integration tests on MongoDB
* Add it_only_db test helper
bd2ea87c1d/CONTRIBUTING.md (L23)
* Handle where val might be null or undefined
* Add integration test for multiple results
* Lowercase text before processing
* Always past if not future
* Precompute seconds multiplication
* Add shorthand for interval
hr, hrs
min, mins
sec, secs
* Throw error if $relativeTime is used with $exists, $ne, and $eq
* Improve coverage for relativeTimeToDate
* Add test for erroring on floating point units
* Remove unnecessary dropDatabase function
* Unit test $ne, $exists, $eq
* Verify field type
* Fix unit test for $exists
Unnest query object
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"Container": true,
|
||||
"equal": true,
|
||||
"notEqual": true,
|
||||
"it_only_db": true,
|
||||
"it_exclude_dbs": true,
|
||||
"describe_only_db": true,
|
||||
"describe_only": true,
|
||||
|
||||
@@ -347,3 +347,118 @@ describe('transformUpdate', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformConstraint', () => {
|
||||
describe('$relativeTime', () => {
|
||||
it('should error on $eq, $ne, and $exists', () => {
|
||||
expect(() => {
|
||||
transform.transformConstraint({
|
||||
$eq: {
|
||||
ttl: {
|
||||
$relativeTime: '12 days ago',
|
||||
}
|
||||
}
|
||||
});
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
transform.transformConstraint({
|
||||
$ne: {
|
||||
ttl: {
|
||||
$relativeTime: '12 days ago',
|
||||
}
|
||||
}
|
||||
});
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
transform.transformConstraint({
|
||||
$exists: {
|
||||
$relativeTime: '12 days ago',
|
||||
}
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('relativeTimeToDate', () => {
|
||||
const now = new Date('2017-09-26T13:28:16.617Z');
|
||||
|
||||
describe('In the future', () => {
|
||||
it('should parse valid natural time', () => {
|
||||
const text = 'in 12 days 10 hours 24 minutes 30 seconds';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2017-10-08T23:52:46.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('future');
|
||||
});
|
||||
});
|
||||
|
||||
describe('In the past', () => {
|
||||
it('should parse valid natural time', () => {
|
||||
const text = '2 days 12 hours 1 minute 12 seconds ago';
|
||||
const { result, status, info } = transform.relativeTimeToDate(text, now);
|
||||
expect(result.toISOString()).toBe('2017-09-24T01:27:04.617Z');
|
||||
expect(status).toBe('success');
|
||||
expect(info).toBe('past');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error cases', () => {
|
||||
it('should error if string is completely gibberish', () => {
|
||||
expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time should either start with 'in' or end with 'ago'",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if string contains neither `ago` nor `in`', () => {
|
||||
expect(transform.relativeTimeToDate('12 hours 1 minute')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time should either start with 'in' or end with 'ago'",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if there are missing units or numbers', () => {
|
||||
expect(transform.relativeTimeToDate('in 12 hours 1')).toEqual({
|
||||
status: 'error',
|
||||
info: 'Invalid time string. Dangling unit or number.',
|
||||
});
|
||||
|
||||
expect(transform.relativeTimeToDate('12 hours minute ago')).toEqual({
|
||||
status: 'error',
|
||||
info: 'Invalid time string. Dangling unit or number.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error on floating point numbers', () => {
|
||||
expect(transform.relativeTimeToDate('in 12.3 hours')).toEqual({
|
||||
status: 'error',
|
||||
info: "'12.3' is not an integer.",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if numbers are invalid', () => {
|
||||
expect(transform.relativeTimeToDate('12 hours 123a minute ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "'123a' is not an integer.",
|
||||
});
|
||||
});
|
||||
|
||||
it('should error on invalid interval units', () => {
|
||||
expect(transform.relativeTimeToDate('4 score 7 years ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "Invalid interval: 'score'",
|
||||
});
|
||||
});
|
||||
|
||||
it("should error when string contains 'ago' and 'in'", () => {
|
||||
expect(transform.relativeTimeToDate('in 1 day 2 minutes ago')).toEqual({
|
||||
status: 'error',
|
||||
info: "Time cannot have both 'in' and 'ago'",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3105,7 +3105,80 @@ describe('Parse.Query testing', () => {
|
||||
equal(result.has('testPointerField'), result.get('shouldBe'));
|
||||
});
|
||||
done();
|
||||
}
|
||||
).catch(done.fail);
|
||||
}).catch(done.fail);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should handle relative times correctly', function(done) {
|
||||
const now = Date.now();
|
||||
const obj1 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj1',
|
||||
ttl: new Date(now + 2 * 24 * 60 * 60 * 1000), // 2 days from now
|
||||
});
|
||||
const obj2 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj2',
|
||||
ttl: new Date(now - 2 * 24 * 60 * 60 * 1000), // 2 days ago
|
||||
});
|
||||
|
||||
Parse.Object.saveAll([obj1, obj2])
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: 'in 1 day' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '1 day ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.lessThan('ttl', { $relativeTime: '5 days ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then((results) => {
|
||||
expect(results.length).toBe(0);
|
||||
})
|
||||
.then(() => {
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '3 days ago' });
|
||||
return q.find({ useMasterKey: true });
|
||||
})
|
||||
.then((results) => {
|
||||
expect(results.length).toBe(2);
|
||||
})
|
||||
.then(done, done.fail);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should error on invalid relative time', function(done) {
|
||||
const obj1 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj1',
|
||||
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
|
||||
});
|
||||
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('ttl', { $relativeTime: '-12 bananas ago' });
|
||||
obj1.save({ useMasterKey: true })
|
||||
.then(() => q.find({ useMasterKey: true }))
|
||||
.then(done.fail, done);
|
||||
});
|
||||
|
||||
it_only_db('mongo')('should error when using $relativeTime on non-Date field', function(done) {
|
||||
const obj1 = new Parse.Object('MyCustomObject', {
|
||||
name: 'obj1',
|
||||
nonDateField: 'abcd',
|
||||
ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
|
||||
});
|
||||
|
||||
const q = new Parse.Query('MyCustomObject');
|
||||
q.greaterThan('nonDateField', { $relativeTime: '1 day ago' });
|
||||
obj1.save({ useMasterKey: true })
|
||||
.then(() => q.find({ useMasterKey: true }))
|
||||
.then(done.fail, done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -409,6 +409,14 @@ global.it_exclude_dbs = excluded => {
|
||||
}
|
||||
}
|
||||
|
||||
global.it_only_db = db => {
|
||||
if (process.env.PARSE_SERVER_TEST_DB === db) {
|
||||
return it;
|
||||
} else {
|
||||
return xit;
|
||||
}
|
||||
};
|
||||
|
||||
global.fit_exclude_dbs = excluded => {
|
||||
if (excluded.indexOf(process.env.PARSE_SERVER_TEST_DB) >= 0) {
|
||||
return xit;
|
||||
|
||||
Reference in New Issue
Block a user